diff --git a/.circleci/config.yml b/.circleci/config.yml index 00da359495..ed9ee61a35 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,24 +1,73 @@ version: 2.1 orbs: - python: circleci/python@0.2.1 + python: circleci/python@4.0.0 jobs: build-and-test: - working_directory: ~/circleci-demo-python-django - docker: - - image: circleci/python:3.8 # primary container for the build job - auth: - username: mydockerhub-user - password: $DOCKERHUB_PASSWORD # context / project UI env-var reference + executor: + name: python/default + tag: '3.10' # or '3.12' + environment: + # HuggingFace: disable xet and use cache directory + # NOTE @deruyter92 2026-05-07: "xet" opens many simultaneous connections + # to different data chunks. Currently doesn't work well with CircleCI. + # See: https://github.com/huggingface/xet-core/issues/800 + HF_HUB_DISABLE_XET: 1 + HF_HOME: ~/.cache/huggingface + steps: - checkout - - python/load-cache - - python/install-deps - - python/save-cache + + # Restore uv cache + - restore_cache: + name: Restore uv cache + keys: + - v2-uv-pip-{{ checksum "pyproject.toml" }} + - v2-uv-pip- + + # Restore HuggingFace weights cache + - restore_cache: + name: Restore Hugging Face cache + keys: + - hf-weights-v1-{{ checksum "pyproject.toml" }} + - hf-weights-v1- + + # Install uv + - run: + name: Install uv + command: | + pip install uv + + # Install DeepLabCut runtime deps only + - run: + name: Install DeepLabCut runtime deps only + command: | + uv pip install --system -e . + + # (Optional) Trim the cache for CI so uploads stay small and fast + - run: + name: Prune uv cache for CI + command: uv cache prune --ci || true + + # Save the uv cache for next runs + - save_cache: + name: Save uv cache + key: v2-uv-pip-{{ checksum "pyproject.toml" }} + paths: + - ~/.cache/uv + + # Test DLC - run: - command: python testscript_cli.py name: TestDLC + command: python testscript_cli.py + + # Save the HF weights cache for next runs + - save_cache: + name: Save huggingface cache + key: hf-weights-v1-{{ checksum "pyproject.toml" }} + paths: + - ~/.cache/huggingface workflows: main: diff --git a/.codespellrc b/.codespellrc index 5afad804bb..b46cf0e61f 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,5 +1,5 @@ [codespell] -skip = .git,*.pdf,*.svg,deeplabcut/pose_estimation_tensorflow/models/pretrained +skip = .git,*.pdf,*.svg,*.ipynb,deeplabcut/pose_estimation_tensorflow/models/pretrained # MOT,SIE - legit acronyms # tThe - for \tThe. codespell is not good detecting those yet -ignore-words-list = mot,sie,tthe +ignore-words-list = mot,sie,tthe,assertin,bu,td,ctd,wither diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a52a2bd42a..764dfe563b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: Bug Report description: File a bug report to help us improve assignees: - - n-poulsen + - mmathislab #temp body: - type: markdown attributes: @@ -14,28 +14,28 @@ body: options: - label: I have searched the existing issues required: true - - type: textarea - id: what-happened - attributes: - label: Bug description - description: Also tell us concisely what you expected to happen - placeholder: What happened? - validations: - required: true - type: textarea attributes: label: Operating System description: What operating system are you using? placeholder: macOS Big Sur - value: operating system validations: required: true - type: textarea attributes: label: DeepLabCut version description: What version of DLC are you using? Please check with `import deeplabcut`, `deeplabcut.__version__` - placeholder: 2.2rc3 - value: dlc version + placeholder: 3.0.0 + validations: + required: true + - type: dropdown + id: backend-engine + attributes: + label: What engine are you using? + options: + - pytorch + - tensorflow + - both (rare!) validations: required: true - type: dropdown @@ -53,20 +53,25 @@ body: label: Device type description: What GPU/CPU are you using? placeholder: GeForce 2080 RTX - value: gpu + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: Bug description 🐛 + description: Also tell us concisely what you expected to happen + placeholder: What happened? validations: required: true - type: textarea attributes: label: Steps To Reproduce - description: Steps to reproduce the behavior. + description: Please provide a minimal example to reproduce the behavior. placeholder: | 1. In this environment... 2. With this config... 3. Run '...' 4. See error... - validations: - required: false - type: textarea id: logs attributes: @@ -80,13 +85,10 @@ body: Links? References? Anything that will give us more context about the issue you are encountering! Tip: You can attach images and other files by clicking this area to highlight it and then dragging files in. - validations: - required: false - type: checkboxes attributes: label: Code of Conduct - description: The Code of Conduct helps create a safe space for everyone. We require - that everyone agrees to it. + description: The Code of Conduct helps create a safe space for everyone. We require that everyone agrees to it. options: - label: I agree to follow this project's [Code of Conduct](https://github.com/DeepLabCut/DeepLabCut/blob/master/CODE_OF_CONDUCT.md) required: true diff --git a/.github/workflows/build-book.yml b/.github/workflows/build-book.yml new file mode 100644 index 0000000000..a1c55b9a0e --- /dev/null +++ b/.github/workflows/build-book.yml @@ -0,0 +1,51 @@ +name: Build (and optionally deploy) Jupyter Book + +on: + workflow_call: + inputs: + python-version: + description: "Python version used to build the docs." + required: false + default: "3.10" + type: string + build_dir: + required: false + default: "./_build/html" + type: string + upload_artifact: + description: "If true, upload the built site as an artifact." + required: false + default: false + type: boolean + + + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install docs dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[docs] + + - name: Build the book + run: jupyter-book build . + + - name: Upload built site artifact + if: ${{ inputs.upload_artifact }} + uses: actions/upload-artifact@v6 + with: + name: built-book + path: ${{ inputs.build_dir }} + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 243ba8ce5f..5c61d13720 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -5,7 +5,8 @@ on: push: branches: [main] pull_request: - branches: [main] + types: [opened, synchronize, reopened] + branches: [ main ] jobs: codespell: @@ -14,6 +15,8 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + - name: Annotate locations with typos + uses: codespell-project/codespell-problem-matcher@v1 - name: Codespell - uses: codespell-project/actions-codespell@v1 + uses: codespell-project/actions-codespell@v2 diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml new file mode 100644 index 0000000000..b35b68eda7 --- /dev/null +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -0,0 +1,59 @@ +name: Docs & notebooks freshness and formatting checks + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + staleness: + name: Docs and notebooks scan (read-only) + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repository (full history for git dates) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install staleness tool dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "pydantic>=2,<3" pyyaml "nbformat>=5" + + - name: Run staleness report (read-only) + run: | + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + report + + + # Optional: run check mode (will fail only once you populate allowlists in config) + - name: Run staleness policy check (optional gate) + continue-on-error: true + run: | + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + --no-step-summary \ + check + + - name: Upload staleness artifacts + uses: actions/upload-artifact@v4 + with: + name: staleness-report + path: | + tmp/docs_nb_checks/*.json + tmp/docs_nb_checks/*.md + if-no-files-found: error diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..9c2ced892f --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,119 @@ +name: pre-commit (PR only on changed files) + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + detect_changes: + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.changed_files.outputs.changed }} + changed_python: ${{ steps.changed_python.outputs.changed_python }} + + steps: + - name: Checkout full history + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Detect changed files + id: changed_files + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + { + echo "changed<> "$GITHUB_OUTPUT" + + - name: Detect changed Python files + id: changed_python + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_PYTHON=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(py|pyi|ipynb)$' || true) + + { + echo "changed_python<> "$GITHUB_OUTPUT" + + - name: Show changed files + run: | + echo "Changed files:" + echo "${{ steps.changed_files.outputs.changed }}" + + echo + echo "Changed Python files:" + echo "${{ steps.changed_python.outputs.changed_python }}" + + precommit: + needs: detect_changes + runs-on: ubuntu-latest + if: ${{ needs.detect_changes.outputs.changed != '' }} + + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install tooling + run: pip install pre-commit ruff + + - name: Run pre-commit (CI check-only stage) on changed files + id: precommit_run + continue-on-error: true + env: + CHANGED_FILES: ${{ needs.detect_changes.outputs.changed }} + run: | + mapfile -t files <<< "$CHANGED_FILES" + pre-commit run --hook-stage manual --files "${files[@]}" --show-diff-on-failure + + - name: Generate Ruff Markdown report + id: ruff_report + if: ${{ always() && needs.detect_changes.outputs.changed_python != '' }} + env: + CHANGED_PYTHON: ${{ needs.detect_changes.outputs.changed_python }} + run: | + mkdir -p tmp + mapfile -t pyfiles <<< "$CHANGED_PYTHON" + python tools/ruff_report.py "${pyfiles[@]}" --output tmp/ruff-report.md + + - name: Add short Ruff report to GitHub Actions summary + if: ${{ always() && steps.precommit_run.outcome == 'failure' && needs.detect_changes.outputs.changed_python != '' }} + run: | + { + echo "# Lint summary" + echo + echo "## Ruff report (top section)" + echo + sed -n '1,80p' tmp/ruff-report.md + echo + echo "_Full report uploaded as workflow artifact: `ruff-report`_" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Ruff report artifact + if: ${{ always() && needs.detect_changes.outputs.changed_python != '' }} + uses: actions/upload-artifact@v6 + with: + name: ruff-report + path: tmp/ruff-report.md + + - name: Fail job if pre-commit failed + if: ${{ steps.precommit_run.outcome == 'failure' }} + run: | + echo "pre-commit reported failures" + exit 1 diff --git a/.github/workflows/intelligent-testing.yml b/.github/workflows/intelligent-testing.yml new file mode 100644 index 0000000000..ae85292c75 --- /dev/null +++ b/.github/workflows/intelligent-testing.yml @@ -0,0 +1,167 @@ +name: "Intelligent Python Testing" + +on: + push: + branches: [ main ] + pull_request: + types: [opened, synchronize, reopened] + branches: [ main ] + +concurrency: + group: intelligent-${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + intelligent-test-selection: + name: Select test plan + runs-on: ubuntu-latest + outputs: + run_skip: ${{ steps.selector.outputs.run_skip }} + run_docs: ${{ steps.selector.outputs.run_docs }} + run_fast: ${{ steps.selector.outputs.run_fast }} + run_full: ${{ steps.selector.outputs.run_full }} + selected_workflows: ${{ steps.selector.outputs.selected_workflows }} + lane_reasons: ${{ steps.selector.outputs.lane_reasons }} + pytest_paths: ${{ steps.selector.outputs.pytest_paths }} + functional_scripts: ${{ steps.selector.outputs.functional_scripts }} + reasons: ${{ steps.selector.outputs.reasons }} + changed_files: ${{ steps.selector.outputs.changed_files }} + provenance: ${{ steps.selector.outputs.provenance }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # needed for merge-base/diff to be reliable + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Run selector + generate report + id: selector + run: | + python -m pip install --upgrade pip + pip install "pydantic>=2,<3" + python tools/test_selector.py \ + --write-github-output \ + --write-summary \ + --report-dir tmp/test-selection \ + --json + + - name: Upload selector report + uses: actions/upload-artifact@v6 + with: + name: test-selection-report + path: | + tmp/test-selection/selection.json + tmp/test-selection/decision.md + retention-days: 7 + if-no-files-found: error + + + + skip: + name: Skipped (lint config change) + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_skip == 'true' + runs-on: ubuntu-latest + steps: + - run: | + echo "Only lint config files changed; skipping docs/tests." + echo "Pre-commit workflow is responsible for lint config validation." + + + docs: + name: Docs build + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_docs == 'true' + uses: ./.github/workflows/build-book.yml + with: + python-version: "3.10" + build_dir: "./_build/html" + upload_artifact: false + secrets: inherit + + + fast-tests: + name: Fast lane (targeted pytest + selected functional) + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_fast == 'true' + uses: ./.github/workflows/python-package.yml + with: + concurrency_key: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || '' }} + matrix_json: >- + {"include":[{"os":"ubuntu-latest","python-version":"3.12", "extras": "[tf]"}]} + pytest_paths_json: ${{ needs.intelligent-test-selection.outputs.pytest_paths }} + functional_scripts_json: ${{ needs.intelligent-test-selection.outputs.functional_scripts }} + full_suite: false + + full-tests: + name: Full matrix tests + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_full == 'true' + uses: ./.github/workflows/python-package.yml + with: + concurrency_key: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || '' }} + matrix_json: >- + { + "include": [ + {"os":"ubuntu-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"ubuntu-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"ubuntu-latest","python-version":"3.12", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.12", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.12", "extras": "[tf]"} + ] + } + full_suite: true + + tf-install-smoke-test: + name: TensorFlow install smoke test + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_full == 'true' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.10', '3.11', '3.12'] + # Run smoke test on the extras that are not tested in the full matrix tests + extras: ["[tf-cu11]", "[tf-cu12]"] + exclude: + - os: windows-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.12' + - extras: "[tf-cu11]" + python-version: '3.12' + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python + uses: conda-incubator/setup-miniconda@v3 + with: + channels: conda-forge + channel-priority: strict + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + shell: bash -el {0} + run: | + python -m ensurepip --upgrade + python -m pip install --upgrade pip setuptools wheel + python -m pip install dependency-groups + python -m pip install --no-cache-dir -e ".${{ matrix.extras }}" --group dev + + - name: Run TensorFlow install smoke test + shell: bash -el {0} + run: | + pytest tests/test_tf_install_smoke.py diff --git a/.github/workflows/lockfile-check.yml b/.github/workflows/lockfile-check.yml new file mode 100644 index 0000000000..5783029fcf --- /dev/null +++ b/.github/workflows/lockfile-check.yml @@ -0,0 +1,37 @@ +name: Lockfile + +on: + pull_request: + paths: + - "pyproject.toml" + - "uv.lock" + # - ".python-version" + - ".github/workflows/uv-lockfile-check.yml" + push: + branches: [main] + paths: + - "pyproject.toml" + - "uv.lock" + # - ".python-version" + - ".github/workflows/uv-lockfile-check.yml" + +jobs: + uv-lockfile-check: + name: uv.lock is current + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: pyproject.toml + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.10.10" + + - name: Check lockfile + run: uv lock --check diff --git a/.github/workflows/publish-book.yml b/.github/workflows/publish-book.yml index 426e87370e..7996b0672b 100644 --- a/.github/workflows/publish-book.yml +++ b/.github/workflows/publish-book.yml @@ -2,32 +2,34 @@ name: publish-book on: push: - branches: - - main + branches: [ main ] + +permissions: + contents: write jobs: - deploy-book: + build: + uses: ./.github/workflows/build-book.yml + with: + python-version: "3.10" + build_dir: "./_build/html" + upload_artifact: true + secrets: inherit + + deploy: + needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install .[tf,docs] - pip install jupyter-book sphinxcontrib-mermaid - - - name: Build the book - run: | - jupyter-book build . + - name: Download built site artifact + uses: actions/download-artifact@v4 + with: + name: built-book + path: site - - name: GitHub Pages action - uses: peaceiris/actions-gh-pages@v3.9.3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./_build/html + - name: Deploy via gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ffe28dea22..db2a30a1f4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,66 +1,250 @@ name: Python package on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + workflow_call: + # This workflow can be called by other workflows (like intelligent-testing.yml) + inputs: + concurrency_key: + required: false + type: string + matrix_json: + required: true + type: string + pytest_paths_json: + required: false + type: string + default: "[]" + functional_scripts_json: + required: false + type: string + default: "[]" + full_suite: + required: false + type: boolean + default: false + + workflow_dispatch: + inputs: + concurrency_key: + description: "Optional stable key to dedupe manual runs (for example: pr-123)" + required: false + type: string + matrix_json: + description: "JSON matrix definition" + required: true + type: string + default: '{"include":[{"os":"ubuntu-latest","python-version":"3.12","extras": ""}]}' + pytest_paths_json: + description: "JSON array of pytest paths" + required: false + type: string + default: "[]" + functional_scripts_json: + description: "JSON array of functional scripts" + required: false + type: string + default: "[]" + full_suite: + description: "Run full pytest and default functional suite" + required: false + type: boolean + default: false jobs: build: runs-on: ${{ matrix.os }} + # Cancel outdated runs on the same OS and Python version when new commits are pushed + # Only cancels on PRs. + # Use a stable concurrency key only when one is explicitly provided + # (e.g. PR/workflow_call/manual dedupe). Otherwise fall back to github.run_id + # so pushes to main never cancel each other. + concurrency: + group: >- + tests-${{ github.workflow }}- + ${{ github.event_name == 'workflow_call' && inputs.concurrency_key + || github.event_name == 'workflow_dispatch' && inputs.concurrency_key + || github.run_id }}- + ${{ matrix.os }}-${{ matrix.python-version }} + cancel-in-progress: true strategy: fail-fast: false - matrix: - os: [ubuntu-latest, macos-13, windows-latest] - python-version: [3.9, "3.10"] - include: - - os: ubuntu-latest - path: ~/.cache/pip - - os: macos-13 - path: ~/Library/Caches/pip - - os: windows-latest - path: ~\AppData\Local\pip\Cache - exclude: - - os: macos-13 - python-version: 3.7 - - os: windows-latest - python-version: 3.7 + matrix: ${{ fromJson(inputs.matrix_json) }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 + + - name: Free up disk space + if: runner.os == 'Linux' + run: | + echo "Disk space before cleanup:" + df -h + # Remove unnecessary software to free up disk space + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + echo "Disk space after cleanup:" + df -h - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - name: Set up Python + uses: conda-incubator/setup-miniconda@v3 with: + channels: conda-forge + channel-priority: strict python-version: ${{ matrix.python-version }} - name: Install dependencies + shell: bash -el {0} # Important to enable conda env run: | + python -m ensurepip --upgrade python -m pip install --upgrade pip setuptools wheel - pip install -r requirements.txt + python -m pip install dependency-groups + python -m pip install --no-cache-dir -e ".${{ matrix.extras }}" --group dev - - name: Install ffmpeg + - name: Install ffmpeg (Linux/macOS) + if: runner.os != 'Windows' + shell: bash run: | if [ "$RUNNER_OS" == "Linux" ]; then sudo apt-get update - sudo apt-get install ffmpeg + sudo apt-get install -y ffmpeg elif [ "$RUNNER_OS" == "macOS" ]; then - brew install ffmpeg - else - choco install ffmpeg + brew install ffmpeg || true fi - shell: bash + - name: Install ffmpeg (Windows, pinned monthly BtbN build) + # NOTE: The pinned version should be retained for ~2 years. This WILL fail if the BtbN release is removed, + # so if you are two years in the future and this step fails, please check the builds. Thanks. + if: runner.os == 'Windows' + shell: pwsh + env: + FFMPEG_TAG: autobuild-2026-03-31-13-11 + FFMPEG_ASSET: ffmpeg-N-123777-g53537f6cf5-win64-gpl-shared.zip + run: | + $ErrorActionPreference = "Stop" + + $tag = $env:FFMPEG_TAG + $asset = $env:FFMPEG_ASSET + + $baseUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/$tag" + $url = "$baseUrl/$asset" + $checksumsUrl = "$baseUrl/checksums.sha256" + + $tmpRoot = Join-Path $env:RUNNER_TEMP "ffmpeg-install" + $zip = Join-Path $tmpRoot $asset + $checksums = Join-Path $tmpRoot "checksums.sha256" + $dest = Join-Path $tmpRoot "ffmpeg" + + if (Test-Path -LiteralPath $tmpRoot) { + Remove-Item -LiteralPath $tmpRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $tmpRoot | Out-Null + + Invoke-WebRequest -Uri $url -OutFile $zip + Invoke-WebRequest -Uri $checksumsUrl -OutFile $checksums + + $expected = Get-Content -LiteralPath $checksums | + ForEach-Object { + if ($_ -match '^(?[0-9A-Fa-f]{64})\s+\*?(?.+)$' -and $matches.name.Trim() -eq $asset) { + $matches.sha.ToLowerInvariant() + } + } | + Select-Object -First 1 - - name: Run pytest tests + if (-not $expected) { + throw "Could not find checksum for $asset in $checksums" + } + + $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + throw "FFmpeg checksum mismatch. Expected $expected but got $actual" + } + + Expand-Archive -LiteralPath $zip -DestinationPath $dest -Force + $ffdir = Get-ChildItem -LiteralPath $dest -Directory | Select-Object -First 1 + if (-not $ffdir) { + throw "Could not find extracted FFmpeg directory." + } + + $binDir = Join-Path $ffdir.FullName "bin" + $binDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Verify ffmpeg/ffprobe available + shell: bash -el {0} + run: | + set -e + ffmpeg -version + ffprobe -version + + - name: Run pytest + shell: bash -el {0} + env: + FULL_SUITE: ${{ inputs.full_suite }} + PYTEST_PATHS_JSON: ${{ inputs.pytest_paths_json }} run: | - pip install pytest - python -m pytest + python - << 'PY' + import json + import os + import subprocess + import sys - - name: Run functional tests + full_suite = os.environ["FULL_SUITE"].lower() == "true" + + if full_suite: + cmd = [sys.executable, "-m", "pytest"] + print("Running full pytest suite") + else: + paths = json.loads(os.environ.get("PYTEST_PATHS_JSON", "[]")) + if not paths: + print("No pytest paths selected; skipping pytest.") + raise SystemExit(0) + cmd = [sys.executable, "-m", "pytest", *paths] + print("Running targeted pytest:", " ".join(paths)) + + raise SystemExit(subprocess.call(cmd)) + PY + + - name: Run functional scripts + shell: bash -el {0} + env: + FULL_SUITE: ${{ inputs.full_suite }} + FUNCTIONAL_SCRIPTS_JSON: ${{ inputs.functional_scripts_json }} run: | - pip install git+https://github.com/${{ github.repository }}.git@${{ github.sha }} - python examples/testscript.py - python examples/testscript_multianimal.py + python - << 'PY' + import json + import os + import subprocess + import sys + + def is_windows_python_3_11_or_greater() -> bool: + """Aligned with pyproject: .[tf*] omits TensorFlow on Windows for Python 3.11+.""" + return sys.platform == "win32" and sys.version_info >= (3, 11) + + full_suite = os.environ["FULL_SUITE"].lower() == "true" + if full_suite: + scripts = [ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ] + else: + scripts = json.loads(os.environ.get("FUNCTIONAL_SCRIPTS_JSON", "[]")) + if not scripts: + print("No functional scripts selected; skipping functional tests.") + raise SystemExit(0) + + for script in scripts: + if "tensorflow" in script and is_windows_python_3_11_or_greater(): + ver = f"{sys.version_info.major}.{sys.version_info.minor}" + print( + f"Skipping TensorFlow example on Windows {ver} (no TF in .[tf*] for 3.11+): {script}" + ) + continue + print("Running:", script) + rc = subprocess.call([sys.executable, script]) + if rc != 0: + raise SystemExit(rc) + PY diff --git a/.gitignore b/.gitignore index 86a6943487..7656027ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,9 @@ _build/* #Data and examples /examples/open* /examples/Reac* -/examples/TES* -/examples/multi* -/examples/3D* /examples/m3* /examples/OUT +/examples/pretrained* .local .DS_Store examples/.DS_Store @@ -18,6 +16,15 @@ examples/.DS_Store *.ckpt snapshot-* +# Modelzoo checkpoints +deeplabcut/modelzoo/checkpoints/ + +# PyTorch backbone weights +deeplabcut/pose_estimation_pytorch/models/backbones/pretrained_weights/ + +# Wandb files +wandb/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -115,7 +122,10 @@ ENV/ # Spyder project settings .spyderproject .spyproject + +# IDEs configurations .vscode/* +.idea/* # Rope project settings .ropeproject @@ -125,3 +135,16 @@ ENV/ # mypy .mypy_cache/ + +# Tools output +tmp/* + +# Test data +tests/data/* + +# Automated docs checks +**/tmp/docs_nb_checks/ + + +# Automatic test selection report +**/tmp/test-selection/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..a5956fde1e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,105 @@ +default_stages: [pre-commit] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + # These are safe to run in both local & CI (they don't require "fix vs check" split) + - id: check-added-large-files + stages: [pre-commit, manual] + - id: check-yaml + stages: [pre-commit, manual] + - id: check-toml + stages: [pre-commit, manual] + - id: check-merge-conflict + stages: [pre-commit, manual] + - id: name-tests-test + args: [--pytest-test-first] + stages: [pre-commit, manual] + - id: check-json + stages: [pre-commit, manual] + + # These modify files. Run locally only (pre-commit stage). + - id: end-of-file-fixer + stages: [pre-commit] + - id: trailing-whitespace + stages: [pre-commit] + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.19.0 + hooks: + - id: pyproject-fmt + stages: [pre-commit] # modifies -> local only + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.25 + hooks: + - id: validate-pyproject + stages: [pre-commit, manual] + + # NOTE: @C-Achard 2026-03-18 disabled for now + # It had its use in introducing and enforcing linting, especially for docstrings + # but now ruff should be our de-facto linter. + # Only re-enable if we end up requiring large-scale docstring reformatting + # or we need some features from this in the future + # - repo: https://github.com/PyCQA/docformatter + # rev: v1.7.7 + # hooks: + # - id: docformatter + # name: docformatter (fix) + # args: [--wrap-descriptions=88, --wrap-summaries=88, --in-place, --black] + # stages: [pre-commit] + + # - id: docformatter + # name: docformatter (ci) + # args: [--wrap-descriptions=88, --wrap-summaries=88, --check, --black] + # stages: [manual] + + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + # -------------------------- + # LOCAL AUTOFIX (developers) + # -------------------------- + - id: ruff-check + name: ruff-check (fix) + args: [--fix, --unsafe-fixes] + stages: [pre-commit] + + - id: ruff-format + name: ruff-format (write) + stages: [pre-commit] + + # -------------------------- + # CI CHECK-ONLY (no writes) + # -------------------------- + - id: ruff-check + name: ruff-check (ci) + args: [--output-format=github] + stages: [manual] + + - id: ruff-format + name: ruff-format (ci) + args: [--check, --diff] + stages: [manual] + + # check only, no modifications + - repo: local + hooks: + - id: dlc-docs-notebooks-check + name: DLC docs+notebooks staleness/check + nbformat validate + normalization + entry: python tools/docs_and_notebooks_check.py + language: python + pass_filenames: true + files: ^(docs/|examples/(JUPYTER|COLAB)/|tools/).*(\.md|\.ipynb)$ + args: + - --config + - tools/docs_and_notebooks_report_config.yml + - check + - --targets + additional_dependencies: + - "pydantic>=2,<3" + - "pyyaml" + - "nbformat>=5" + stages: [pre-commit, manual] diff --git a/AUTHORS b/AUTHORS index 9fe42d2f98..d53068678f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,5 @@ DeepLabCut (www.deeplabcut.org) was initially developed by -Alexander & Mackenzie Mathis in collaboration with Matthias Bethge. +Alexander & Mackenzie Mathis in collaboration with Matthias Bethge in 2017. It is actively developed by Alexander & Mackenzie Mathis (steering council and owners). DeepLabCut is an open-source tool and has benefited from suggestions and edits by many @@ -82,23 +82,23 @@ T Nath, https://github.com/meet10may Preprint: Multi-animal pose estimation and tracking with DeepLabCut -J Lauer, M Zhou, S Ye, W Menegas, S Schneider, T Nath, MM Rahman, V Di Santo, +J Lauer, M Zhou, S Ye, W Menegas, S Schneider, T Nath, MM Rahman, V Di Santo, D Soberanes, G Feng, VN Murthy, G Lauder, C Dulac, M Mathis, A Mathis (2021). https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1 -Publication: +Publication: Multi-animal pose estimation, identification and tracking with DeepLabCut -Lauer, J., Zhou, M., Ye, S., Menegas, W., Schneider, S., Nath, T., Rahman, M.M., -Di Santo, V., Soberanes, D., Feng, G., Murthy, V.N., Lauder, G.V., Dulac, C., -Mathis, M.W., & Mathis, A. (2022). +Lauer, J., Zhou, M., Ye, S., Menegas, W., Schneider, S., Nath, T., Rahman, M.M., +Di Santo, V., Soberanes, D., Feng, G., Murthy, V.N., Lauder, G.V., Dulac, C., +Mathis, M.W., & Mathis, A. (2022). Nature Methods, 19, 496 - 504. -Conceptualization was done by A.M. and M.W.M. Formal analysis and code were done by J.L., A.M. and M.W.M. -New deep architectures were designed by M.Z., S.Y. and A.M. GUIs were done by J.L., M.W.M. and T.N. -Benchmark was set by S.S., M.W.M., A.M. and J.L. Marmoset data were gathered by W.M. and G.F. +Conceptualization was done by A.M. and M.W.M. Formal analysis and code were done by J.L., A.M. and M.W.M. +New deep architectures were designed by M.Z., S.Y. and A.M. GUIs were done by J.L., M.W.M. and T.N. +Benchmark was set by S.S., M.W.M., A.M. and J.L. Marmoset data were gathered by W.M. and G.F. Marmoset behavioral analysis was carried out by W.M. Parenting data were gathered by M.M.R., A.M. and C.D. -Tri-mouse data were gathered by D.S., A.M. and V.N.M. Fish data were gathered by V.D.S. and G.L. -The article was written by A.M., M.W.M. and J.L. with input from all authors. +Tri-mouse data were gathered by D.S., A.M. and V.N.M. Fish data were gathered by V.D.S. and G.L. +The article was written by A.M., M.W.M. and J.L. with input from all authors. M.W.M. and A.M. co-supervised the project. ############################################################################################################ @@ -108,8 +108,32 @@ A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab J Lauer, jessy@deeplabcut.org | https://github.com/jeylau N Poulsen, neils.poulsen@epfl.ch | https://github.com/n-poulsen -S Ye, https://github.com/yeshaokai +S Schneider, stes@hey.com | https://github.com/stes +S Ye, shaokai.ye@epfl.ch | https://github.com/yeshaokai -Preprint: -Ye, S., Filippova, A., Lauer, J., Vidal, M., Schneider, S., Qiu, T., Mathis, A., & Mathis, M.W. (2022). +Preprint: +Ye, S., Filippova, A., Lauer, J., Schneider, S., Vidal, M., Qiu, T., Mathis, A., & Mathis, M.W. (2023). SuperAnimal pretrained pose estimation models for behavioral analysis. https://arxiv.org/abs/2203.07436 + + +############################################################################################################ + +DeepLabCut 3.0 Toolbox +M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab +A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG +N Poulsen, neils.poulsen@epfl.ch | https://github.com/n-poulsen +S Ye, shaokai.ye@epfl.ch | https://github.com/yeshaokai +A Filippova, anastasiia.filippova@epfl.ch | https://github.com/nastya236 +Q Macé | https://github.com/QuentinJGMace +J Lauer, jessy@deeplabcut.org | https://github.com/jeylau +L Stoffl, lucas.stoffl@epfl.ch | https://github.com/LucZot + +We also greatly thank the 2023 DeepLabCut AI Residents who contributed: +Anna Teruel-Sanchis | https://github.com/anna-teruel +Riza Rae Pineda | https://github.com/rizarae-p +Konrad Danielewski | https://github.com/KonradDanielewski + +Products: +PyTorch backend for DeepLabCut +Expanded SuperAnimal capabilities +New model architectures (WIP: stay tuned, but includes BUCTD) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f4396c3d16..b26ffb0fb0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal +level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at alexander.mathis@bethgelab.org. All +reported by contacting the project team at alexander.mathis@epfl.ch. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index daffcaa2b5..79f60b0674 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,77 +1,153 @@ -# How to Contribute to DeepLabCut +# Contributing to DeepLabCut -DeepLabCut is an actively developed package and we welcome community development and involvement. We are especially seeking people from underrepresented backgrounds in OSS to contribute their expertise and experience. Please get in touch if you want to discuss specific contributions you are interested in developing, and we can help shape a road-map. +Thanks for your interest in contributing to DeepLabCut! We welcome bug fixes, new features, documentation improvements, tests, and general maintenance contributions. -We are happy to receive code extensions, bug fixes, documentation updates, etc. +We especially encourage contributions from people from backgrounds that are underrepresented in open-source software. If you want to discuss an idea before opening a pull request, feel free to start a discussion or open an issue. -If you are a new user, we recommend checking out the detailed [Github Guides](https://guides.github.com). +If you are new to GitHub, the [GitHub Guides](https://guides.github.com/) are a great place to start. -## Setting up a development installation +## Ways to contribute -In order to make changes to `deeplabcut`, you will need to [fork](https://guides.github.com/activities/forking/#fork) the -[repository](https://github.com/deeplabcut/deeplabcut). +You can help by: -If you are not familiar with `git`, we recommend reading up on [this guide](https://guides.github.com/introduction/git-handbook/#basic-git). +- Fixing bugs +- Improving documentation +- Adding tests +- Improving examples +- Refactoring or cleaning up code +- Proposing or implementing new features -Here are guidelines for installing deeplabcut locally on your own computer, where you can make changes to the code! We often update the master deeplabcut code base on github, and then ~1 a month we push out a stable release on pypi. This is what most users turn to on a daily basis (i.e. pypi is where you get your `pip install deeplabcut` code from! +## Development setup -But, sometimes we add things to the repo that are not yet integrated, or you might want to edit the code yourself, or you will need to do this to contribute. Here, we show you how to do this. +To work on DeepLabCut locally: -**Step 1:** +1. [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). +2. Clone your fork: -- git clone the repo into a folder on your computer: +```bash +git clone https://github.com//DeepLabCut.git +cd DeepLabCut +``` -- click on this green button and copy the link: +3. Create and activate a Python environment. -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581984907363-G8AFGX4V20Y1XD1PSZAK/ke17ZwdGBToddI8pDm48kGJBV0_F4LE4_UtCip_K_3lZw-zPPgdn4jUwVcJE1ZvWEtT5uBSRWt4vQZAgTJucoTqqXjS3CfNDSuuf31e0tVE0ejQCe16973Pm-pux3j5_Oqt57D2H0YbaJ3tl8vn_eR926scO3xePJoa6uVJa9B4/gitclone.png?format=500w) +We recommend using the project's development dependency group from `pyproject.toml` so you get the tools needed for local development (including formatting, linting, and testing). -- then in the terminal type: `git clone https://github.com/DeepLabCut/DeepLabCut.git` +For example, with `uv`: -**Step 2:** +```bash +uv sync --group dev +``` -- Now you will work from the terminal inside this cloned folder: +With `pip` (e.g. in a `conda` environment): -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581985288123-V8XUAY0C0ZDNJ5WBHB7Y/ke17ZwdGBToddI8pDm48kIsGBOdR9tS_SxF6KQXIcDtZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpz3c8X74DzCy4P3pv-ZANOdh-3ZL9iVkcryTbbTskaGvEc42UcRKU-PHxLXKM6ZekE/terminal.png?format=750w) +```bash +pip install -e . --group dev +``` -- Now, when you start `ipython` and `import deeplabcut` you are importing the folder "deeplabcut" - so any changes you make, or any changes we made before adding it to the pip package, are here. +If you use a different environment manager, install the package in editable/development mode together with the `dev` dependency group defined in `pyproject.toml`. -- You can also check which deeplabcut you are importing by running: `deeplabcut.__file__` +## Working on the code -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581985466026-94OCSZJ5TL8U52JLB5VU/ke17ZwdGBToddI8pDm48kNdOD5iqmBzHwUaWGKS6qHBZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpyQPoegsR7K4odW9xcCi1MIHmvHh95_BFXYdKinJaRhV61R4G3qaUq94yWmtQgdj1A/importlocal.png?format=750w) +Once your environment is ready, your local checkout is what Python will import. -If you make changes to the code/first use the code, be sure you run `./resinstall.sh`, which you find in the main DeepLabCut folder: +If you want to verify that you are using the local source tree, you can run: -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1609353210708-FRNREI7HUNS4GLDSJ00G/ke17ZwdGBToddI8pDm48kAya1IcSd32bok4WHvykeicUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYy7Mythp_T-mtop-vrsUOmeInPi9iDjx9w8K4ZfjXt2dq18t0tDkB2HMfL2JGcLHN27k5rSOPIU8nEAZT0p1MiSCjLISwBs8eEdxAxTptZAUg/Screen+Shot+2020-12-30+at+7.33.16+PM.png?format=2500w) +```bash +python -c "import deeplabcut; print(deeplabcut.__file__)" +``` +Using `ipython` or Jupyter is completely optional—use whatever workflow you prefer. +If you change packaged resources or otherwise need to refresh the local installation, run: -Note, before committing to DeepLabCut, please be sure your code is formatted according to `black`. To learn more, -see [`black`'s documentation](https://black.readthedocs.io/en/stable/). +```bash +./reinstall.sh +``` -Now, please make a [pull request](https://github.com/DeepLabCut/DeepLabCut/pull/new/) that includes both a **summary of and changes to**: +> [!NOTE] +> This script automatically uninstalls the package, builds a new wheel using `setup.py`, and installs that wheel. It is not a simple `pip install -e .` because some resources are copied during installation and need to be refreshed. -- How you modified the code and what new functionality it has. -- DOCSTRING update for your change -- A working example of how it works for users. -- If it's a function that also can be used in downstream steps (i.e. could be plotted) we ask you (1) highlight this, and (2) idealy you provide that functionality as well. If you have any questions, please reach out: admin@deeplabcut.org +## Code style and pre-commit -**TestScript outputs:** +We use `pre-commit` to run formatting and other checks before code is committed. -- The **OS it has been tested on** -- the **output of the [testscript.py](/examples/testscript.py)** and if you are editing the **3D code the [testscript_3d.py](/examples/testscript_3d.py)**, and if you edit multi-animal code please run the [maDLC test script](https://github.com/DeepLabCut/DeepLabCut/blob/master/examples/testscript_multianimal.py). +Set it up once in your clone: -**Review & Formatting:** +```bash +pre-commit install +``` -- Please run black on the code to conform to our Black code style (see more at https://pypi.org/project/black/). -- Please assign a reviewer, typically @AlexEMG, @mmathislab, or @jeylau (i/e. the [core-developers](https://github.com/orgs/DeepLabCut/teams/core-developers/members)) +Whenever you commit, `pre-commit` will run the configured checks. -**Code headers** +Please run `pre-commit` before opening a pull request. This helps catch formatting, import ordering, whitespace, YAML, and other common issues early and accelerates code review greatly. -- The code headers can be standardized by running `python tools/update_license_headers.py` -- Edit `NOTICE.yml` to update the header. +## Tests -**DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals:** +Pull requests are validated in CI, and contributors are encouraged to run tests locally using: -- the [authors](/AUTHORS) -- [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) +```bash +pytest tests +``` +in the project root before opening a pull request. +> [!IMPORTANT] +> Heavier tests are also run automatically on GitHub, so this is not a strict requirement, +> but it can help catch issues early and speed up the review process. + +## Pull request guidelines + +When submitting a pull request, please: + +- Clearly describe what changed and why +- Link any related issue(s) +- Update docstrings and documentation when behavior changes +- Add or update tests when appropriate +- Include a small usage example when it helps reviewers understand and/or test the change + +Smaller, focused pull requests are usually much easier to review than very large ones. + +### Draft pull requests + +We use draft pull requests to indicate work in progress. +You may still request reviews and feedbacks on draft pull requests, and we encourage you to do so if you would like early feedback on your work. +Please note that the draft status is in no way related to the perceived quality of the code or its potential for merging, but is simply a way to indicate that the work is not yet ready for final review and merging. +Most pull requests exist for the majority of their lifetime as drafts, which is expected. + +## Documentation + +Documentation improvements are always welcome. + +If your change affects users, please update the relevant docs, examples, or inline docstrings so the behavior is discoverable and easy to understand. + +## Code headers and notices + +If you need to standardize code headers, run: + +```bash +python tools/update_license_headers.py +``` + +Contributors are requested not to update `NOTICE.yml` or `LICENSE` files. + +## Review process + +A maintainer will review your pull request. You do not need to supply a specific release timeline in your PR description—contributions are reviewed and merged as capacity allows. + +If you have questions about where a change should go or how to structure it, opening a draft pull request is completely fine. + +## Need help? + +If you are unsure whether something is in scope, open an issue or draft PR and ask. +We'd much rather help early than have you spend time on the wrong thing. +We also welcome "Feature requests" issues if you would like to discuss implementation details or would like preliminary feedback. + +## Acknowledgments + +DeepLabCut is an open-source project and has benefited from many contributors over time, including: + +- The [authors](/AUTHORS) +- Listed [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) +- And many others over the years. + +We look forward to your contributions! diff --git a/LICENSE b/LICENSE index 341c30bda4..65c5ca88a6 100644 --- a/LICENSE +++ b/LICENSE @@ -163,4 +163,3 @@ whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. - diff --git a/NOTICE.yml b/NOTICE.yml index 68ff6362df..d2ced97e0f 100644 --- a/NOTICE.yml +++ b/NOTICE.yml @@ -5,7 +5,7 @@ https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for contributors. - https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS + https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 include: @@ -17,6 +17,7 @@ # License for files adapted from DeeperCut by Eldar Insafutdinov # https://github.com/eldar/pose-tensorflow + # Applies to most files in deeplabcut.pose_estimation_tensorflow - header: | DeepLabCut Toolbox (deeplabcut.org) @@ -107,3 +108,8 @@ include: - deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py - deeplabcut/pose_tracking_pytorch/model/backones/vit_pytorch.py + +# PyTorch license + +- header: | + See https://github.com/pytorch/pytorch/blob/main/LICENSE diff --git a/README.md b/README.md index ed6b63e483..5c177728ad 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,45 @@
- +

- + + + + + + + + - - - [📚Documentation](https://deeplabcut.github.io/DeepLabCut/README.html) | [🛠️ Installation](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) | [🌎 Home Page](https://www.deeplabcut.org) | -[🐿🐴🐁🐘🐆 Model Zoo](http://www.mackenziemathislab.org/dlc-modelzoo/) | +[🐿🐴🐁🐘🐆 Model Zoo](http://www.mackenziemathislab.org/deeplabcut/) | [🚨 News](https://deeplabcut.github.io/DeepLabCut/README.html#news-and-in-the-news) | -[🪲 Reporting Issues](https://github.com/DeepLabCut/DeepLabCut/issues) +[🪲 Reporting Issues](https://github.com/DeepLabCut/DeepLabCut/issues) -[🫶 Getting Assistance](https://deeplabcut.github.io/DeepLabCut/README.html#be-part-of-the-dlc-community) | -[∞ DeepLabCut Online Course](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials/blob/master/DLCcourse.md) | -[📝 Publications](https://deeplabcut.github.io/DeepLabCut/README.html#references) | -[👩🏾‍💻👨‍💻 DeepLabCut AI Residency](https://www.deeplabcutairesidency.org/) +[🫶 Getting Assistance](https://deeplabcut.github.io/DeepLabCut/README.html#be-part-of-the-dlc-community) | +[∞ DeepLabCut Online Course](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials/blob/master/DLCcourse.md) | +[📝 Publications](https://deeplabcut.github.io/DeepLabCut/README.html#references) | +[👩🏾‍💻👨‍💻 DeepLabCut AI Residency](https://www.deeplabcutairesidency.org/) +![Version](https://img.shields.io/badge/python_version-3.10-purple) [![Downloads](https://pepy.tech/badge/deeplabcut)](https://pepy.tech/project/deeplabcut) [![Downloads](https://pepy.tech/badge/deeplabcut/month)](https://pepy.tech/project/deeplabcut) [![PyPI version](https://badge.fury.io/py/deeplabcut.svg)](https://badge.fury.io/py/deeplabcut) -![Python package](https://github.com/DeepLabCut/DeepLabCut/workflows/Python%20package/badge.svg) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) Code style: black [![GitHub stars](https://img.shields.io/github/stars/DeepLabCut/DeepLabCut.svg?style=social&label=Star)](https://github.com/DeepLabCut/DeepLabCut) @@ -43,48 +47,99 @@ [![Percentage of issues still open](http://isitmaintained.com/badge/open/deeplabcut/deeplabcut.svg)](http://isitmaintained.com/project/deeplabcut/deeplabcut "Percentage of issues still open") [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut) [![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://twitter.com/DeepLabCut) +[![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) [![Generic badge](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](CONTRIBUTING.md) - - +[![CZI's Essential Open Source Software for Science](https://chanzuckerberg.github.io/open-science/badges/CZI-EOSS.svg)](https://czi.co/EOSS)

# Welcome! 👋 -**DeepLabCut™️** is a toolbox for state-of-the-art markerless pose estimation of animals performing various behaviors. As long as you can see (label) what you want to track, you can use this toolbox, as it is animal and object agnostic. [Read a short development and application summary below](https://github.com/DeepLabCut/DeepLabCut#why-use-deeplabcut). +**DeepLabCut™️** is a toolbox for state-of-the-art markerless pose estimation of animals performing various behaviors. As long as you can see (label) what you want to track, you can use this toolbox, as it is animal and object agnostic. [Read a short development and application summary below](https://github.com/DeepLabCut/DeepLabCut#why-use-deeplabcut). # [Installation: how to install DeepLabCut](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) -Very quick start: `pip install "deeplabcut[gui,tf]"` that includes all functions plus GUIs, or `pip install deeplabcut[tf]` (headless version with PyTorch and TensorFlow). -* We recommend using our conda file, see [here](https://github.com/DeepLabCut/DeepLabCut/blob/master/conda-environments/README.md) or the new [`deeplabcut-docker` package](https://github.com/DeepLabCut/DeepLabCut/tree/master/docker). +Please click the link above for all the information you need to get started! Please note that currently we support only Python 3.10+ (see conda files for guidance). -# [Documentation: The DeepLabCut Process](https://deeplabcut.github.io/DeepLabCut) +## Quick start + +Developers Stable Release: very quick start (Python 3.10+ required) to install +DeepLabCut with the PyTorch engine + +- [1] [Install PyTorch](https://pytorch.org/get-started/locally/) (**install and then select the desired +CUDA version if you want to use a GPU**): `pip install torch torchvision`. +Or as an example for GPU support (please check pytorch docs to get the perfect version for your CUDA): +```bash +conda install pytorch cudatoolkit=11.3 -c pytorch +``` +- [2] Then, install `DeepLabCut` (with all functions + the GUI): + +```bash +pip install --pre "deeplabcut[gui]" +``` +or `pip install --pre "deeplabcut"` (headless +version with PyTorch)! + +To use the TensorFlow (TF) engine (requires Python 3.10; TF up to v2.10 supported on Windows, +up to v2.12 on other platforms): you'll need to run `pip install "deeplabcut[gui,tf]"` +(which includes all functions plus GUIs) or `pip install "deeplabcut[tf]"` (headless +version with PyTorch and TensorFlow). We aim to depreciate the TF part in 2027. + +We recommend using our conda file, see [here](https://github.com/DeepLabCut/DeepLabCut/blob/main/conda-environments/README.md) or the [`deeplabcut-docker` package](https://github.com/DeepLabCut/DeepLabCut/tree/main/docker). + +# [Documentation: The DeepLabCut Process](https://deeplabcut.github.io/DeepLabCut/README.html) Our docs walk you through using DeepLabCut, and key API points. For an overview of the toolbox and workflow for project management, see our step-by-step at [Nature Protocols paper](https://doi.org/10.1038/s41596-019-0176-0). -For a deeper understanding and more resources for you to get started with Python and DeepLabCut, please check out our free online course! http://DLCcourse.deeplabcut.org +For a deeper understanding and more resources for you to get started with Python and DeepLabCut, please check out our free online course! https://deeplabcut.github.io/DeepLabCut/docs/course.html

-# [DEMO the code](/examples) +# [DEMO the code](examples/README.md) -🐭 demo [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb) +🐭 pose tracking of single animals demo [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb) -🐭🐭🐭 demo [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_3miceDemo.ipynb) - -- See [more demos here](https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/README.md). We provide data and several Jupyter Notebooks: one that walks you through a demo dataset to test your installation, and another Notebook to run DeepLabCut from the beginning on your own data. We also show you how to use the code in Docker, and on Google Colab. +See [more demos here](https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/README.md). We provide data and several Jupyter Notebooks: one that walks you through a demo dataset to test your installation, and another Notebook to run DeepLabCut from the beginning on your own data. We also show you how to use the code in Docker, and on Google Colab. # Why use DeepLabCut? -In 2018, we demonstrated the capabilities for [trail tracking](https://vnmurthylab.org/), [reaching in mice](http://www.mousemotorlab.org/) and various Drosophila behaviors during egg-laying (see [Mathis et al.](https://www.nature.com/articles/s41593-018-0209-y) for details). There is, however, nothing specific that makes the toolbox only applicable to these tasks and/or species. The toolbox has already been successfully applied (by us and others) to [rats](http://www.mousemotorlab.org/deeplabcut), humans, various fish species, bacteria, leeches, various robots, cheetahs, [mouse whiskers](http://www.mousemotorlab.org/deeplabcut) and [race horses](http://www.mousemotorlab.org/deeplabcut). DeepLabCut utilized the feature detectors (ResNets + readout layers) of one of the state-of-the-art algorithms for human pose estimation by Insafutdinov et al., called DeeperCut, which inspired the name for our toolbox (see references below). Since this time, the package has changed substantially. The code has been re-tooled and re-factored since 2.1+: We have added faster and higher performance variants with MobileNetV2s, EfficientNets, and our own DLCRNet backbones (see [Pretraining boosts out-of-domain robustness for pose estimation](https://arxiv.org/abs/1909.11229) and [Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0)). Additionally, we have improved the inference speed and provided both additional and novel augmentation methods, added real-time, and multi-animal support. We currently provide state-of-the-art performance for animal pose estimation. +DeepLabCut continues to be actively maintained and we strive to provide a user-friendly `GUI` and `API` for computer vision researchers and life scientists alike. This means we integrate state-of-the-art models and frameworks, while providing our "best-guess" defaults for life scientists. We highly encourage you to read our papers to get a better understanding of what to use and how to modify the models for your setting. + +## Performance 🔥 + +In general, we provide all the tooling for you to train and use custom models with various high-performance backbones. +We also provide two foundation pretrained animal models: `SuperAnimal-Quadruped`, `SuperAnimal-TopViewMouse`. To gauge their *out-of-distribution* performance, we provide the following tables. + +These models are trained on the [SuperAnimal-Quadruped with AP-10K held out for out-of-domain testing]([https://cocodataset.org/](https://www.nature.com/articles/s41467-024-48792-2)) and the [SuperAnimal-TopViewMouse with DLC-openfield held out for out-of-distribution testing](https://www.nature.com/articles/s41467-024-48792-2). We provide models that include AP-10K in the API (and GUI). +Note, there are many different models to select from in DeepLabCut 3.0. We strongly recommend you check [this Guide](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/architectures.html) for more details. +This table, and those below, give you a sense of performance in real-world complex in-the-wild and lab mouse data, respectively. +This [link provides the model weights](https://huggingface.co/mwmathis/DeepLabCutModelZoo-SuperAnimal-Quadruped) to reproduce the numbers; but please note, our `full` models are in our DLClibrary and released in the API. + +
DLC 3.0 Pose Estimation (Top Down Models) + +| Model Name | Type | mAP SA-Q on AP-10K | mAP SA-TVM on DLC-OpenField | +|------------------------------|------------|---------------------|-----------------------------| +| top_down_resnet_50 | Top-Down | 54.9 | 93.5 | +| top_down_resnet_101 | Top-Down | 55.9 | 94.1 | +| top_down_hrnet_w32 | Top-Down | 52.5 | 92.4 | +| top_down_hrnet_w48 | Top-Down | 55.3 | 93.8 | +| rtmpose_s | Top-Down | 52.9 | 92.9 | +| rtmpose_m | Top-Down | 55.4 | 94.8 | +| rtmpose_x | Top-Down | 57.6 | 94.5 | +
+ +## The History + +In 2018, we demonstrated the capabilities for [trail tracking](https://vnmurthylab.org/), [reaching in mice](http://www.mousemotorlab.org/) and various Drosophila behaviors during egg-laying (see [Mathis et al.](https://www.nature.com/articles/s41593-018-0209-y) for details). There is, however, nothing specific that makes the toolbox only applicable to these tasks and/or species. The toolbox has already been successfully applied (by us and others) to [rats](http://www.mousemotorlab.org/deeplabcut), humans, various fish species, bacteria, leeches, various robots, cheetahs, [mouse whiskers](http://www.mousemotorlab.org/deeplabcut) and [race horses](http://www.mousemotorlab.org/deeplabcut). DeepLabCut utilized the feature detectors (ResNets + readout layers) of one of the state-of-the-art algorithms for human pose estimation by Insafutdinov et al., called DeeperCut, which inspired the name for our toolbox (see references below). Since this time, the package has changed substantially. The code has been re-tooled and re-factored since 2.1+: We have added faster and higher performance variants with MobileNetV2s, EfficientNets, and our own DLCRNet backbones (see [Pretraining boosts out-of-domain robustness for pose estimation](https://arxiv.org/abs/1909.11229) and [Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0)). Additionally, we have improved the inference speed and provided both additional and novel augmentation methods, added real-time, and multi-animal support. +In v3.0+ we have changed the backend to support PyTorch. This brings not only an easier installation process for users, but performance gains, developer flexibility, and a lot of new tools! Importantly, the high-level API stays the same, so it will be a seamless transition for users 💜! +We currently provide state-of-the-art performance for animal pose estimation and the labs (M. Mathis Lab and A. Mathis Group) have both top journal and computer vision conference papers.

- +

@@ -98,101 +153,33 @@ In 2018, we demonstrated the capabilities for [trail tracking](https://vnmurthyl ## Code contributors: -DLC code was originally developed by [Alexander Mathis](https://github.com/AlexEMG) & [Mackenzie Mathis](https://github.com/MMathisLab), and was extended in 2.0 with the core dev team consisting of [Tanmay Nath](https://github.com/meet10may) (2.0-2.1), and currently (2.1+) with [Jessy Lauer](https://github.com/jeylau) and (2.3+) [Niels Poulsen](https://github.com/n-poulsen). DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals including Mert Yuksekgonul, Tom Biasi, Richard Warren, Ronny Eichler, Hao Wu, Federico Claudi, Gary Kane and Jonny Saunders as well as the [100+ contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors). Please see [AUTHORS](https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS) for more details! +DLC code was originally developed by [Alexander Mathis](https://github.com/AlexEMG) & [Mackenzie Mathis](https://github.com/MMathisLab), and was extended in 2.0 with the core dev team consisting of [Tanmay Nath](https://github.com/meet10may) (2.0-2.1), [Jessy Lauer](https://github.com/jeylau) (2.1-2.4), and [Niels Poulsen](https://github.com/n-poulsen) (2.3-3.0). +DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals including early contributors: Mert Yuksekgonul, Tom Biasi, Richard Warren, Ronny Eichler, Hao Wu, Federico Claudi, Gary Kane and Jonny Saunders as well as the [100+ contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors). Please see [AUTHORS](https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS) for more details! + +🤩 This is an actively developed package and we welcome community development and involvement: + +[![Contributors](https://contrib.rocks/image?repo=DeepLabCut/DeepLabCut)](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) + + -This is an actively developed package and we welcome community development and involvement. # Get Assistance & be part of the DLC Community✨: | 🚉 Platform | 🎯 Goal | ⏱️ Estimated Response Time | 📢 Support Squad | |------------------------------------------------------------|-----------------------------------------------------------------------------|---------------------------|----------------------------------------| -| [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut)
🐭Tag: DeepLabCut | To ask help and support questions👋 | Promptly🔥 | DLC Team and The DLC Community | -| GitHub DeepLabCut/[Issues](https://github.com/DeepLabCut/DeepLabCut/issues) | To report bugs and code issues🐛 (we encourage you to search issues first) | 2-3 days | DLC Team | -|[![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) | To discuss with other users, share ideas and collaborate💡 | 2 days | The DLC Community | -| GitHub DeepLabCut/[Contributing](https://github.com/DeepLabCut/DeepLabCut/blob/master/CONTRIBUTING.md) | To contribute your expertise and experience🙏💯 | Promptly🔥 | DLC Team | -| 🚧 GitHub DeepLabCut/[Roadmap](https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/roadmap.md) | To learn more about our journey✈️ | N/A | N/A | -| [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://twitter.com/DeepLabCut) | To keep up with our latest news and updates 📢 | Daily | DLC Team | +| GitHub DeepLabCut/[Issues](https://github.com/DeepLabCut/DeepLabCut/issues) | To report bugs and code issues🐛 (we encourage you to search issues first) | 2-5 days | DLC Core Dev Team | +| GitHub DeepLabCut/[Contributing](https://github.com/DeepLabCut/DeepLabCut/blob/master/CONTRIBUTING.md) | To contribute your expertise and experience🙏💯 | 2-5 days | DLC Core Dev Team | +| 🚧 GitHub DeepLabCut/[Roadmap](https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/roadmap.md) | To learn more about our journey✈️ | N/A | N/A +| [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut)
🐭Tag: DeepLabCut | To ask help and support questions 👋 | Promptly🔥 | The DLC Community | +|[![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) | To discuss with other users, share ideas and collaborate💡 | 2-5 days | The DLC Community | +| [BluSky🦋](https://bsky.app/profile/deeplabcut.bsky.social) | To keep up with our latest news and updates 📢 | 2-5 days | DLC Team | +| [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) | To keep up with our latest news and updates 📢 | 2-5 days | DLC Team | | The DeepLabCut [AI Residency Program](https://www.deeplabcutairesidency.org/) | To come and work with us next summer👏 | Annually | DLC Team | -## References: - -If you use this code or data we kindly ask that you please [cite Mathis et al, 2018](https://www.nature.com/articles/s41593-018-0209-y) and, if you use the Python package (DeepLabCut2.x) please also cite [Nath, Mathis et al, 2019](https://doi.org/10.1038/s41596-019-0176-0). If you utilize the MobileNetV2s or EfficientNets please cite [Mathis, Biasi et al. 2021](https://openaccess.thecvf.com/content/WACV2021/papers/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.pdf). If you use versions 2.2beta+ or 2.2rc1+, please cite [Lauer et al. 2022](https://www.nature.com/articles/s41592-022-01443-0). - -DOIs (#ProTip, for helping you find citations for software, check out [CiteAs.org](http://citeas.org/)!): - -- Mathis et al 2018: [10.1038/s41593-018-0209-y](https://doi.org/10.1038/s41593-018-0209-y) -- Nath, Mathis et al 2019: [10.1038/s41596-019-0176-0](https://doi.org/10.1038/s41596-019-0176-0) -- Lauer et al 2022: [10.1038/s41592-022-01443-0](https://doi.org/10.1038/s41592-022-01443-0) - - -Please check out the following references for more details: - - @article{Mathisetal2018, - title = {DeepLabCut: markerless pose estimation of user-defined body parts with deep learning}, - author = {Alexander Mathis and Pranav Mamidanna and Kevin M. Cury and Taiga Abe and Venkatesh N. Murthy and Mackenzie W. Mathis and Matthias Bethge}, - journal = {Nature Neuroscience}, - year = {2018}, - url = {https://www.nature.com/articles/s41593-018-0209-y}} - - @article{NathMathisetal2019, - title = {Using DeepLabCut for 3D markerless pose estimation across species and behaviors}, - author = {Nath*, Tanmay and Mathis*, Alexander and Chen, An Chi and Patel, Amir and Bethge, Matthias and Mathis, Mackenzie W}, - journal = {Nature Protocols}, - year = {2019}, - url = {https://doi.org/10.1038/s41596-019-0176-0}} - - @InProceedings{Mathis_2021_WACV, - author = {Mathis, Alexander and Biasi, Thomas and Schneider, Steffen and Yuksekgonul, Mert and Rogers, Byron and Bethge, Matthias and Mathis, Mackenzie W.}, - title = {Pretraining Boosts Out-of-Domain Robustness for Pose Estimation}, - booktitle = {Proceedings of the IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)}, - month = {January}, - year = {2021}, - pages = {1859-1868}} - - @article{Lauer2022MultianimalPE, - title={Multi-animal pose estimation, identification and tracking with DeepLabCut}, - author={Jessy Lauer and Mu Zhou and Shaokai Ye and William Menegas and Steffen Schneider and Tanmay Nath and Mohammed Mostafizur Rahman and Valentina Di Santo and Daniel Soberanes and Guoping Feng and Venkatesh N. Murthy and George Lauder and Catherine Dulac and M. Mathis and Alexander Mathis}, - journal={Nature Methods}, - year={2022}, - volume={19}, - pages={496 - 504}} - - @article{insafutdinov2016eccv, - title = {DeeperCut: A Deeper, Stronger, and Faster Multi-Person Pose Estimation Model}, - author = {Eldar Insafutdinov and Leonid Pishchulin and Bjoern Andres and Mykhaylo Andriluka and Bernt Schiele}, - booktitle = {ECCV'16}, - url = {http://arxiv.org/abs/1605.03170}} - -Review & Educational articles: - - @article{Mathis2020DeepLT, - title={Deep learning tools for the measurement of animal behavior in neuroscience}, - author={Mackenzie W. Mathis and Alexander Mathis}, - journal={Current Opinion in Neurobiology}, - year={2020}, - volume={60}, - pages={1-11}} - - @article{Mathis2020Primer, - title={A Primer on Motion Capture with Deep Learning: Principles, Pitfalls, and Perspectives}, - author={Alexander Mathis and Steffen Schneider and Jessy Lauer and Mackenzie W. Mathis}, - journal={Neuron}, - year={2020}, - volume={108}, - pages={44-65}} - -Other open-access pre-prints related to our work on DeepLabCut: - - @article{MathisWarren2018speed, - author = {Mathis, Alexander and Warren, Richard A.}, - title = {On the inference speed and video-compression robustness of DeepLabCut}, - year = {2018}, - doi = {10.1101/457242}, - publisher = {Cold Spring Harbor Laboratory}, - URL = {https://www.biorxiv.org/content/early/2018/10/30/457242}, - eprint = {https://www.biorxiv.org/content/early/2018/10/30/457242.full.pdf}, - journal = {bioRxiv}} +## References \& Citations: + +Please see our [dedicated page](https://deeplabcut.github.io/DeepLabCut/docs/citation.html) on how to **cite DeepLabCut** 🙏 and our suggestions for your Methods section! ## License: @@ -202,20 +189,24 @@ SuperAnimal models are provided for research use only (non-commercial use). ## Major Versions: -- For all versions, please see [here](https://github.com/DeepLabCut/DeepLabCut/releases). +**For all versions, please see [here](https://github.com/DeepLabCut/DeepLabCut/releases).** + +VERSION 3.0: A whole new experience with PyTorch🔥. While the high-level API remains the same, the backend and developer friendliness have greatly improved, along with performance gains! VERSION 2.3: Model Zoo SuperAnimals, and a whole new GUI experience. VERSION 2.2: Multi-animal pose estimation, identification, and tracking with DeepLabCut is supported (as well as single-animal projects). VERSION 2.0-2.1: This is the **Python package** of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) that was originally released in Oct 2018 with our [Nature Protocols](https://doi.org/10.1038/s41596-019-0176-0) paper (preprint [here](https://www.biorxiv.org/content/10.1101/476531v1)). -This package includes graphical user interfaces to label your data, and take you from data set creation to automatic behavioral analysis. It also introduces an active learning framework to efficiently use DeepLabCut on large experimental projects, and data augmentation tools that improve network performance, especially in challenging cases (see [panel b](https://camo.githubusercontent.com/77c92f6b89d44ca758d815bdd7e801247437060b/68747470733a2f2f737461746963312e73717561726573706163652e636f6d2f7374617469632f3537663664353163396637343536366635356563663237312f742f3563336663316336373538643436393530636537656563372f313534373638323338333539352f636865657461682e706e673f666f726d61743d37353077)). +This package includes graphical user interfaces to label your data, and take you from data set creation to automatic behavioral analysis. It also introduces an active learning framework to efficiently use DeepLabCut on large experimental projects, and data augmentation tools that improve network performance, especially in challenging cases. VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) can be found in the history of git, or here: https://github.com/DeepLabCut/DeepLabCut/releases/tag/1.11 # News (and in the news): -:purple_heart: The DeepLabCut Model Zoo launches SuperAnimals, see more [here](http://www.mackenziemathislab.org/dlc-modelzoo/). +:purple_heart: We released a major update, moving from 2.x --> 3.x with the backend change to PyTorch + +:purple_heart: The DeepLabCut Model Zoo launches SuperAnimals, see more [here](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html). :purple_heart: **DeepLabCut supports multi-animal pose estimation!** maDLC is out of beta/rc mode and beta is deprecated, thanks to the testers out there for feedback! Your labeled data will be backwards compatible, but not all other steps. Please see the [new `2.2+` releases](https://github.com/DeepLabCut/DeepLabCut/releases) for what's new & how to install it, please see our new [paper, Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0), and the [new docs]( https://deeplabcut.github.io/DeepLabCut) on how to use it! @@ -223,7 +214,10 @@ VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://ww :purple_heart: We have a **real-time** package available! http://DLClive.deeplabcut.org -- January 2024: Our original paper ['DeepLabCut: markerless pose estimation of user-defined body parts with deep learning'](https://www.nature.com/articles/s41593-018-0209-y) in Nature Neuroscience has surpassed 3,000 Google Scholar citations! + +- June 2024: Our second DLC paper ['Using DeepLabCut for 3D markerless pose estimation across species and behaviors'](https://www.nature.com/articles/s41596-019-0176-0) in Nature Protocols has surpassed 1,000 Google Scholar citations! +- May 2024: DeepLabCut was featured in Nature: ['DeepLabCut: the motion-tracking tool that went viral'](https://www.nature.com/articles/d41586-024-01474-x) +- January 2024: Our original paper ['DeepLabCut: markerless pose estimation of user-defined body parts with deep learning'](https://www.nature.com/articles/s41593-018-0209-y) in Nature Neuroscience has surpassed 3,000 Google Scholar citations! - December 2023: DeepLabCut hit 600,000 downloads! - October 2023: DeepLabCut celebrates a milestone with 4,000 🌟 in Github! - July 2023: The user forum is very active with more than 1k questions and answers: [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut) @@ -242,7 +236,7 @@ VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://ww - Oct 2019: DLC 2.1 released with lots of updates. In particular, a Project Manager GUI, MobileNetsV2, and augmentation packages (Imgaug and Tensorpack). For detailed updates see [releases](https://github.com/DeepLabCut/DeepLabCut/releases) - Sept 2019: We published two preprints. One showing that [ImageNet pretraining contributes to robustness](https://arxiv.org/abs/1909.11229) and a [review on animal pose estimation](https://arxiv.org/abs/1909.13868). Check them out! - Jun 2019: DLC 2.0.7 released with lots of updates. For updates see [releases](https://github.com/DeepLabCut/DeepLabCut/releases) -- Feb 2019: DeepLabCut joined [twitter](https://twitter.com/deeplabcut) [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://twitter.com/DeepLabCut) +- Feb 2019: DeepLabCut joined [twitter](https://x.com/deeplabcut) [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) - Jan 2019: We hosted workshops for DLC in Warsaw, Munich and Cambridge. The materials are available [here](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials) - Jan 2019: We joined the Image Source Forum for user help: [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut) @@ -256,3 +250,7 @@ importing a project into the new data format for DLC 2.0 - August 2018: NVIDIA AI Developer News: [AI Enables Markerless Animal Tracking](https://news.developer.nvidia.com/ai-enables-markerless-animal-tracking/) - July 2018: Ed Yong covered DeepLabCut and interviewed several users for the [Atlantic](https://www.theatlantic.com/science/archive/2018/07/deeplabcut-tracking-animal-movements/564338). - April 2018: first DeepLabCut preprint on [arXiv.org](https://arxiv.org/abs/1804.03142) + + ## Funding + + We are grateful for the follow support over the years! This software project was supported in part by the Essential Open Source Software for Science (EOSS) program at Chan Zuckerberg Initiative (cycles 1, 3, 3-DEI, 4), and jointly with the Kavli Foundation for EOSS Cycle 6! We also thank the Rowland Institute at Harvard for funding from 2017-2020, and EPFL from 2020-present. diff --git a/_config.yml b/_config.yml index c55ef8cebb..e1112a19ae 100644 --- a/_config.yml +++ b/_config.yml @@ -5,8 +5,15 @@ only_build_toc_files: true sphinx: config: - autodoc_mock_imports: ["wx"] + autodoc_mock_imports: ["wx", "matplotlib", "qtpy", "PySide6", "napari", "shiboken6"] mermaid_output_format: raw + html_static_path: ["docs/_static"] + html_css_files: ["custom.css"] + exclude_patterns: + - ".venv/**" + - "venv/**" + - "**/site-packages/**" + - "**/_build/**" extra_extensions: - numpydoc - sphinxcontrib.mermaid diff --git a/_toc.yml b/_toc.yml index 5280e84bb4..6bb51801ff 100644 --- a/_toc.yml +++ b/_toc.yml @@ -1,54 +1,126 @@ format: jb-book root: README + parts: - caption: Getting Started chapters: - file: docs/UseOverviewGuide + - file: docs/course + - caption: Installation chapters: - file: docs/installation - file: docs/recipes/installTips - file: docs/docker -- caption: User Guides + +- caption: Main User Guides chapters: - file: docs/standardDeepLabCut_UserGuide - file: docs/maDLC_UserGuide - file: docs/Overviewof3D - file: docs/HelperFunctions + - caption: Graphical User Interfaces (GUIs) chapters: - - file: docs/PROJECT_GUI - - file: docs/napari_GUI - - file: docs/recipes/ClusteringNapari -- caption: DeepLabCut-Live! + - file: docs/gui/PROJECT_GUI + - file: docs/gui/napari_GUI + sections: + - file: docs/gui/napari/basic_usage + - file: docs/gui/napari/advanced_usage + +- caption: DLC3 PyTorch Specific Docs chapters: - - file: docs/deeplabcutlive -- caption: DeepLabCut Model Zoo + - file: docs/pytorch/user_guide.md + - file: docs/pytorch/pytorch_config.md + - file: docs/pytorch/architectures.md + +- caption: Quick Start Tutorials chapters: - - file: docs/ModelZoo - - file: docs/recipes/UsingModelZooPupil - - file: docs/recipes/MegaDetectorDLCLive -- caption: DeepLabCut Benchmark + - file: docs/quick-start/single_animal_quick_guide + - file: docs/quick-start/tutorial_maDLC + +- caption: "🚀 Beginner's Guide to DeepLabCut" chapters: - - file: docs/benchmark -- caption: Hardware + - file: docs/beginner-guides/beginners-guide + - file: docs/beginner-guides/manage-project + - file: docs/beginner-guides/labeling + - file: docs/beginner-guides/Training-Evaluation + - file: docs/beginner-guides/video-analysis + +- caption: "🚀 Main Demo Notebooks" chapters: - - file: docs/recipes/TechHardware -- caption: Tutorials & Cookbook + - file: examples/COLAB/COLAB_DEMO_SuperAnimal + - file: examples/COLAB/COLAB_DEMO_mouse_openfield + - file: examples/COLAB/COLAB_3miceDemo + - file: examples/COLAB/COLAB_HumanPose_with_RTMPose + +- caption: "🚀 Notebooks For Your Data" + chapters: + - file: examples/COLAB/COLAB_YOURDATA_SuperAnimal + - file: examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis + - file: examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis + +- caption: "🚀 Special Feature Demos" + chapters: + - file: examples/COLAB/COLAB_transformer_reID + - file: examples/COLAB/COLAB_BUCTD_and_CTD_tracking + - file: examples/JUPYTER/Demo_3D_DeepLabCut + - file: examples/COLAB/COLAB_DLC_ModelZoo + +- caption: "🧑‍🍳 Cookbook (detailed helper guides)" chapters: - - file: docs/tutorial - file: docs/convert_maDLC + - file: docs/recipes/OtherData - file: docs/recipes/io - file: docs/recipes/nn - file: docs/recipes/post - file: docs/recipes/BatchProcessing - file: docs/recipes/DLCMethods + - file: docs/recipes/ClusteringNapari - file: docs/recipes/OpenVINO - file: docs/recipes/flip_and_rotate - file: docs/recipes/pose_cfg_file_breakdown - file: docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook -- caption: Mission & Contribute + +- caption: Hardware Tips + chapters: + - file: docs/recipes/TechHardware + +- caption: DeepLabCut-Live! + chapters: + - file: docs/dlc-live/deeplabcutlive + - file: docs/dlc-live/dlc-live-gui/index + sections: + - file: docs/dlc-live/dlc-live-gui/quickstart/install + - file: docs/dlc-live/dlc-live-gui/user_guide/overview + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support + sections: + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing + sections: + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format + +- caption: "🦄 DeepLabCut Model Zoo" + chapters: + - file: docs/ModelZoo + - file: docs/recipes/UsingModelZooPupil + +- caption: DeepLabCut Benchmarking + chapters: + - file: docs/benchmark + - file: docs/pytorch/Benchmarking_shuffle_guide + +- caption: "Mission & Contribute" chapters: - file: docs/MISSION_AND_VALUES - file: docs/roadmap - file: docs/Governance + - file: CONTRIBUTING + +- caption: Citations for DeepLabCut + chapters: + - file: docs/citation diff --git a/changelog/3_0_0/images/buctd_benchmarks.png b/changelog/3_0_0/images/buctd_benchmarks.png new file mode 100644 index 0000000000..127b1d4f1b Binary files /dev/null and b/changelog/3_0_0/images/buctd_benchmarks.png differ diff --git a/changelog/3_0_0/images/openfield_benchmark_pr2613.png b/changelog/3_0_0/images/openfield_benchmark_pr2613.png new file mode 100644 index 0000000000..0ebedddb4c Binary files /dev/null and b/changelog/3_0_0/images/openfield_benchmark_pr2613.png differ diff --git a/changelog/3_0_0/images/speed_tensorflow.avif b/changelog/3_0_0/images/speed_tensorflow.avif new file mode 100644 index 0000000000..43dcf74443 Binary files /dev/null and b/changelog/3_0_0/images/speed_tensorflow.avif differ diff --git a/changelog/3_0_0/v3_0_0.md b/changelog/3_0_0/v3_0_0.md new file mode 100644 index 0000000000..994a65958b --- /dev/null +++ b/changelog/3_0_0/v3_0_0.md @@ -0,0 +1,158 @@ +# DeepLabCut 3.0: familiar workflows, modern foundations, better performance + +DeepLabCut 3.0 introduces a PyTorch-first training and inference stack while keeping the core DeepLabCut workflow familiar. +Projects still follow the same labeling, training, evaluation, and video-analysis pipeline used throughout the 2.x series, but the underlying engine has been substantially modernized. + +For users who have already been following the release candidates, many of these changes will already feel familiar. +DeepLabCut 3.0 consolidates these incremental changes into a stable release. + +## Increased model performance and speed + + + + + + + + +
Benchmark tableOpenfield benchmark results
Pose estimation performance of the 3.0 PyTorch models compared against previous TensorFlow models on the DeepLabCut Openfield dataset (see PR #2613); RMSE: root mean squared error. *Values from Mathis et al. 2018.
+ + + + + + + + + +
Trimice datasetSpeed comparison: PyTorch vs TensorFlow
Speed of the current PyTorch implementation (ResNet50) compared to the TensorFlow implementation. Results were obtained using a NVIDIA GeForce RTX 2080 Ti with CUDA 12.2 on the DeepLabCut Trimice dataset.
+ + + + + + + + +
BUCTD benchmarks
Comparison of the new BUCTD model architectures with DLCRNet and DEKR on the Marmoset, Fish and Trimice dataset. From Zhou et al., ICCV 2023.
+ +## The journey to 3.0 + +A quick recap of some of the major milestones leading to this release: + +- #2613 - Full initial PyTorch backend implementation +- #2952 - New bottom-up conditional top-down (BUCTD) model architecture +- #2795 - New RTMPose top-down architecture +- #2868 - Updated notebooks and Colab examples for PyTorch workflows +- #2804 - PyTorch model export + +And more, find the full PR reference [on GitHub](https://github.com/DeepLabCut/DeepLabCut/pulls?page=1&q=is%3Apr+label%3ADLC3.0%F0%9F%94%A5)! + +## Notable features in 3.0.0 + +### PyTorch-first, TensorFlow-compatible + +DeepLabCut 3.0 adds a new PyTorch backend while retaining TensorFlow support for legacy workflows. +Project management remains the same and labeled datasets remain compatible. +PyTorch models can be trained alongside previous TensorFlow models on the same train/test splits for direct benchmarking and comparison. + + +### Expanded architecture support + +DeepLabCut 3.0 significantly broadens the supported model ecosystem beyond the classic ResNet-based workflows. The PyTorch stack includes: + +- ResNet and HRNet backbones +- Bottom-up multi-animal approaches such as DEKR and PAF/DLCRNet variants +- Top-down detector-plus-pose pipelines including RTMPose +- Hybrid architectures such as BUCTD and CTD variants +- SuperAnimal-related pretrained workflows + +The documentation now includes dedicated [architecture guides](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/architectures.html) to help users choose models based on scene complexity and experimental needs. + +### Flexible PyTorch training configuration + +The PyTorch engine introduces a modern training stack with expanded augmentation options, training schedules, device management, and model architectures. For each training run, the settings are stored in a `pytorch_config.yaml`, enabling easy reproducibility. + +### Improved interoperability + +The new PyTorch data pipeline introduces loaders for both standard DeepLabCut projects and COCO-style datasets, making it easier to integrate DeepLabCut with broader computer-vision workflows and external annotation formats. + +### Model Zoo and SuperAnimal workflows + +DeepLabCut 3.0 continues to expand the Model Zoo and SuperAnimal ecosystem, making pretrained models and transfer learning more accessible. +Colab notebooks and updated GUI tooling make it easier to experiment with modern architectures without extensive setup. (see the [documentation](https://deeplabcut.github.io/DeepLabCut/README.html)) + +### Modernized installation and packaging + +The project has been moved to a newer packaging system, and is now based around pyproject.toml. This enables the use of modern package-managers & dependency resolvers, such as `uv` or `pdm`. +Users can still install only the components they require, be it GUI support, TensorFlow compatibility, ModelZoo features, and optional experimental integrations. + +### Labeling GUI + +DeepLabCut 3.0 is shipped with a new release of the napari-deeplabcut plugin. Our napari-based labeling GUI has undergone a major internal re-write and modernization: while preserving familiar UI and the DeepLabCut workflow, the update substantially improves stability, data handling, usability, visualization, and annotation workflows, now with automated point tracking for faster labeling. See the [release notes](https://github.com/DeepLabCut/napari-deeplabcut/releases) to find out about all improvements. + +### Upcoming: refreshed documentation + +We have updated and streamlined the documentation, with a focus on clarity and up-to-date information in core areas (installation, getting started guides, and more). +Expect the documentation to continue evolving soon after the release! + +## A major transition + +The jump from the final DeepLabCut 2.x releases to the current codebase is best understood as a transition to more recent Python & deep learning ecosystems rather than a routine update. +Taken together, the PyTorch backend, broader architecture support, ModelZoo integration, packaging modernization, updated labeling GUI, and documentation improvements represent a major evolution of DeepLabCut, which we are happy to release as 3.0. + + +## Closing thoughts + +We hope you enjoy this new version, and we aim to keep sharing many exciting improvements in the future in all areas, be it performance and speed, codebase quality improvements, foundation models integration, user experience and documentation. + +--- +## Changelog since 3.0.0rc14 + +- Add up-to-date uv.lock (#3242) +- Remove unnecessary imports (#3224) +- Add custom styling options for docs (custom.css) (#3207) +- Add internal helper for batched modelzoo inference from in-memory arrays (inference runner) (#3222) +- Implement intelligent test selection in CI (#3046) +- Revamp CONTRIBUTING.md (#3241) +- Update FMPose3D modelzoo integration (#3221) +- Add automated docs & notebooks freshness + normalization checks (#3228) +- Install from PyPI pre-release; add both-backends (#3238) +- Apply linting to entire codebase & add CI workflow to check linting (#3216) +- Bump requests from 2.32.5 to 2.33.0 (#3259) +- Refactor Analyze Videos tab (#3268) +- Consolidate test workflow infrastructure in CI (#3254) +- Move protobuf requirement to pyproject.toml (#3235) +- Use pinned ffmpeg version in CI (#3276) +- Docs versioning: Add glob support, better validation and reporting (#3278) +- Bump cryptography from 46.0.5 to 46.0.7 (#3277) +- Fix failing local Windows tests due to ruamel parsing (#3275) +- Update & de-duplicate skeleton builder (#3258) +- Bump pygments from 2.19.2 to 2.20.0 (#3262) +- update conda yaml: install pyside6 via conda instead of pip (#3253) +- Bump pyasn1 from 0.6.2 to 0.6.3 (#3249) +- Fix SuperAnimal / pretrained load for RTMPose: implement convert_weights on RTMCCHead (#3270) +- Use async update check in GUI (#3234) +- Update napari-DLC docs for refactor (#3280) +- Refactor/predict multianimal (#3220) +- Fix incorrect MultiLevel construction in outlier_frames.compute_deviations (#3247) +- Bump pillow from 12.1.1 to 12.2.0 (#3283) +- Bump pytest from 9.0.2 to 9.0.3 (#3284) +- CircleCI: disable hugginface xet (#3316) +- Update and diversify TensorFlow optional installations. (#3292) +- Bump urllib3 from 2.6.3 to 2.7.0 (#3325) +- Bump gitpython from 3.1.47 to 3.1.50 (#3322) +- make GenerativeSampler visibility-aware (#3305) +- Add isatty method to StreamWriter + eval GUI fix (#3314) +- Add conditional replacement of `@torch.inference_mode` for inference on AMD DirectML GPUs (#3295) +- Robustness fix: Annotation file not dropping likelihood column if present from machine labels (#3323) +- Remove trailing comma in models_to_framework.json (#3330) +- update `list_videos_in_folder` (#3303) +- Improve `TrainingDatasetMetadata` and `get_shuffle_engine` for incomplete projects (#3313) +- update RTMPose `SimCCPredictor`: expose `apply_softmax` and fix visibility thresholding (#3306) +- GUI: Add "Generate debug log" action (#3328) +- [Docker 1] Simplify and modernize Dockerfile (#3290) +- [Docker 2] Update deeplabcut-docker package (#3291) +- Add additional drop_likelihood_columns guards (#3333) +- Resolve inconsistent parameter names via aliasing + deprecationwarning (#3332) +- bump dlclibrary (v0.0.12) and napari-deeplabcut (v3.1.0) (#3338) diff --git a/conda-environments/DEEPLABCUT.yaml b/conda-environments/DEEPLABCUT.yaml index d89b1c5be6..55da8128b3 100644 --- a/conda-environments/DEEPLABCUT.yaml +++ b/conda-environments/DEEPLABCUT.yaml @@ -1,17 +1,21 @@ # DEEPLABCUT.yaml -#DeepLabCut2.0 Toolbox (deeplabcut.org) +#DeepLabCut Toolbox (deeplabcut.org) #© A. & M.W. Mathis Labs #https://github.com/DeepLabCut/DeepLabCut #Please see AUTHORS for contributors. -#https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +#https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS #Licensed under GNU Lesser General Public License v3.0 # # DeepLabCut environment -# FIRST: INSTALL CORRECT DRIVER for GPU, see https://stackoverflow.com/questions/30820513/what-is-the-correct-version-of-cuda-for-my-nvidia-driver/30820690 # -# AFTER THIS FILE IS INSTALLED, if you have a GPU be sure to install `conda forge cudnn` +# FIRST: If you have an NVIDIA GPU and want to use it, check that you have drivers installed! +# To check if your GPUs are visible to PyTorch (and thus DeepLabCut), run: +# >>> python -c "import torch; print(torch.cuda.is_available())" +# +# If "False" is printed, PyTorch (and thus DeepLabCut) cannot access your GPU. For +# more information, see: https://pytorch.org/get-started/locally/ # # install: conda env create -f DEEPLABCUT.yaml # update: conda env update -f DEEPLABCUT.yaml @@ -20,12 +24,14 @@ channels: - conda-forge - defaults dependencies: - - python=3.9 + - python=3.10 - pip - ipython - jupyter - - nb_conda - - notebook<7.0.0 - ffmpeg + - pyside6 - pip: - - "deeplabcut[gui,tf]" + - torch + - torchvision + - --pre + - deeplabcut[gui,modelzoo,wandb] diff --git a/conda-environments/DEEPLABCUT_M1.yaml b/conda-environments/DEEPLABCUT_M1.yaml deleted file mode 100644 index e5a8bab83d..0000000000 --- a/conda-environments/DEEPLABCUT_M1.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# DEEPLABCUT_M1.yaml - -#DeepLabCut2.0 Toolbox (deeplabcut.org) -#© A. & M.W. Mathis Labs -#https://github.com/DeepLabCut/DeepLabCut -#Please see AUTHORS for contributors. - -#https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -#Licensed under GNU Lesser General Public License v3.0 -# -# DeepLabCut M1/M2 (Apple Silicon) environment instructions -# -# We'll get the miniconda M1 bash installer, as explained in -# https://docs.conda.io/projects/conda/en/latest/user-guide/install/macos.html -# -# In the Terminal, run the following commands: -# wget https://repo.anaconda.com/miniconda/Miniconda3-py39_4.12.0-MacOSX-arm64.sh -O ~/miniconda.sh -# bash ~/miniconda.sh -b -p $HOME/miniconda -# source ~/miniconda/bin/activate -# conda init zsh -# -# Then, `git clone DeepLabCut`, and run: -# -# conda env create -f conda-environments/DEEPLABCUT_M1.yaml -# -# Next, activate the environment, and launch DLC with pythonw -m deeplabcut - -name: DEEPLABCUT_M1 -channels: - - conda-forge - - defaults -dependencies: - - python=3.9 - - pip - - ipython - - jupyter - - nb_conda - - notebook<7.0.0 - - python.app - - ffmpeg - - apple::tensorflow-deps - - pip: - - "deeplabcut[gui,apple_mchips]" diff --git a/conda-environments/README.md b/conda-environments/README.md index 9a182095a6..e7ff0fee0f 100644 --- a/conda-environments/README.md +++ b/conda-environments/README.md @@ -1 +1 @@ -### Please head over to [Installation](/docs/installation.md) to see how to utilize our supplied conda envs! +# Please head over to [Installation](/docs/installation.md) to see how to utilize our supplied conda envs! diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index 2da4b6a9f5..df55aec39b 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -9,119 +9,269 @@ # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations +import logging import os +from importlib import import_module +from typing import Any -# Suppress tensorflow warning messages -import tensorflow as tf - -tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR) -DEBUG = True and "DEBUG" in os.environ and os.environ["DEBUG"] -from deeplabcut.version import __version__, VERSION - -print(f"Loading DLC {VERSION}...") - -try: - from deeplabcut.gui.tracklet_toolbox import refine_tracklets - from deeplabcut.gui.launch_script import launch_dlc - from deeplabcut.gui.tabs.label_frames import ( - label_frames, - refine_labels, - ) - from deeplabcut.gui.widgets import SkeletonBuilder -except (ModuleNotFoundError, ImportError): - print( - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)" - ) - -from deeplabcut.create_project import ( +logger = logging.getLogger(__name__) + +# DEBUG="", "0", "false", "no" -> False +DEBUG = os.environ.get("DEBUG", "").strip().lower() not in {"", "0", "false", "no"} + +from .version import VERSION, __version__ + +if DEBUG: + logger.debug("Loading DLC %s", VERSION) + +# ----------------------------------------------------------------------------- +# Always-available public API +# ----------------------------------------------------------------------------- + +# Train / evaluate / predict functions (compat layer) +from .compat import ( + analyze_images, + analyze_time_lapse_frames, + analyze_videos, + convert_detections2tracklets, + create_tracking_dataset, + evaluate_network, + export_model, + extract_maps, + extract_save_all_maps, + return_evaluate_network_data, + return_train_network_path, + train_network, + visualize_locrefs, + visualize_paf, + visualize_scoremaps, +) +from .core.engine import Engine +from .create_project import ( + add_new_videos, create_new_project, create_new_project_3d, - add_new_videos, - load_demo_data, - create_pretrained_project, create_pretrained_human_project, + create_pretrained_project, + load_demo_data, ) -from deeplabcut.generate_training_dataset import ( +from .generate_training_dataset import ( + adddatasetstovideolistandviceversa, check_labels, + comparevideolistsanddatafolders, + create_multianimaltraining_dataset, create_training_dataset, - extract_frames, - mergeandsplit, -) -from deeplabcut.generate_training_dataset import ( + create_training_dataset_from_existing_split, create_training_model_comparison, - create_multianimaltraining_dataset, -) -from deeplabcut.generate_training_dataset import ( dropannotationfileentriesduetodeletedimages, - comparevideolistsanddatafolders, - dropimagesduetolackofannotation, - adddatasetstovideolistandviceversa, dropduplicatesinannotatinfiles, + dropimagesduetolackofannotation, dropunlabeledframes, + extract_frames, + mergeandsplit, ) -from deeplabcut.utils import ( - create_labeled_video, - create_video_with_all_detections, - plot_trajectories, - auxiliaryfunctions, - convert2_maDLC, - convertcsv2h5, +from .modelzoo.video_inference import video_inference_superanimal +from .pose_estimation_3d import ( + calibrate_cameras, + check_undistortion, + create_labeled_video_3d, + triangulate, +) +from .post_processing import analyzeskeleton, filterpredictions +from .refine_training_dataset import ( + extract_outlier_frames, + find_outliers_in_raw_data, + merge_datasets, +) +from .refine_training_dataset.stitch import stitch_tracklets +from .utils import ( analyze_videos_converth5_to_csv, analyze_videos_converth5_to_nwb, auxfun_videos, + auxiliaryfunctions, + convert2_maDLC, + convertcsv2h5, + create_labeled_video, + create_video_with_all_detections, + plot_trajectories, ) - -try: - from deeplabcut.pose_tracking_pytorch import transformer_reID -except ModuleNotFoundError as e: - import warnings - - warnings.warn( - """ - As PyTorch is not installed, unsupervised identity learning will not be available. - Please run `pip install torch`, or ignore this warning. - """ - ) - -from deeplabcut.utils.auxfun_videos import ( - ShortenVideo, - DownSampleVideo, +from .utils.auxfun_videos import ( CropVideo, + DownSampleVideo, + ShortenVideo, check_video_integrity, + collect_video_paths, ) -# Train, evaluate & predict functions / all require TF -from deeplabcut.pose_estimation_tensorflow import ( - train_network, - return_train_network_path, - evaluate_network, - return_evaluate_network_data, - analyze_videos, - create_tracking_dataset, - analyze_time_lapse_frames, - convert_detections2tracklets, - extract_maps, - visualize_scoremaps, - visualize_locrefs, - visualize_paf, - extract_save_all_maps, - export_model, - video_inference_superanimal, -) +# ----------------------------------------------------------------------------- +# Optional / lazy public API +# ----------------------------------------------------------------------------- +# These names are part of the public API, but importing them may require +# optional GUI or torch dependencies, so we lazy load them. +# +# Example: +# import deeplabcut as dlc +# dlc.launch_dlc() # imports GUI code lazily +# dlc.transformer_reID(...) # imports torch-dependent code lazily +# ----------------------------------------------------------------------------- +_OPTIONAL_EXPORTS: dict[str, tuple[str, str]] = { + # GUI + "launch_dlc": (".gui.launch_script", "launch_dlc"), + "label_frames": (".gui.tabs.label_frames", "label_frames"), + "refine_labels": (".gui.tabs.label_frames", "refine_labels"), + "refine_tracklets": (".gui.tracklet_toolbox", "refine_tracklets"), + "SkeletonBuilder": (".gui.widgets", "SkeletonBuilder"), + # Optional torch feature + "transformer_reID": (".pose_tracking_pytorch", "transformer_reID"), +} -from deeplabcut.pose_estimation_3d import ( - calibrate_cameras, - check_undistortion, - triangulate, - create_labeled_video_3d, -) -from deeplabcut.refine_training_dataset.stitch import stitch_tracklets -from deeplabcut.refine_training_dataset import ( - extract_outlier_frames, - merge_datasets, - find_outliers_in_raw_data, +def __getattr__(name: str) -> Any: + """Lazily load optional public exports.""" + if name not in _OPTIONAL_EXPORTS: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + module_name, attr_name = _OPTIONAL_EXPORTS[name] + + try: + module = import_module(module_name, package=__name__) + value = getattr(module, attr_name) + except (ModuleNotFoundError, ImportError) as exc: + if name in { + "launch_dlc", + "label_frames", + "refine_labels", + "refine_tracklets", + "SkeletonBuilder", + }: + raise AttributeError( + f"{name!r} is unavailable because DeepLabCut was loaded without GUI dependencies." + ) from exc + + if name == "transformer_reID": + raise AttributeError( + f"{name!r} is unavailable because the PyTorch-based tracking dependencies are not installed." + ) from exc + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc + + # Cache the resolved object so future access is fast + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """Improve IDE / autocomplete discoverability.""" + return sorted(set(globals()) | set(__all__)) + + +# ----------------------------------------------------------------------------- +# Public API +# ----------------------------------------------------------------------------- + +_VERSION_EXPORTS = [ + "__version__", + "VERSION", + "DEBUG", +] + +_CORE_EXPORTS = [ + "Engine", +] + +_PROJECT_EXPORTS = [ + "add_new_videos", + "create_new_project", + "create_new_project_3d", + "create_pretrained_human_project", + "create_pretrained_project", + "load_demo_data", +] + +_DATASET_EXPORTS = [ + "adddatasetstovideolistandviceversa", + "check_labels", + "comparevideolistsanddatafolders", + "create_multianimaltraining_dataset", + "create_training_dataset", + "create_training_dataset_from_existing_split", + "create_training_model_comparison", + "dropannotationfileentriesduetodeletedimages", + "dropduplicatesinannotatinfiles", + "dropimagesduetolackofannotation", + "dropunlabeledframes", + "extract_frames", + "mergeandsplit", +] + +_COMPAT_EXPORTS = [ + "analyze_images", + "analyze_time_lapse_frames", + "analyze_videos", + "convert_detections2tracklets", + "create_tracking_dataset", + "evaluate_network", + "export_model", + "extract_maps", + "extract_save_all_maps", + "return_evaluate_network_data", + "return_train_network_path", + "train_network", + "visualize_locrefs", + "visualize_paf", + "visualize_scoremaps", +] + +_UTIL_EXPORTS = [ + "analyze_videos_converth5_to_csv", + "analyze_videos_converth5_to_nwb", + "auxfun_videos", + "auxiliaryfunctions", + "convert2_maDLC", + "convertcsv2h5", + "create_labeled_video", + "create_video_with_all_detections", + "plot_trajectories", + "CropVideo", + "DownSampleVideo", + "ShortenVideo", + "check_video_integrity", +] + +_POST_PROCESSING_EXPORTS = [ + "analyzeskeleton", + "filterpredictions", + "extract_outlier_frames", + "find_outliers_in_raw_data", + "merge_datasets", + "stitch_tracklets", +] + +_THREE_D_EXPORTS = [ + "calibrate_cameras", + "check_undistortion", + "create_labeled_video_3d", + "triangulate", +] + +_MODELZOO_EXPORTS = [ + "video_inference_superanimal", +] + +_OPTIONAL_API_EXPORTS = list(_OPTIONAL_EXPORTS) + +__all__ = ( + _VERSION_EXPORTS + + _CORE_EXPORTS + + _PROJECT_EXPORTS + + _DATASET_EXPORTS + + _COMPAT_EXPORTS + + _UTIL_EXPORTS + + _POST_PROCESSING_EXPORTS + + _THREE_D_EXPORTS + + _MODELZOO_EXPORTS + + _OPTIONAL_API_EXPORTS ) -from deeplabcut.post_processing import filterpredictions, analyzeskeleton diff --git a/deeplabcut/__main__.py b/deeplabcut/__main__.py index 93b3f44b64..ea7e3ca1c6 100644 --- a/deeplabcut/__main__.py +++ b/deeplabcut/__main__.py @@ -8,21 +8,29 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from importlib import import_module -try: - import PySide6 - lite = False -except ModuleNotFoundError: - lite = True +def main(): + try: + import_module("PySide6") -# if module is executed directly (i.e. `python -m deeplabcut.__init__`) launch straight into the GUI -if not lite: - print("Starting GUI...") - from deeplabcut.gui.launch_script import launch_dlc + lite = False + except ModuleNotFoundError: + lite = True - launch_dlc() -else: - print( - "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install 'deeplabcut[gui]''" - ) + # if module is executed directly (i.e. `python -m deeplabcut.__init__`) launch straight into the GUI + if not lite: + print("Starting GUI...") + from deeplabcut.gui.launch_script import launch_dlc + + launch_dlc() + else: + print( + "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install" + "'deeplabcut[gui]''" + ) + + +if __name__ == "__main__": + main() diff --git a/deeplabcut/benchmark/__init__.py b/deeplabcut/benchmark/__init__.py index 2e70eae786..e663705b8e 100644 --- a/deeplabcut/benchmark/__init__.py +++ b/deeplabcut/benchmark/__init__.py @@ -12,7 +12,7 @@ import json import os -from typing import Container +from collections.abc import Container from typing import Literal from deeplabcut.benchmark.base import Benchmark, Result, ResultCollection @@ -38,9 +38,7 @@ class needs to be a subclass of the ``benchmark.base.Benchmark`` not a subclass of ``benchmark.base.Benchmark``. """ if not issubclass(cls, Benchmark): - raise ValueError( - f"Can only register subclasses of {type(Benchmark)}, " f"but got {cls}." - ) + raise ValueError(f"Can only register subclasses of {type(Benchmark)}, but got {cls}.") __registry.append(cls) @@ -85,7 +83,14 @@ def evaluate( continue benchmark = benchmark_cls() for name in benchmark.names(): - if Result(method_name=name, benchmark_name=benchmark_cls.name) in results: + if ( + Result( + code=benchmark.code, + method_name=name, + benchmark_name=benchmark_cls.name, + ) + in results + ): continue else: result = benchmark.evaluate(name, on_error=on_error) @@ -102,14 +107,12 @@ def savecache(results: ResultCollection): json.dump(results.todicts(), fh, indent=2) -def loadcache( - cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore" -) -> ResultCollection: +def loadcache(cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore") -> ResultCollection: if not os.path.exists(cache): if on_missing == "raise": raise FileNotFoundError(cache) return ResultCollection() - with open(cache, "r") as fh: + with open(cache) as fh: try: data = json.load(fh) except json.decoder.JSONDecodeError as e: diff --git a/deeplabcut/benchmark/base.py b/deeplabcut/benchmark/base.py index b9465cf07d..c19448ed9d 100644 --- a/deeplabcut/benchmark/base.py +++ b/deeplabcut/benchmark/base.py @@ -9,7 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # -"""Base classes for benchmark and result definition +"""Base classes for benchmark and result definition. Benchmarks subclass the abstract ``Benchmark`` class and are defined by ``name``, their ``keypoints`` names, as well as groundtruth and metadata necessary to run evaluation. @@ -23,8 +23,8 @@ import abc import dataclasses -from typing import Iterable -from typing import Tuple +import warnings +from collections.abc import Iterable import pandas as pd @@ -46,9 +46,9 @@ class Benchmark(abc.ABC): def names(self): """A unique key to describe this submission, e.g. the model name. - This is also the name that will later appear in the benchmark table. - The name needs to be unique across the whole benchmark. Non-unique names - will raise an error during submission of a PR. + This is also the name that will later appear in the benchmark table. The name + needs to be unique across the whole benchmark. Non-unique names will raise an + error during submission of a PR. """ raise NotImplementedError() @@ -58,13 +58,10 @@ def get_predictions(self): raise NotImplementedError() def __init__(self): - keys = ["name", "keypoints", "ground_truth", "metadata"] + keys = ["code", "name", "keypoints", "ground_truth", "metadata"] for key in keys: if not hasattr(self, key): - raise NotImplementedError( - f"Subclass of abstract Benchmark class need " - f"to define the {key} property." - ) + raise NotImplementedError(f"Subclass of abstract Benchmark class need to define the {key} property.") def compute_pose_rmse(self, results_objects): return deeplabcut.benchmark.metrics.calc_rmse_from_obj( @@ -80,15 +77,14 @@ def evaluate(self, name: str, on_error="raise"): """Evaluate this benchmark with all registered methods.""" if name not in self.names(): - raise ValueError( - f"{name} is not registered. Valid names are {self.names()}" - ) + raise ValueError(f"{name} is not registered. Valid names are {self.names()}") if on_error not in ("ignore", "return", "raise"): raise ValueError(f"on_error got an undefined value: {on_error}") mean_avg_precision = float("nan") root_mean_squared_error = float("nan") try: predictions = self.get_predictions(name) + predictions = self._validate_predictions(name, predictions) mean_avg_precision = self.compute_pose_map(predictions) root_mean_squared_error = self.compute_pose_rmse(predictions) except Exception as exception: @@ -102,23 +98,41 @@ def evaluate(self, name: str, on_error="raise"): pass elif on_error == "raise": # raise the error and stop evaluation - raise BenchmarkEvaluationError( - f"Error during benchmark evaluation for model {name}" - ) from exception + raise BenchmarkEvaluationError(f"Error during benchmark evaluation for model {name}") from exception else: raise NotImplementedError() from exception return Result( + code=self.code, method_name=name, benchmark_name=self.name, mean_avg_precision=mean_avg_precision, root_mean_squared_error=root_mean_squared_error, ) + def _validate_predictions(self, name: str, predictions: dict) -> dict: + """Validates the submitted predictions object Checks that there is a prediction + for each test image, and raises a warning if that is not the case. + + Returns only predictions made for test images. + """ + test_images = deeplabcut.benchmark.metrics.load_test_images(self.ground_truth, self.metadata) + missing_images = set(test_images) - set(predictions.keys()) + if len(missing_images) > 0: + warnings.warn( + f"Missing {len(missing_images)} test images in the predictions for " + f"{name}: {list(missing_images)} Metrics will be computed as if no " + "individuals were detected in those images.", + stacklevel=2, + ) + + return {img: predictions.get(img, tuple()) for img in test_images} + @dataclasses.dataclass class Result: """Benchmark result.""" + code: str method_name: str benchmark_name: str root_mean_squared_error: float = float("nan") @@ -126,6 +140,7 @@ class Result: benchmark_version: str = __version__ _export_mapping = dict( + code="code", benchmark_name="benchmark", method_name="method", benchmark_version="version", @@ -136,13 +151,13 @@ class Result: _primary_key = ("benchmark_name", "method_name", "benchmark_version") @property - def primary_key(self) -> Tuple[str]: + def primary_key(self) -> tuple[str]: """The primary key to uniquely identify this result.""" return tuple(getattr(self, k) for k in self._primary_key) @property - def primary_key_names(self) -> Tuple[str]: - """Names of the primary keys""" + def primary_key_names(self) -> tuple[str]: + """Names of the primary keys.""" return tuple(self._export_mapping.get(k) for k in self._primary_key) def __str__(self): @@ -172,10 +187,10 @@ def primary_key_names(self): return next(iter(self.results.values())).primary_key_names def toframe(self) -> pd.DataFrame: - """Convert results to pandas dataframe""" - return pd.DataFrame( - [result.todict() for result in self.results.values()] - ).set_index(list(self.primary_key_names)) + """Convert results to pandas dataframe.""" + return pd.DataFrame([result.todict() for result in self.results.values()]).set_index( + list(self.primary_key_names) + ) def add(self, result: Result): """Add a result to the collection.""" @@ -202,10 +217,7 @@ def __len__(self): def __contains__(self, other: Result): if not isinstance(other, Result): - raise ValueError( - f"{type(self)} can only store objects of type Result, " - f"but got {type(other)}." - ) + raise ValueError(f"{type(self)} can only store objects of type Result, but got {type(other)}.") return other.primary_key in self.results def __eq__(self, other): diff --git a/deeplabcut/benchmark/benchmarks.py b/deeplabcut/benchmark/benchmarks.py index ee18e215c6..0701714a4a 100644 --- a/deeplabcut/benchmark/benchmarks.py +++ b/deeplabcut/benchmark/benchmarks.py @@ -24,9 +24,19 @@ class TriMouseBenchmark(deeplabcut.benchmark.base.Benchmark): """Datasets with three mice with a top-view camera. - Three wild-type (C57BL/6J) male mice ran on a paper spool following odor trails (Mathis et al 2018). These experiments were carried out in the laboratory of Venkatesh N. Murthy at Harvard University. Data were recorded at 30 Hz with 640 x 480 pixels resolution acquired with a Point Grey Firefly FMVU-03MTM-CS. One human annotator was instructed to localize the 12 keypoints (snout, left ear, right ear, shoulder, four spine points, tail base and three tail points). All surgical and experimental procedures for mice were in accordance with the National Institutes of Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard Institutional Animal Care and Use Committee. 161 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + Three wild-type (C57BL/6J) male mice ran on a paper spool following odor trails + (Mathis et al 2018). These experiments were carried out in the laboratory of + Venkatesh N. Murthy at Harvard University. Data were recorded at 30 Hz with 640 x + 480 pixels resolution acquired with a Point Grey Firefly FMVU-03MTM-CS. One human + annotator was instructed to localize the 12 keypoints (snout, left ear, right ear, + shoulder, four spine points, tail base and three tail points). All surgical and + experimental procedures for mice were in accordance with the National Institutes of + Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard + Institutional Animal Care and Use Committee. 161 frames were labeled, making this a + real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "trimouse" @@ -45,18 +55,34 @@ class TriMouseBenchmark(deeplabcut.benchmark.base.Benchmark): "tailend", ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Daniel.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-MultiMouse_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-MultiMouse_70shuffle1.pickle") num_animals = 3 class ParentingMouseBenchmark(deeplabcut.benchmark.base.Benchmark): """Datasets with three mice, one parenting, two pups. - Parenting behavior is a pup directed behavior observed in adult mice involving complex motor actions directed towards the benefit of the offspring. These experiments were carried out in the laboratory of Catherine Dulac at Harvard University. The behavioral assay was performed in the homecage of singly housed adult female mice in dark/red light conditions. For these videos, the adult mice was monitored for several minutes in the cage followed by the introduction of pup (4 days old) in one corner of the cage. The behavior of the adult and pup was monitored for a duration of 15 minutes. Video was recorded at 30Hz using a Microsoft LifeCam camera (Part#: 6CH-00001) with a resolution of 1280 x 720 pixels or a Geovision camera (model no.: GV-BX4700-3V) also acquired at 30 frames per second at a resolution of 704 x 480 pixels. A human annotator labeled on the adult animal the same 12 body points as in the tri-mouse dataset, and five body points on the pup along its spine. Initially only the two ends were labeled, and intermediate points were added by interpolation and their positions was manually adjusted if necessary. All surgical and experimental procedures for mice were in accordance with the National Institutes of Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard Institutional Animal Care and Use Committee. 542 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + Parenting behavior is a pup directed behavior observed in adult mice involving + complex motor actions directed towards the benefit of the offspring. These + experiments were carried out in the laboratory of Catherine Dulac at Harvard + University. The behavioral assay was performed in the homecage of singly housed + adult female mice in dark/red light conditions. For these videos, the adult mice was + monitored for several minutes in the cage followed by the introduction of pup (4 + days old) in one corner of the cage. The behavior of the adult and pup was monitored + for a duration of 15 minutes. Video was recorded at 30Hz using a Microsoft LifeCam + camera (Part#: 6CH-00001) with a resolution of 1280 x 720 pixels or a Geovision + camera (model no.: GV-BX4700-3V) also acquired at 30 frames per second at a + resolution of 704 x 480 pixels. A human annotator labeled on the adult animal the + same 12 body points as in the tri-mouse dataset, and five body points on the pup + along its spine. Initially only the two ends were labeled, and intermediate points + were added by interpolation and their positions was manually adjusted if necessary. + All surgical and experimental procedures for mice were in accordance with the + National Institutes of Health Guide for the Care and Use of Laboratory Animals and + approved by the Harvard Institutional Animal Care and Use Committee. 542 frames were + labeled, making this a real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "parenting" @@ -81,9 +107,7 @@ class ParentingMouseBenchmark(deeplabcut.benchmark.base.Benchmark): ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mostafizur.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-CrackingParenting_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-CrackingParenting_70shuffle1.pickle") num_animals = 2 def compute_pose_map(self, results_objects): @@ -96,13 +120,32 @@ def compute_pose_map(self, results_objects): symmetric_kpts=[(0, 4), (1, 3)], ) + def _validate_predictions(self, name: str, predictions: dict) -> dict: + """Fixes filenames for predictions made on old versions of the dataset.""" + return super()._validate_predictions( + name, + {k.replace("Dummy", "D").replace("Dead pup", "DP"): v for k, v in predictions.items()}, + ) + class MarmosetBenchmark(deeplabcut.benchmark.base.Benchmark): """Dataset with two marmosets. - All animal procedures are overseen by veterinary staff of the MIT and Broad Institute Department of Comparative Medicine, in compliance with the NIH guide for the care and use of laboratory animals and approved by the MIT and Broad Institute animal care and use committees. Video of common marmosets (Callithrix jacchus) was collected in the laboratory of Guoping Feng at MIT. Marmosets were recorded using Kinect V2 cameras (Microsoft) with a resolution of 1080p and frame rate of 30 Hz. After acquisition, images to be used for training the network were manually cropped to 1000 x 1000 pixels or smaller. The dataset is 7,600 labeled frames from 40 different marmosets collected from 3 different colonies (in different facilities). Each cage contains a pair of marmosets, where one marmoset had light blue dye applied to its tufts. One human annotator labeled the 15 marker points on each animal present in the frame (frames contained either 1 or 2 animals). - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + All animal procedures are overseen by veterinary staff of the MIT and Broad + Institute Department of Comparative Medicine, in compliance with the NIH guide for + the care and use of laboratory animals and approved by the MIT and Broad Institute + animal care and use committees. Video of common marmosets (Callithrix jacchus) was + collected in the laboratory of Guoping Feng at MIT. Marmosets were recorded using + Kinect V2 cameras (Microsoft) with a resolution of 1080p and frame rate of 30 Hz. + After acquisition, images to be used for training the network were manually cropped + to 1000 x 1000 pixels or smaller. The dataset is 7,600 labeled frames from 40 + different marmosets collected from 3 different colonies (in different facilities). + Each cage contains a pair of marmosets, where one marmoset had light blue dye + applied to its tufts. One human annotator labeled the 15 marker points on each + animal present in the frame (frames contained either 1 or 2 animals). + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "marmosets" @@ -124,26 +167,34 @@ class MarmosetBenchmark(deeplabcut.benchmark.base.Benchmark): "Body3", ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mackenzie.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-Marmoset_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-Marmoset_70shuffle1.pickle") num_animals = 2 class FishBenchmark(deeplabcut.benchmark.base.Benchmark): - """Dataset with multiple fish, filmed from top-view - - Schools of inland silversides (Menidia beryllina, n=14 individuals per school) were recorded in the Lauder Lab at Harvard University while swimming at 15 speeds (0.5 to 8 BL/s, body length, at 0.5 BL/s intervals) in a flow tank with a total working section of 28 x 28 x 40 cm as described in previous work, at a constant temperature (18±1°C) and salinity (33 ppt), at a Reynolds number of approximately 10,000 (based on BL). Dorsal views of steady swimming across these speeds were recorded by high-speed video cameras (FASTCAM Mini AX50, Photron USA, San Diego, CA, USA) at 60-125 frames per second (feeding videos at 60 fps, swimming alone 125 fps). The dorsal view was recorded above the swim tunnel and a floating Plexiglas panel at the water surface prevented surface ripples from interfering with dorsal view videos. Five keypoints were labeled (tip, gill, peduncle, dorsal fin tip, caudal tip). 100 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + """Dataset with multiple fish, filmed from top-view. + + Schools of inland silversides (Menidia beryllina, n=14 individuals per school) were + recorded in the Lauder Lab at Harvard University while swimming at 15 speeds (0.5 to + 8 BL/s, body length, at 0.5 BL/s intervals) in a flow tank with a total working + section of 28 x 28 x 40 cm as described in previous work, at a constant temperature + (18±1°C) and salinity (33 ppt), at a Reynolds number of approximately 10,000 (based + on BL). Dorsal views of steady swimming across these speeds were recorded by high- + speed video cameras (FASTCAM Mini AX50, Photron USA, San Diego, CA, USA) at 60-125 + frames per second (feeding videos at 60 fps, swimming alone 125 fps). The dorsal + view was recorded above the swim tunnel and a floating Plexiglas panel at the water + surface prevented surface ripples from interfering with dorsal view videos. Five + keypoints were labeled (tip, gill, peduncle, dorsal fin tip, caudal tip). 100 frames + were labeled, making this a real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "fish" keypoints = ("tip", "gill", "peduncle", "caudaltip", "dfintip") ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Valentina.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-Schooling_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-Schooling_70shuffle1.pickle") num_animals = 14 def compute_pose_rmse(self, results_objects): diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py index a3b000ed3f..91f34a457b 100644 --- a/deeplabcut/benchmark/metrics.py +++ b/deeplabcut/benchmark/metrics.py @@ -11,15 +11,6 @@ """Evaluation metrics for the DeepLabCut benchmark.""" -import sys -import unittest.mock - -# TODO(stes) mocking a few modules to rely in fewer dependencies, without -# causing import errors when using deeplabcut. -MOCK_MODULES = ["statsmodels", "statsmodels.api", "pytables"] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = unittest.mock.MagicMock() - import os import pickle from collections import defaultdict @@ -28,22 +19,17 @@ import pandas as pd import deeplabcut.benchmark.utils -from deeplabcut.pose_estimation_tensorflow.core import evaluate_multianimal -from deeplabcut.pose_estimation_tensorflow.lib import inferenceutils +from deeplabcut.core import crossvalutils, inferenceutils from deeplabcut.utils.conversioncode import guarantee_multiindex_rows -def _format_gt_data(h5file): +def _format_gt_data(h5file: str, test_indices: list[int] | None = None): df = pd.read_hdf(h5file) animals = _get_unique_level_values(df.columns, "individuals") kpts = _get_unique_level_values(df.columns, "bodyparts") try: - n_unique = len( - _get_unique_level_values( - df.xs("single", level="individuals", axis=1).columns, "bodyparts" - ) - ) + n_unique = len(_get_unique_level_values(df.xs("single", level="individuals", axis=1).columns, "bodyparts")) except KeyError: n_unique = 0 guarantee_multiindex_rows(df) @@ -54,9 +40,13 @@ def _format_gt_data(h5file): .reindex(kpts, level="bodyparts", axis=1) ) data = temp.to_numpy().reshape((len(file_paths), len(animals), -1, 2)) + if test_indices is not None: + file_paths = [file_paths[i] for i in test_indices] + data = [data[i] for i in test_indices] + meta = {"animals": animals, "keypoints": kpts, "n_unique": n_unique} return { - "annotations": dict(zip(file_paths, data)), + "annotations": dict(zip(file_paths, data, strict=False)), "metadata": meta, } @@ -94,9 +84,7 @@ def calc_prediction_errors(preds, gt): if visible.size and xy_pred_.size: # Pick the predictions closest to ground truth, # rather than the ones the model has most confident in. - neighbors = evaluate_multianimal._find_closest_neighbors( - xy_gt_[visible], xy_pred_, k=3 - ) + neighbors = crossvalutils.find_closest_neighbors(xy_gt_[visible], xy_pred_, k=3) found = neighbors != -1 if ~np.any(found): continue @@ -111,10 +99,8 @@ def calc_prediction_errors(preds, gt): def _map(strings, substrings): - """ - Map image paths from predicted data to GT as the first are typically - absolute whereas the latter are relative to the project path. - """ + """Map image paths from predicted data to GT as the first are typically absolute + whereas the latter are relative to the project path.""" lookup = dict() strings_ = strings.copy() @@ -167,16 +153,22 @@ def calc_map_from_obj( pass n_animals = len(df.columns.get_level_values("individuals").unique()) kpts = list(df.columns.get_level_values("bodyparts").unique()) - image_paths = list(eval_results_obj) - ground_truth = ( - df.loc[image_paths].to_numpy().reshape((len(image_paths), n_animals, -1, 2)) - ) + + test_indices = _load_test_indices(metadata_file) + df_test = df.iloc[test_indices] + test_images = load_test_images(h5_file, metadata_file) + missing_images = set(test_images) - set(eval_results_obj.keys()) + if len(missing_images) > 0: + raise ValueError( + f"Failed to compute the test mAP: there are test images missing from theprediction object: {missing_images}" + ) + + ground_truth = df_test.to_numpy().reshape((len(test_images), n_animals, -1, 2)) temp = np.ones((*ground_truth.shape[:3], 3)) temp[..., :2] = ground_truth - assemblies_gt = inferenceutils._parse_ground_truth_data(temp) - with open(metadata_file, "rb") as f: - inds_test = set(pickle.load(f)[2]) - assemblies_gt_test = {k: v for k, v in assemblies_gt.items() if k in inds_test} + assemblies_gt_test = { + test_images[i]: assembly for i, assembly in inferenceutils._parse_ground_truth_data(temp).items() + } # TODO(stes): remove/rewrite if drop_kpts is not None: @@ -192,9 +184,7 @@ def calc_map_from_obj( for ind in sorted(drop_kpts, reverse=True): kpts.pop(ind) - assemblies_pred_ = conv_obj_to_assemblies(eval_results_obj, kpts) - assemblies_pred = dict(enumerate(assemblies_pred_.values())) - + assemblies_pred = conv_obj_to_assemblies(eval_results_obj, kpts) with deeplabcut.benchmark.utils.DisableOutput(): oks = inferenceutils.evaluate_assembly( assemblies_pred, @@ -202,6 +192,7 @@ def calc_map_from_obj( oks_sigma, margin=margin, symmetric_kpts=symmetric_kpts, + greedy_matching=True, ) return oks["mAP"] @@ -213,18 +204,24 @@ def calc_rmse_from_obj( drop_kpts=None, ): """Calc prediction errors for submissions.""" - gt = _format_gt_data(h5_file) + test_indices = _load_test_indices(metadata_file) + gt = _format_gt_data(h5_file, test_indices=test_indices) kpts = gt["metadata"]["keypoints"] if drop_kpts: for k, v in gt["annotations"].items(): gt["annotations"][k] = np.delete(v, drop_kpts, axis=1) for ind in sorted(drop_kpts, reverse=True): kpts.pop(ind) - with open(metadata_file, "rb") as f: - inds_test = set(pickle.load(f)[2]) - test_objects = { - k: v for i, (k, v) in enumerate(eval_results_obj.items()) if i in inds_test - } + + test_objects = {k: v for k, v in eval_results_obj.items() if k in gt["annotations"].keys()} + if len(gt["annotations"]) != len(test_objects): + gt_images = list(gt["annotations"].keys()) + missing_images = [img for img in gt_images if img not in test_objects] + raise ValueError( + "Failed to compute the test RMSE: there are test images missing from the" + f"prediction object: {missing_images}" + ) + assemblies_pred = conv_obj_to_assemblies(test_objects, kpts) preds = defaultdict(dict) preds["metadata"]["keypoints"] = kpts @@ -240,3 +237,24 @@ def calc_rmse_from_obj( with deeplabcut.benchmark.utils.DisableOutput(): errors = calc_prediction_errors(preds, gt) return np.nanmean(errors[..., 0]) + + +def load_test_images(h5file: str, metadata: str) -> list[str]: + """Returns the names of the test images for the benchmark, in the order + corresponding to the test indices.""" + df = pd.read_hdf(h5file) + test_indices = _load_test_indices(metadata) + df_test = df.iloc[test_indices] + test_images = [] + for img_path in df_test.index: + if not isinstance(img_path, str): + img_path = os.path.join(*img_path) + test_images.append(img_path) + return test_images + + +def _load_test_indices(shuffle_metadata_path: str) -> list[int]: + """Returns the indices of test images in the training dataset dataframe.""" + with open(shuffle_metadata_path, "rb") as f: + test_indices = set([int(i) for i in pickle.load(f)[2]]) + return list(sorted(test_indices)) diff --git a/deeplabcut/benchmark/mot.py b/deeplabcut/benchmark/mot.py new file mode 100644 index 0000000000..4c0f8e6e29 --- /dev/null +++ b/deeplabcut/benchmark/mot.py @@ -0,0 +1,219 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from __future__ import annotations + +import warnings + +import motmetrics as mm +import numpy as np +import pandas as pd +from numpy.typing import NDArray + +from deeplabcut.core import trackingutils + + +def convert_bboxes_to_xywh(bboxes: NDArray, inplace: bool = False) -> NDArray: + """Converts bounding box coordinates from [x_min, y_min, x_max, y_max] format to [x, + y, width, height] format. + + Parameters + ---------- + bbox : numpy.ndarray + A 2D array of shape (N, M), where N is the number of bounding boxes + and M >= 4. The first four columns represent the bounding box in the format + [x_min, y_min, x_max, y_max]. + inplace : bool, optional + If True, modifies the input array in place. If False, returns a copy of + the array with the converted bounding box format. Defaults to False. + + Returns + ------- + numpy.ndarray or None + If `inplace` is False, returns a new array of the same shape as `bbox` + with the format [x, y, width, height]. If `inplace` is True, the input + array is modified directly, and nothing is returned. + """ + w = bboxes[:, 2] - bboxes[:, 0] + h = bboxes[:, 3] - bboxes[:, 1] + if not inplace: + new_bboxes = bboxes.copy() + new_bboxes[:, 2] = w + new_bboxes[:, 3] = h + return new_bboxes + bboxes[:, 2] = w + bboxes[:, 3] = h + + +_convert_bboxes_to_xywh = convert_bboxes_to_xywh + + +def reconstruct_bboxes_from_bodyparts(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: + """Reconstructs bounding boxes from body part coordinates and likelihoods. + + Parameters + ---------- + data : pandas.DataFrame + A DataFrame containing body part data with a multi-level column index. + The expected levels include 'x', 'y', and 'likelihood', where: + - 'x' and 'y' contain the coordinates of the body parts. + - 'likelihood' contains the confidence scores for each body part. + margin : float + The margin to add/subtract from the minimum/maximum coordinates when defining the bounding box. + to_xywh : bool, optional + If True, converts the bounding box format from [x_min, y_min, x_max, y_max] + to [x, y, width, height]. Defaults to False. + + Returns + ------- + numpy.ndarray + An array of shape (N, 5), where N is the number of rows in `data`. + Each row represents a bounding box with the following values: + - [x_min, y_min, x_max, y_max, likelihood] + If `to_xywh` is True, the format will be [x, y, width, height, likelihood]. + + Notes + ----- + - NaN values in the input data are ignored when computing the bounding box dimensions. + - Warnings related to NaN values are suppressed during calculations. + """ + x = data.xs("x", axis=1, level="coords") + y = data.xs("y", axis=1, level="coords") + p = data.xs("likelihood", axis=1, level="coords") + xy = np.stack([x, y], axis=2) + bboxes = np.full((data.shape[0], 5), np.nan) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + bboxes[:, :2] = np.nanmin(xy, axis=1) - margin + bboxes[:, 2:4] = np.nanmax(xy, axis=1) + margin + bboxes[:, 4] = np.nanmean(p, axis=1) + if to_xywh: + convert_bboxes_to_xywh(bboxes, inplace=True) + return bboxes + + +def reconstruct_all_bboxes(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: + """Reconstructs bounding boxes for multiple individuals from body part data. + + Parameters + ---------- + data : pandas.DataFrame + A DataFrame containing body part data with a multi-level column index. + The expected levels include: + - 'individuals': Names of the individuals (e.g., animals). + - 'x', 'y', and 'likelihood': Coordinate and confidence data for body parts. + margin : float + The margin to add/subtract from the minimum/maximum coordinates when defining the bounding box. + to_xywh : bool + If True, converts the bounding box format from [x_min, y_min, x_max, y_max] + to [x, y, width, height]. + + Returns + ------- + numpy.ndarray + A 3D array of shape (A, F, 5), where: + - A is the number of individuals (excluding 'single', if present). + - F is the number of frames (rows) in the input `data`. + - Each bounding box is represented as [x_min, y_min, x_max, y_max, likelihood]. + If `to_xywh` is True, the format will be [x, y, width, height, likelihood]. + + Notes + ----- + - Individuals are extracted from the 'individuals' level of the DataFrame columns. + - If an individual named 'single' exists, it is excluded from the bounding box computation. + - NaN values in the input data are ignored during calculations. + """ + animals = data.columns.get_level_values("individuals").unique().tolist() + try: + animals.remove("single") + except ValueError: + pass + bboxes = np.full((len(animals), data.shape[0], 5), np.nan) + for n, animal in enumerate(animals): + bboxes[n] = reconstruct_bboxes_from_bodyparts(data.xs(animal, axis=1, level="individuals"), margin, to_xywh) + return bboxes + + +def compute_mot_metrics( + h5_file_gt: str, + h5_file_pred: str, + tracker_type: str = "bbox", + **kwargs, +) -> mm.MOTAccumulator: + df_gt = pd.read_hdf(h5_file_gt) + df = pd.read_hdf(h5_file_pred) + if tracker_type == "bbox": + func = reconstruct_all_bboxes + elif tracker_type == "ellipse": + func = trackingutils.reconstruct_all_ellipses + else: + raise ValueError(f"Unrecognized tracker type {tracker_type}.") + + trackers_gt = func(df_gt, **kwargs) + trackers = func(df, **kwargs) + return _compute_mot_metrics( + trackers_gt, + trackers, + tracker_type, + ) + + +def _compute_mot_metrics( + trackers_ground_truth: NDArray, + trackers: NDArray, + tracker_type: str = "bbox", +) -> mm.MOTAccumulator: + if trackers_ground_truth.shape != trackers.shape: + raise ValueError("Dimensions mismatch. There must be as many `trackers_ground_truth` as there are `trackers`.") + + if tracker_type == "bbox": + sl = slice(0, 4) + cost_func = mm.distances.iou_matrix + elif tracker_type == "ellipse": + sl = slice(0, 5) + + def cost_func(ellipses_gt, ellipses_hyp): + cost_matrix = np.zeros((len(ellipses_gt), len(ellipses_hyp))) + gt_el = [trackingutils.Ellipse(*e[:5]) for e in ellipses_gt] + hyp_el = [trackingutils.Ellipse(*e[:5]) for e in ellipses_hyp] + for i, el in enumerate(gt_el): + for j, tracker in enumerate(hyp_el): + cost_matrix[i, j] = 1 - el.calc_similarity_with(tracker) + return cost_matrix + + else: + raise ValueError(f"Unrecognized tracker type {tracker_type}.") + + ids = np.arange(trackers_ground_truth.shape[0]) + acc = mm.MOTAccumulator(auto_id=True) + for i in range(trackers_ground_truth.shape[1]): + trackers_gt = trackers_ground_truth[:, i, sl] + trackers_hyp = trackers[:, i, sl] + empty_gt = np.isnan(trackers_gt).any(axis=1) + empty_hyp = np.isnan(trackers_hyp).any(axis=1) + trackers_gt = trackers_gt[~empty_gt] + trackers_hyp = trackers_hyp[~empty_hyp] + cost = cost_func(trackers_gt, trackers_hyp) + acc.update(ids[~empty_gt], ids[~empty_hyp], cost) + return acc + + +def print_all_metrics(accumulators: list[mm.MOTAccumulator], all_params: list[str] | None = None): + if not all_params: + names = [f"iter{i + 1}" for i in range(len(accumulators))] + else: + s = "_".join("{}" for _ in range(len(all_params[0]))) + names = [s.format(*params.values()) for params in all_params] + mh = mm.metrics.create() + summary = mh.compute_many(accumulators, metrics=mm.metrics.motchallenge_metrics, names=names) + strsummary = mm.io.render_summary(summary, formatters=mh.formatters, namemap=mm.io.motchallenge_metric_names) + print(strsummary) + return summary diff --git a/deeplabcut/benchmark/utils.py b/deeplabcut/benchmark/utils.py index bc1cd64d3c..9c93fcf579 100644 --- a/deeplabcut/benchmark/utils.py +++ b/deeplabcut/benchmark/utils.py @@ -9,16 +9,18 @@ # Licensed under GNU Lesser General Public License v3.0 # -"""Helper functions in this file are not affected by the main repositories -license. They are independent from the remainder of the benchmarking code. +"""Helper functions in this file are not affected by the main repositories license. + +They are independent from the remainder of the benchmarking code. """ + import importlib import os import pkgutil import sys -class RedirectStdStreams(object): +class RedirectStdStreams: """Context manager for redirecting stdout and stderr Reference: https://stackoverflow.com/a/6796752 @@ -49,7 +51,7 @@ def __init__(self): def import_submodules(package, recursive=True): - """Import all submodules of a module, recursively, including subpackages + """Import all submodules of a module, recursively, including subpackages. :param package: package (name or actual module) :type package: str | module @@ -62,7 +64,7 @@ def import_submodules(package, recursive=True): if isinstance(package, str): package = importlib.import_module(package) results = {} - for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): + for _loader, name, is_pkg in pkgutil.walk_packages(package.__path__): full_name = package.__name__ + "." + name results[full_name] = importlib.import_module(full_name) if recursive and is_pkg: diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 5123080055..3ac5dc7de2 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -26,7 +26,7 @@ def main(ctx, verbose): click.echo(main.get_help(ctx)) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("project") @click.argument("experimenter") @@ -49,7 +49,9 @@ def main(ctx, verbose): # help='Directory to create project in. Default is cwd().') @click.pass_context def create_new_project(_, *args, **kwargs): - """Create a new project directory, sub-directories and a basic configuration file. The configuration file is loaded with default values. Change its parameters to your projects need.\n + """Create a new project directory, sub-directories and a basic configuration file. + The configuration file is loaded with default values. Change its parameters to your + projects need.\n. Options \n ---------- \n @@ -60,28 +62,32 @@ def create_new_project(_, *args, **kwargs): videos : list \n \tA list of string containing the full paths of the videos to include in the project.\n working_directory : string, optional \n - \tThe directory where the project will be created. The default is the ``current working directory``; if provided, it must be a string\n + \tThe directory where the project will be created. + The default is the ``current working directory``; if provided, it must be a string\n copy_videos : bool, optional \n - If this is set to True, the symlink of the videos are copied to the project/videos directory. The default is ``True``; if provided it must be either ``True`` or ``False`` \n + If this is set to True, the symlink of the videos are copied to the project/videos directory. + The default is ``True``; if provided it must be either ``True`` or ``False`` \n Example \n -------- \n To create the project in the current working directory \n - python3 dlc.py create_new_project reaching-task Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ + python3 dlc.py create_new_project reaching-task + Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ To create the project in the current working directory but do not want to create the symlinks \n - python3 dlc.py create_new_project reaching-task Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ -c False + python3 dlc.py create_new_project reaching-task + Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ -c False To create the project in another directory \n - python3 dlc.py create_new_project reaching-task Tanmay /data/vies/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi analysis/project -d home/project - + python3 dlc.py create_new_project reaching-task + Tanmay /data/vies/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi analysis/project -d home/project """ from deeplabcut.create_project import new new.create_new_project(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -95,8 +101,7 @@ def create_new_project(_, *args, **kwargs): ) @click.pass_context def add_new_videos(_, *args, **kwargs): - """ - Add new videos to the config file at any stage of the project.\n + """Add new videos to the config file at any stage of the project.\n. Options\n ----------\n @@ -113,14 +118,13 @@ def add_new_videos(_, *args, **kwargs): Examples\n --------\n >>> python3 dlc.py add_new_videos /home/project/reaching-task-Tanmay-2018-08-23/config.yaml /data/videos/mouse5.avi - """ from deeplabcut.create_project import add add.add_new_videos(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("mode") @@ -139,9 +143,10 @@ def add_new_videos(_, *args, **kwargs): ) @click.pass_context def extract_frames(_, *args, **kwargs): - """ - Extracts frames from the videos in the config.yaml file. Only the videos in the config.yaml will be used to select the frames.\n - Use the function ``add_new_videos`` at any stage of the project to add new videos to the config file and extract their frames.\n + """Extracts frames from the videos in the config.yaml file. Only the videos in the + config.yaml will be used to select the frames.\n Use the function ``add_new_videos`` + at any stage of the project to add new videos to the config file and extract their + frames.\n. CONFIG : string \n Full path of the config.yaml file as a string. \n \n \n @@ -153,47 +158,54 @@ def extract_frames(_, *args, **kwargs): for selecting frames automatically with 'kmeans' and do not want to crop the frames \n >>> python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml automatic --algo kmeans \n -------- \n - for selecting frames automatically with 'uniform' and want to crop the frames based on the ``crop`` parameters in config.yaml \n + for selecting frames automatically with 'uniform' and want to + crop the frames based on the ``crop`` parameters in config.yaml \n >>> python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml automatic --crop -------- \n for selecting frames manually, \n >>> deeplabcut.extract_frames /analysis/project/reaching-task/config.yaml manual \n - While selecting the frames manually, you do not need to specify the cropping parameters. Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n + While selecting the frames manually, you do not need to specify the cropping parameters. + Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n -------- \n - """ - from deeplabcut.generate_training_dataset import frameExtraction + from deeplabcut.generate_training_dataset.frame_extraction import extract_frames as _extract_frames - frameExtraction.extract_frames(*args, **kwargs) + _extract_frames(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context def label_frames(_, config): - """Manually label/annotate the extracted frames. Update the list of body parts you want to localize in the config.yaml file first.\n + """Manually label/annotate the extracted frames. Update the list of body parts you + want to localize in the config.yaml file first.\n. + Example\n --------\n python3 dlc.py label_frames /analysis/project/reaching-task/config.yaml """ - from deeplabcut.generate_training_dataset import labelFrames + from deeplabcut.gui.tabs.label_frames import label_frames as _label_frames - labelFrames.label_frames(config) + _label_frames(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context def check_labels(_, config): - """Check if labels were stored correctly by plotting annotations and inspect them visually. If some are wrong, then use the refine_labels to correct the labels.\n""" - from deeplabcut.generate_training_dataset import labelFrames + """Check if labels were stored correctly by plotting annotations and inspect them + visually. + + If some are wrong, then use the refine_labels to correct the labels.\n + """ + from deeplabcut.generate_training_dataset.trainingsetmanipulation import check_labels as _check_labels - labelFrames.check_labels(config) + _check_labels(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -205,7 +217,8 @@ def check_labels(_, config): ) @click.pass_context def create_training_dataset(_, *args, **kwargs): - """Combine frame and label information into a an array. Create training and test sets. Update parameters TrainFraction, iteration in config.yaml + """Combine frame and label information into a an array. Create training and test sets. + Update parameters TrainFraction, iteration in config.yaml Also update parameters for pose_config.yaml as wanted.\n CONFIG: Full path of the config.yaml file in the train directory of a project.\n Example \n @@ -216,12 +229,14 @@ def create_training_dataset(_, *args, **kwargs): To create a training dataset with only 2 shuffles python3 dlc.py create_training_dataset /analysis/project/reaching-task/config.yaml num_shuffles 2 """ - from deeplabcut.generate_training_dataset import labelFrames + from deeplabcut.generate_training_dataset.trainingsetmanipulation import ( + create_training_dataset as _create_training_dataset, + ) - labelFrames.create_training_dataset(*args, **kwargs) + _create_training_dataset(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -246,7 +261,7 @@ def train_network(_, *args, **kwargs): training.train_network(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -256,9 +271,7 @@ def train_network(_, *args, **kwargs): default=[1], help="Shuffle index of the training dataset. Default is set to 1.", ) -@click.option( - "-p", "--plot", "plotting", is_flag=True, help="Make plots. Default is False." -) +@click.option("-p", "--plot", "plotting", is_flag=True, help="Make plots. Default is False.") @click.pass_context def evaluate_network(_, config, **kwargs): """Evaluates a trained Feature detector model.\n @@ -270,12 +283,12 @@ def evaluate_network(_, config, **kwargs): python3 dlc.py evaluate_network /home/project/reaching/config.yaml """ - from deeplabcut.pose_estimation_tensorflow import evaluate + from deeplabcut.pose_estimation_tensorflow.core.evaluate import evaluate_network as _evaluate_network - evaluate.evaluate_network(config, **kwargs) + _evaluate_network(config, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -291,7 +304,7 @@ def evaluate_network(_, config, **kwargs): @click.option( "-vtype", "--video_type", - "videotype", + "video_extensions", default=".avi", help="The extension of video in case the input is a directory", ) @@ -322,7 +335,7 @@ def analyze_videos(_, *args, **kwargs): # predict.predict_video(config, video,**kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -333,17 +346,22 @@ def analyze_videos(_, *args, **kwargs): "--num_shuffles", "shuffle", default=1, - help="The shuffle index of training dataset. The extracted frames will be stored in the labeled-dataset for the corresponding shuffle of training dataset. Default is set to 1", + help="The shuffle index of training dataset. The extracted frames will be stored in the " + "labeled-dataset for the corresponding shuffle of training dataset. Default is set to 1", ) @click.option( "-outlier", "--outlier_algo", "outlieralgorithm", default="fitting", - help="String specifying the algorithm used to detect the outliers. Currently, deeplabcut supports only sarimax (this will be updated). \ - This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model to data and computes confidence interval. \ - Based on the fraction of data points outside the confidence interval and the average distance (compared to delta) \ - the user can identify potential outlier frames. The default is set to ``fitting``. Other choices: `fitting`, `jump`, `uncertain`", + help="String specifying the algorithm used to detect the outliers.\ + Currently, deeplabcut supports only sarimax (this will be updated). \ + This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model \ + to data and computes confidence interval. \ + Based on the fraction of data points outside the confidence interval \ + and the average distance (compared to delta) \ + the user can identify potential outlier frames.\ + The default is set to ``fitting``. Other choices: `fitting`, `jump`, `uncertain`", ) @click.option( "-compare", @@ -352,7 +370,8 @@ def analyze_videos(_, *args, **kwargs): default="all", help="This select the body parts for which the comparisons with the outliers are carried out. Either ``all``, \ then all body parts from config.yaml are used orr a list of strings that are a subset of the full list.\ - E.g. [`hand`,`Joystick`] for the demo Reaching-Mackenzie-2018-08-30/config.yaml to select only these two body parts.", + E.g. [`hand`,`Joystick`]" + " for the demo Reaching-Mackenzie-2018-08-30/config.yaml to select only these two body parts.", ) @click.option( "-e", @@ -360,15 +379,18 @@ def analyze_videos(_, *args, **kwargs): "epsilon", default=20, help="Meaning depends on outlieralgoritm. The default is set to 20 pixels.For outlieralgorithm `fitting`: \ - Float bound according to which frames are picked when the (average) body part estimate deviates from model fit. \ - For outlieralgorithm `jump`: Float bound specifying the distance by which body points jump from one frame to next (Euclidean distance)", + Float bound according to which frames are picked when the (average)\ + body part estimate deviates from model fit. \ + For outlier algorithm `jump`:" + "Float bound specifying the distance by which body points jump from one frame to next (Euclidean distance)", ) @click.option( "-p", "--p_bound", "p_bound", default=0.01, - help="For outlieralgorithm `uncertain` this parameter defines the likelihood below, below which a body part will be flagged as a putative outlier.", + help="For outlieralgorithm `uncertain` this parameter defines the likelihood below, " + "below which a body part will be flagged as a putative outlier.", ) @click.option( "-ard", @@ -383,7 +405,7 @@ def analyze_videos(_, *args, **kwargs): "--ma_degree", "MAdegree", default=1, - help="Int value. For outlieralgorithm `fitting`: MovingAvarage degree of Sarimax model degree.\ + help="Int value. For outlieralgorithm `fitting`: Moving Average degree of Sarimax model degree.\ See https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html", ) @click.option( @@ -398,15 +420,17 @@ def analyze_videos(_, *args, **kwargs): "--extraction_algo", "extractionalgorithm", default="uniform", - help="String specifying the algorithm to use for selecting the frames from the identified outliers. \ - Currently, deeplabcut supports either ``kmeans`` or ``uniform`` based selection (same logic as for extract_frames).\ - The default is set to``uniform``, if provided it must be either ``uniform`` or ``kmeans``.", + help="String specifying the algorithm to use for selecting the frames from the identified outliers.\ + Currently, deeplabcut supports either ``kmeans`` or ``uniform``\ + based selection (same logic as for extract_frames).\ + The default is set to``uniform``,\ + if provided it must be either ``uniform`` or ``kmeans``.", ) @click.pass_context def extract_outlier_frames(_, *args, **kwargs): - """ - Extracts the outlier frames in case, the predictions are not correct for a certain video from the cropped video running from - start to stop as defined in config.yaml. + """Extracts the outlier frames in case, the predictions are not correct for a + certain video from the cropped video running from start to stop as defined in + config.yaml. Another crucial parameter in config.yaml is how many frames to extract 'numframes2extract'. @@ -418,29 +442,32 @@ def extract_outlier_frames(_, *args, **kwargs): Example \n --------\n for extracting the frames with default settings\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + ... /analysis/project/video/reachinvideo1.avi \n --------\n for extracting the frames with kmeans\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + ... /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n --------\n for extracting the frames with kmeans and epsilon = 5 pixels.\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + ... /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n --------\n - """ from deeplabcut.refine_training_dataset import outlier_frames outlier_frames.extract_outlier_frames(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context def refine_labels(_, config): - """Refines the labels of the outlier frames extracted from the analyzed videos.\n Helps in augmenting the training dataset. - Use the function ``analyze_video`` to analyze a video and extracts the outlier frames using the function - ``extract_outlier_frames`` before refining the labels.\n + """Refines the labels of the outlier frames extracted from the analyzed videos.\n + Helps in augmenting the training dataset. Use the function ``analyze_video`` to + analyze a video and extracts the outlier frames using the function + ``extract_outlier_frames`` before refining the labels.\n. Examples \n --------\n @@ -452,7 +479,7 @@ def refine_labels(_, config): outlier_frames.refine_labels(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("videos", nargs=-1) @@ -466,7 +493,7 @@ def refine_labels(_, config): @click.option( "-v", "--video_type", - "videotype", + "video_extensions", default=".avi", help="Checks for the extension of the video in case the input is a directory.\ Only videos with this extension are analyzed. The default is ``.avi``", @@ -493,15 +520,16 @@ def refine_labels(_, config): ) @click.pass_context def create_labeled_video(_, *args, **kwargs): - """ - Labels the bodyparts in a video. Make sure the video is already analyzed by the function 'analyze_video' + """Labels the bodyparts in a video. + + Make sure the video is already analyzed by the function 'analyze_video' """ from deeplabcut.utils import make_labeled_video make_labeled_video.create_labeled_video(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("videos", nargs=-1) @@ -515,7 +543,7 @@ def create_labeled_video(_, *args, **kwargs): @click.option( "-v", "--video_type", - "videotype", + "video_extensions", default=".avi", help="Checks for the extension of the video in case the input is a directory.\ Only videos with this extension are analyzed. The default is ``.avi``", @@ -530,13 +558,13 @@ def create_labeled_video(_, *args, **kwargs): ) @click.pass_context def plot_trajectories(_, *args, **kwargs): - """ - Plots the trajectories of various bodyparts across the video.\n + """Plots the trajectories of various bodyparts across the video.\n. Example\n --------\n for labeling the frames\n - >>> python3 dlc.py plot_trajectories /analysis/project/reaching-task/config.yaml /analysis/project/videos/reachingvideo1.avi \n + >>> python3 dlc.py plot_trajectories /analysis/project/reaching-task/config.yaml + /analysis/project/videos/reachingvideo1.avi \n --------\n """ from deeplabcut.utils import plotting @@ -544,7 +572,7 @@ def plot_trajectories(_, *args, **kwargs): plotting.plot_trajectories(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("cfg-path", nargs=1, type=click.STRING) @click.option( @@ -606,9 +634,9 @@ def plot_trajectories(_, *args, **kwargs): ) @click.pass_context def export_model(_, *args, **kwargs): - """ - Export DLC models for the model zoo or for live inference.\n - Saves the pose configuration, snapshot files, and frozen graph of the model to a directory named exported-models within the project directory + """Export DLC models for the model zoo or for live inference.\n Saves the pose + configuration, snapshot files, and frozen graph of the model to a directory named + exported-models within the project directory. Parameters ----------- @@ -647,4 +675,4 @@ def export_model(_, *args, **kwargs): export_model(*args, **kwargs) -########################################################################################################################### +########################################################################## diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py new file mode 100644 index 0000000000..cf511533bf --- /dev/null +++ b/deeplabcut/compat.py @@ -0,0 +1,2006 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Compatibility file for methods available with either PyTorch or Tensorflow.""" + +from __future__ import annotations + +from collections.abc import Sequence +from pathlib import Path + +import numpy as np +from ruamel.yaml import YAML + +import deeplabcut.core.visualization as visualization +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset.metadata import get_shuffle_engine +from deeplabcut.utils.deprecation import renamed_parameter + +DEFAULT_ENGINE = Engine.PYTORCH + + +def get_project_engine(cfg: dict) -> Engine: + """ + Args: + cfg: the project configuration file + + Returns: + the engine specified for the project, or the default engine if none is specified + """ + if cfg.get("engine") is not None: + return Engine(cfg["engine"]) + + return DEFAULT_ENGINE + + +def get_available_aug_methods(engine: Engine) -> tuple[str, ...]: + """ + Args: + engine: the engine for which augmentation methods should be returned + + Returns: + the augmentations available for the given engine, where the first one is the + default method to use + + Raises: + RuntimeError: if no augmentations methods are defined for the given engine + """ + if engine == Engine.TF: + return "imgaug", "default", "deterministic", "scalecrop", "tensorpack" + elif engine == Engine.PYTORCH: + return ("albumentations",) + + raise RuntimeError(f"Unknown augmentation for engine: {engine}") + + +@renamed_parameter(old="maxiters", new="max_iters", since="3.0.0") +@renamed_parameter(old="saveiters", new="save_iters", since="3.0.0") +@renamed_parameter(old="displayiters", new="display_iters", since="3.0.0") +def train_network( + config: str | Path, + shuffle: int = 1, + trainingsetindex: int = 0, + max_snapshots_to_keep: int | None = None, + display_iters: int | None = None, + save_iters: int | None = None, + max_iters: int | None = None, + epochs: int | None = None, + save_epochs: int | None = None, + allow_growth: bool = True, + gputouse: str | None = None, + autotune: bool = False, + keepdeconvweights: bool = True, + modelprefix: str = "", + superanimal_name: str = "", + superanimal_transfer_learning: bool = False, + engine: Engine | None = None, + device: str | None = None, + snapshot_path: str | Path | None = None, + detector_path: str | Path | None = None, + batch_size: int | None = None, + detector_batch_size: int | None = None, + detector_epochs: int | None = None, + detector_save_epochs: int | None = None, + pose_threshold: float | None = 0.1, + pytorch_cfg_updates: dict | None = None, +): + """Trains the network with the labels in the training dataset. + + Parameters + ---------- + config : string + Full path of the config.yaml file as a string. + + shuffle: int, optional, default=1 + Integer value specifying the shuffle index to select for training. + + trainingsetindex: int, optional, default=0 + Integer specifying which TrainingsetFraction to use. + Note that TrainingFraction is a list in config.yaml. + + max_snapshots_to_keep: int or None + Sets how many snapshots are kept, i.e. states of the trained network. Every + saving iteration many times a snapshot is stored, however only the last + ``max_snapshots_to_keep`` many are kept! If you change this to None, then all + are kept. + See: https://github.com/DeepLabCut/DeepLabCut/issues/8#issuecomment-387404835 + + display_iters: optional, default=None + This variable is actually set in ``pose_config.yaml``. However, you can + overwrite it with this hack. Don't use this regularly, just if you are too lazy + to dig out the ``pose_config.yaml`` file for the corresponding project. If + ``None``, the value from there is used, otherwise it is overwritten! + + save_iters: optional, default=None + Only for the TensorFlow engine (for the PyTorch engine see the ``torch_kwargs``: + you can use ``save_epochs``). + This variable is actually set in ``pose_config.yaml``. However, you can + overwrite it with this hack. Don't use this regularly, just if you are too lazy + to dig out the ``pose_config.yaml`` file for the corresponding project. + If ``None``, the value from there is used, otherwise it is overwritten! + + max_iters: optional, default=None + Only for the TensorFlow engine (for the PyTorch engine see the ``torch_kwargs``: + you can use ``epochs``). + This variable is actually set in ``pose_config.yaml``. However, you can + overwrite it with this hack. Don't use this regularly, just if you are too lazy + to dig out the ``pose_config.yaml`` file for the corresponding project. + If ``None``, the value from there is used, otherwise it is overwritten! + + epochs: optional, default=None + Only for the PyTorch engine (equivalent to the `max_iters` parameter for the + TensorFlow engine). The maximum number of epochs to train the model for. If + None, the value will be read from the `pytorch_config.yaml` file. An epoch is a + single pass through the training dataset, which means your model has seen each + training image exactly once. So if you have 64 training images for your network, + an epoch is 64 iterations with batch size 1 (or 32 iterations with batch size 2, + 16 with batch size 4, etc.). + + save_epochs: optional, default=None + Only for the PyTorch engine (equivalent to the `save_iters` parameter for the + TensorFlow engine). The number of epochs between each snapshot save. If + None, the value will be read from the `pytorch_config.yaml` file. + + allow_growth: bool, optional, default=True. + Only for the TensorFlow engine. + For some smaller GPUs the memory issues happen. If ``True``, the memory + allocator does not pre-allocate the entire specified GPU memory region, instead + starting small and growing as needed. + See issue: https://forum.image.sc/t/how-to-stop-running-out-of-vram/30551/2 + + gputouse: optional, default=None + Only for the TensorFlow engine (for the PyTorch engine see the ``torch_kwargs``: + you can use ``device``). + Natural number indicating the number of your GPU (see number in nvidia-smi). + If you do not have a GPU put None. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + autotune: bool, optional, default=False + Only for the TensorFlow engine. + Property of TensorFlow, somehow faster if ``False`` + (as Eldar found out, see https://github.com/tensorflow/tensorflow/issues/13317). + + keepdeconvweights: bool, optional, default=True + Also restores the weights of the deconvolution layers (and the backbone) when + training from a snapshot. Note that if you change the number of bodyparts, you + need to set this to false for re-training. + + modelprefix: str, optional, default="" + Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. + + superanimal_name: str, optional, default ="" + Only for the TensorFlow engine. For the PyTorch engine, you need to specify + this through the ``weight_init`` when creating the training dataset. + Specified if transfer learning with superanimal is desired + + superanimal_transfer_learning: bool, optional, default = False. + Only for the TensorFlow engine. For the PyTorch engine, you need to specify + this through the ``weight_init`` when creating the training dataset. + If set true, the training is transfer learning (new decoding layer). If set + false, and superanimal_name is True, then the training is fine-tuning (reusing + the decoding layer) + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + device: str, optional, default = None. + Only for the PyTorch engine. The device to run the training on (e.g. "cuda:0") + + snapshot_path: str or Path, optional, default = None. + Only for the PyTorch engine. The path to the pose model snapshot to resume training from. + + detector_path: str or Path, optional, default = None. + Only for the PyTorch engine. The path to the detector model snapshot to resume training from. + + batch_size: int, optional, default = None. + Only for the PyTorch engine. The batch size to use while training. + + detector_batch_size: int, optional, default = None. + Only for the PyTorch engine. The batch size to use while training the detector. + + detector_epochs: int, optional, default = None. + Only for the PyTorch engine. The number of epochs to train the detector for. + + detector_save_epochs: int, optional, default = None. + Only for the PyTorch engine. The number of epochs between each detector snapshot save. + + pose_threshold: float, optional, default = 0.1. + Only for the PyTorch engine. Used for memory-replay. Pseudo-predictions with confidence lower + than this threshold are discarded for memory-replay + + pytorch_cfg_updates: dict, optional, default = None. + A dictionary of updates to the pytorch config. The keys are the dot-separated + paths to the values to update in the config. + For example, to update the gpus to run the training on, you can use: + ``` + pytorch_cfg_updates={"runner.gpus": [0,1,2,3]} + ``` + + Returns + ------- + None + + Examples + -------- + To train the network for first shuffle of the training dataset + + >>> deeplabcut.train_network('/analysis/project/reaching-task/config.yaml') + + To train the network for second shuffle of the training dataset + + >>> deeplabcut.train_network( + '/analysis/project/reaching-task/config.yaml', + shuffle=2, + keepdeconvweights=True, + ) + + To train the network for shuffle created with a PyTorch engine, while overriding the + number of epochs, batch size and other parameters. + + >>> deeplabcut.train_network( + '/analysis/project/reaching-task/config.yaml', + shuffle=1, + batch_size=8, + epochs=100, + save_epochs=10, + display_iters=50, + ) + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import train_network + + if max_snapshots_to_keep is None: + max_snapshots_to_keep = 5 + + return train_network( + str(config), + shuffle=shuffle, + trainingsetindex=trainingsetindex, + max_snapshots_to_keep=max_snapshots_to_keep, + displayiters=display_iters, + saveiters=save_iters, + maxiters=max_iters, + allow_growth=allow_growth, + gputouse=gputouse, + autotune=autotune, + keepdeconvweights=keepdeconvweights, + superanimal_name=superanimal_name, + superanimal_transfer_learning=superanimal_transfer_learning, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import train_network + + return train_network( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + device=device, + snapshot_path=snapshot_path, + detector_path=detector_path, + load_head_weights=keepdeconvweights, + batch_size=batch_size, + epochs=epochs, + save_epochs=save_epochs, + detector_batch_size=detector_batch_size, + detector_epochs=detector_epochs, + detector_save_epochs=detector_save_epochs, + display_iters=display_iters, + max_snapshots_to_keep=max_snapshots_to_keep, + pose_threshold=pose_threshold, + pytorch_cfg_updates=pytorch_cfg_updates, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def return_train_network_path( + config, + shuffle: int = 1, + trainingsetindex: int = 0, + modelprefix: str = "", + engine: Engine | None = None, +) -> tuple[Path, Path, Path]: + """Returns the training and test pose config file names as well as the folder where + the snapshot is. + + Parameters + ---------- + config : string + Full path of the config.yaml file as a string. + + shuffle: int + Integer value specifying the shuffle index to select for training. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. By default the first (note + that TrainingFraction is a list in config.yaml). + + modelprefix: str, optional + Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Returns the triple: trainposeconfigfile, testposeconfigfile, snapshotfolder + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import return_train_network_path + + return return_train_network_path( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis.utils import ( + return_train_network_path, + ) + + return return_train_network_path( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +@renamed_parameter(old="Shuffles", new="shuffles", since="3.0.0") +def evaluate_network( + config: str | Path, + shuffles: Sequence[int] = (1,), + trainingsetindex: int | str = 0, + plotting: bool | str = False, + show_errors: bool = True, + comparison_bodyparts: str | list[str] = "all", + gputouse: str | None = None, + rescale: bool = False, + modelprefix: str = "", + per_keypoint_evaluation: bool = False, + snapshots_to_evaluate: list[str] | None = None, + pcutoff: float | list[float] | dict[str, float] | None = None, + engine: Engine | None = None, + **torch_kwargs, +): + """Evaluates the network. + + Evaluates the network based on the saved models at different stages of the training + network. The evaluation results are stored in the .h5 and .csv file under the + subdirectory 'evaluation_results'. Change the snapshotindex parameter in the config + file to 'all' in order to evaluate all the saved models. + + Parameters + ---------- + config : string + Full path of the config.yaml file. + + shuffles: sequence of int, optional, default=[1] + List of integers specifying the shuffle indices of the training dataset. + + trainingsetindex: int or str, optional, default=0 + Integer specifying which "TrainingsetFraction" to use. + Note that "TrainingFraction" is a list in config.yaml. This variable can also + be set to "all". + + plotting: bool or str, optional, default=False + Plots the predictions on the train and test images. + If provided it must be either ``True``, ``False``, ``"bodypart"``, or + ``"individual"``. Setting to ``True`` defaults as ``"bodypart"`` for + multi-animal projects. + If a detector is used, the predicted bounding boxes will also be plotted. + + show_errors: bool, optional, default=True + Display train and test errors. + + comparison_bodyparts: str or list, optional, default="all" + The average error will be computed for those body parts only. + The provided list has to be a subset of the defined body parts. + + gputouse: int or None, optional, default=None + Indicates the GPU to use (see number in ``nvidia-smi``). If you do not have a + GPU put `None``. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + rescale: bool, optional, default=False + Evaluate the model at the ``'global_scale'`` variable (as set in the + ``pose_config.yaml`` file for a particular project). I.e. every image will be + resized according to that scale and prediction will be compared to the resized + ground truth. The error will be reported in pixels at rescaled to the + *original* size. I.e. For a [200,200] pixel image evaluated at + ``global_scale=.5``, the predictions are calculated on [100,100] pixel images, + compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! + + modelprefix: str, optional, default="" + Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. + + per_keypoint_evaluation: bool, default=False + Compute the train and test RMSE for each keypoint, and save the results to + a {model_name}-keypoint-results.csv in the evaluation-results folder + + snapshots_to_evaluate: List[str], optional, default=None + List of snapshot names to evaluate (e.g. ["snapshot-5000", "snapshot-7500"]). + + pcutoff: float | list[float] | dict[str, float] | None, default=None + Only for the PyTorch engine. For the TensorFlow engine, please set the pcutoff + in the `config.yaml` file. + The cutoff to use for computing evaluation metrics. When `None` (default), the + cutoff will be loaded from the project config. If a list is provided, there + should be one value for each bodypart and one value for each unique bodypart + (if there are any). If a dict is provided, the keys should be bodyparts + mapping to pcutoff values for each bodypart. Bodyparts that are not defined + in the dict will have pcutoff set to 0.6. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + torch_kwargs: + You can add any keyword arguments for the deeplabcut.pose_estimation_pytorch + evaluate_network function here. These arguments are passed to the downstream + function. Available parameters are `snapshotindex`, which overrides the + `snapshotindex` parameter in the project configuration file. For top-down models + the `detector_snapshot_index` parameter can override the index of the detector + to use for evaluation in the project configuration file. + + Returns + ------- + None + + Examples + -------- + If you do not want to plot and evaluate with shuffle set to 1. + + >>> deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', shuffles=[1], + ) + + If you want to plot and evaluate with shuffle set to 0 and 1. + + >>> deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', + shuffles=[0, 1], + plotting=True, + ) + + If you want to plot assemblies for a maDLC project + + >>> deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', + shuffles=[1], + plotting="individual", + ) + + If you have a PyTorch model for which you want to set a different p-cutoff for + "left_ear" and "right_ear" bodyparts, and keep the one set in the project config + for other bodyparts: + + >>> deeplabcut.evaluate_network( + >>> "/analysis/project/reaching-task/config.yaml", + >>> shuffles=[0, 1], + >>> pcutoff={"left_ear": 0.8, "right_ear": 0.8}, + >>> ) + + Note: This defaults to standard plotting for single-animal projects. + """ + if engine is None: + cfg = _load_config(config) + engines = set() + for shuffle in shuffles: + engines.add( + get_shuffle_engine( + cfg, + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + ) + if len(engines) == 0: + raise ValueError(f"You must pass at least one shuffle to evaluate (had {list(shuffles)})") + elif len(engines) > 1: + raise ValueError(f"All shuffles must have the same engine (found {list(engines)})") + engine = engines.pop() + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import evaluate_network + + return evaluate_network( + str(config), + Shuffles=shuffles, + trainingsetindex=trainingsetindex, + plotting=plotting, + show_errors=show_errors, + comparisonbodyparts=comparison_bodyparts, + gputouse=gputouse, + rescale=rescale, + modelprefix=modelprefix, + per_keypoint_evaluation=per_keypoint_evaluation, + snapshots_to_evaluate=snapshots_to_evaluate, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import evaluate_network + + _update_device(gputouse, torch_kwargs) + return evaluate_network( + config, + shuffles=shuffles, + trainingsetindex=trainingsetindex, + plotting=plotting, + show_errors=show_errors, + comparison_bodyparts=comparison_bodyparts, + snapshots_to_evaluate=snapshots_to_evaluate, + per_keypoint_evaluation=per_keypoint_evaluation, + modelprefix=modelprefix, + pcutoff=pcutoff, + **torch_kwargs, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +@renamed_parameter(old="Snapindex", new="snapshotindex", since="3.0.0") +def return_evaluate_network_data( + config: str, + shuffle: int = 0, + trainingsetindex: int = 0, + comparison_bodyparts: str | list[str] = "all", + snapshotindex: str | int | None = None, + rescale: bool = False, + fulldata: bool = False, + show_errors: bool = True, + modelprefix: str = "", + returnjustfns: bool = True, + engine: Engine | None = None, +): + """Returns the results for (previously evaluated) network. + deeplabcut.evaluate_network(..) Returns list of (per model): [trainingsiterations,tr + ainfraction,shuffle,trainerror,testerror,pcutoff,trainerrorpcutoff,testerrorpcutoff, + Snapshots[snapshotindex],scale,net_type] + + This function is only implemented for tensorflow models/shuffles, and will throw + an error if called with a PyTorch shuffle. + + If fulldata=True, also returns (the complete annotation and prediction array) + Returns list of: + (DataMachine, Data, data, trainIndices, + testIndices, trainFraction, DLCscorer, + comparison_bodyparts, cfg, Snapshots[snapshotindex] + ) + ---------- + config : string + Full path of the config.yaml file as a string. + + shuffle: integer + integers specifying shuffle index of the training dataset. + The default is 0. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". + + comparison_bodyparts: list of bodyparts, Default is "all". + The average error will be computed for those body parts only + (Has to be a subset of the body parts). + + rescale: bool, default False + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to + that scale and prediction will be compared to the resized ground truth. + The error will be reported in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Examples + -------- + If you do not want to plot + >>> deeplabcut._evaluate_network_data('/analysis/project/reaching-task/config.yaml', shuffle=[1]) + -------- + If you want to plot + >>> deeplabcut.evaluate_network('/analysis/project/reaching-task/config.yaml',shuffle=[1],plotting=True) + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import return_evaluate_network_data + + return return_evaluate_network_data( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparisonbodyparts=comparison_bodyparts, + Snapindex=snapshotindex, + rescale=rescale, + fulldata=fulldata, + show_errors=show_errors, + modelprefix=modelprefix, + returnjustfns=returnjustfns, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="batchsize", new="batch_size", since="3.0.0") +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def analyze_videos( + config: str, + videos: list[str], + video_extensions: str | Sequence[str] | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + gputouse: str | None = None, + save_as_csv: bool = False, + in_random_order: bool = True, + destfolder: str | None = None, + batch_size: int | None = None, + cropping: list[int] | None = None, + TFGPUinference: bool = True, + dynamic: tuple[bool, float, int] = (False, 0.5, 10), + modelprefix: str = "", + robust_nframes: bool = False, + allow_growth: bool = False, + use_shelve: bool = False, + auto_track: bool = True, + n_tracks: int | None = None, + animal_names: list[str] | None = None, + calibrate: bool = False, + identity_only: bool = False, + use_openvino: str | None = None, + engine: Engine | None = None, + **torch_kwargs, +): + """Makes prediction based on a trained network. + + The index of the trained network is specified by parameters in the config file + (in particular the variable 'snapshotindex'). + + The labels are stored as MultiIndex Pandas Array, which contains the name of + the network, body part name, (x, y) label position in pixels, and the + likelihood for each frame per body part. These arrays are stored in an + efficient Hierarchical Data Format (HDF) in the same directory where the video + is stored. However, if the flag save_as_csv is set to True, the data can also + be exported in comma-separated values format (.csv), which in turn can be + imported in many programs, such as MATLAB, R, Prism, etc. + + Parameters + ---------- + config: str + Full path of the config.yaml file. + + videos: list[str] + A list of strings containing the full paths to videos for analysis or a path to + the directory, where all the videos with same extension are stored. + + video_extensions : str | Sequence[str] | None, optional, default=None + Controls how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). + + shuffle: int, optional, default=1 + An integer specifying the shuffle index of the training dataset used for + training the network. + + trainingsetindex: int, optional, default=0 + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + + gputouse: int or None, optional, default=None + Only for the TensorFlow engine (for the PyTorch engine see the ``torch_kwargs``: + you can use ``device``). + Indicates the GPU to use (see number in ``nvidia-smi``). If you do not have a + GPU put ``None``. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + save_as_csv: bool, optional, default=False + Saves the predictions in a .csv file. + + in_random_order: bool, optional (default=True) + Whether or not to analyze videos in a random order. + This is only relevant when specifying a video directory in `videos`. + + destfolder: string or None, optional, default=None + Specifies the destination folder for analysis data. If ``None``, the path of + the video is used. Note that for subsequent analysis this folder also needs to + be passed. + + batch_size: int or None, optional, default=None + Currently not supported by the PyTorch engine. + Change batch size for inference; if given overwrites value in ``pose_cfg.yaml``. + + cropping: list or None, optional, default=None + List of cropping coordinates as [x1, x2, y1, y2]. + Note that the same cropping parameters will then be used for all videos. + If different video crops are desired, run ``analyze_videos`` on individual + videos with the corresponding cropping coordinates. + + TFGPUinference: bool, optional, default=True + Only for the TensorFlow engine. + Perform inference on GPU with TensorFlow code. Introduced in "Pretraining + boosts out-of-domain robustness for pose estimation" by Alexander Mathis, + Mert Yüksekgönül, Byron Rogers, Matthias Bethge, Mackenzie W. Mathis. + Source: https://arxiv.org/abs/1909.11229 + + dynamic: tuple(bool, float, int) triple containing (state, det_threshold, margin) + If the state is true, then dynamic cropping will be performed. That means that + if an object is detected (i.e. any body part > detectiontreshold), then object + boundaries are computed according to the smallest/largest x position and + smallest/largest y position of all body parts. This window is expanded by the + margin and from then on only the posture within this crop is analyzed (until the + object is lost, i.e. >> deeplabcut.analyze_videos( + 'C:\\myproject\\reaching-task\\config.yaml', + ['C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi'], + ) + + Analyzing a single video on Linux/MacOS + + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + ['/analysis/project/videos/reachingvideo1.avi'], + ) + + Analyze all videos of type ``avi`` in a folder + + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + ['/analysis/project/videos'], + video_extensions='.avi', + ) + + Analyze multiple videos + + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + [ + '/analysis/project/videos/reachingvideo1.avi', + '/analysis/project/videos/reachingvideo2.avi', + ], + ) + + Analyze multiple videos with ``shuffle=2`` + + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + [ + '/analysis/project/videos/reachingvideo1.avi', + '/analysis/project/videos/reachingvideo2.avi', + ], + shuffle=2, + ) + + Analyze multiple videos with ``shuffle=2``, save results as an additional csv file + + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + [ + '/analysis/project/videos/reachingvideo1.avi', + '/analysis/project/videos/reachingvideo2.avi', + ], + shuffle=2, + save_as_csv=True, + ) + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import analyze_videos + + kwargs = {} + if use_openvino is not None: # otherwise default comes from tensorflow API + kwargs["use_openvino"] = use_openvino + + return analyze_videos( + config, + videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + save_as_csv=save_as_csv, + in_random_order=in_random_order, + destfolder=destfolder, + batchsize=batch_size, + cropping=cropping, + TFGPUinference=TFGPUinference, + dynamic=dynamic, + modelprefix=modelprefix, + robust_nframes=robust_nframes, + allow_growth=allow_growth, + use_shelve=use_shelve, + auto_track=auto_track, + n_tracks=n_tracks, + animal_names=animal_names, + calibrate=calibrate, + identity_only=identity_only, + **kwargs, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import analyze_videos + + _update_device(gputouse, torch_kwargs) + + if batch_size is not None: + if "batch_size" in torch_kwargs: + print( + f"You called analyze_videos with parameters ``batch_size={batch_size}" + f"`` and batch_size={torch_kwargs['batch_size']}. Only one is " + f"needed/used. Using batch size {torch_kwargs['batch_size']}" + ) + else: + torch_kwargs["batch_size"] = batch_size + + return analyze_videos( + config, + videos=videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + save_as_csv=save_as_csv, + in_random_order=in_random_order, + destfolder=destfolder, + dynamic=dynamic, + modelprefix=modelprefix, + use_shelve=use_shelve, + robust_nframes=robust_nframes, + auto_track=auto_track, + n_tracks=n_tracks, + animal_names=animal_names, + calibrate=calibrate, + identity_only=identity_only, + overwrite=False, + cropping=cropping, + **torch_kwargs, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="batchsize", new="batch_size", since="3.0.0") +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def create_tracking_dataset( + config: str, + videos: list[str], + track_method: str, + video_extensions: str | Sequence[str] | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + gputouse: int | None = None, + destfolder: str | None = None, + batch_size: int | None = None, + cropping: list[int] | None = None, + TFGPUinference: bool = True, + modelprefix: str = "", + robust_nframes: bool = False, + n_triplets: int = 1000, + engine: Engine | None = None, +) -> str: + """Creates a tracking dataset to train a ReID tracklet stitcher. + + Parameters + ---------- + config: str + Full path of the config.yaml file. + + videos: list[str] + A list of strings containing the full paths to videos from which to create a + tracking dataset, or a path to the directory where all the videos with same + extension are stored. + + track_method: str + Specifies the tracker used to generate the pose estimation data. Must be either + 'box', 'skeleton', or 'ellipse'. + + video_extensions : str | Sequence[str] | None, optional, default=None + Controls how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). + + shuffle: int, optional, default=1 + An integer specifying the shuffle index of the training dataset used for + training the network. + + trainingsetindex: int, optional, default=0 + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + + gputouse: int or None, optional, default=None + Only for the TensorFlow engine (for the PyTorch engine use ``device``). + Indicates the GPU to use (see number in ``nvidia-smi``). If you do not have a + GPU put ``None``. See: + https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + TFGPUinference: bool, optional, default=True + Only for the TensorFlow engine. + Perform inference on GPU with TensorFlow code. Introduced in "Pretraining + boosts out-of-domain robustness for pose estimation" by Alexander Mathis, + Mert Yüksekgönül, Byron Rogers, Matthias Bethge, Mackenzie W. Mathis. + Source: https://arxiv.org/abs/1909.11229 + + destfolder: + Specifies the destination folder for analysis data. If ``None``, the path of + the video is used. Note that for subsequent analysis this folder also needs to + be passed. + + modelprefix: str, optional, default="" + Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. + + robust_nframes: bool, optional, default=False + Evaluate a video's number of frames in a robust manner. + This option is slower (as the whole video is read frame-by-frame), + but does not rely on metadata, hence its robustness against file corruption. + + n_triplets: int, default=1000 + The number of triplets to extract for the dataset. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Returns + ------- + DLCScorer: str + the scorer used to analyze the videos + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import create_tracking_dataset + + return create_tracking_dataset( + config, + videos, + track_method, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + destfolder=destfolder, + batchsize=batch_size, + cropping=cropping, + TFGPUinference=TFGPUinference, + modelprefix=modelprefix, + robust_nframes=robust_nframes, + n_triplets=n_triplets, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import create_tracking_dataset + + return create_tracking_dataset( + config, + videos, + track_method, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + destfolder=destfolder, + batch_size=batch_size, + cropping=cropping, + modelprefix=modelprefix, + robust_nframes=robust_nframes, + n_triplets=n_triplets, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def analyze_images( + config: str | Path, + images: str | Path | list[str] | list[Path], + frame_type: str | None = None, + destfolder: str | Path | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + max_individuals: int | None = None, + device: str | None = None, + snapshot_index: int | None = None, + detector_snapshot_index: int | None = None, + save_as_csv: bool = False, + modelprefix: str = "", + plotting: bool | str = False, + pcutoff: float | None = None, + bbox_pcutoff: float | None = None, + plot_skeleton: bool = False, + **torch_kwargs, +) -> dict[str, dict[str, np.ndarray | np.ndarray]]: + """Analyzes images with a DeepLabCut model and stores the output in an H5 file. + + This method is only implemented for PyTorch models. + + The labels are stored as Pandas DataFrame, which contains the name of the network, + body part name, (x, y) label position in pixels, and the likelihood for each frame + per body part. + + Parameters + ---------- + config : str, Path + Full path of the project's config.yaml file. + + images: str, Path, list[str], list[Path] + The image(s) to run inference on. Can be the path to an image, the path + to a directory containing images, or a list of image paths or directories + containing images. + + frame_type: string, optional + Filters the images to analyze to only the ones with the given suffix (e.g. + setting `frame_type`=".png" will only analyze ".png" images). The default + behavior analyzes all ".jpg", ".jpeg" and ".png" images. + + destfolder: str, Path, optional + The directory where the predictions will be stored. If None, the predictions + will be stored in the same directory as the first image given in the `images` + argument (if it's a directory, that directory will be used; if it's an image, + the directory containing the image will be used). + + shuffle: int, optional + An integer specifying the shuffle with which to run image analysis. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. By default, the first one + is used (note that TrainingFraction is a list in config.yaml). + + max_individuals: int, optional + The maximum number of individuals to detect in each image. Set to the number of + individuals in the project if None. + + device: str, optional + The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. For + more information, see https://pytorch.org/docs/stable/notes/cuda.html + + snapshot_index: int, optional + Index (starting at 0) of the snapshot to use for image analysis. To evaluate the + last one, use -1. Default uses the value set in the project config. + + detector_snapshot_index: int, optional + Only for Top-Down PyTorch models. If defined, uses the detector with the given + index for pose estimation. To evaluate the last one, use -1. Default uses the + value set in the project config. + + save_as_csv: bool, optional + Saves the predictions in a .csv file. The default is ``False``; if provided it + must be either ``True`` or ``False``. + + modelprefix: str, optional + Directory containing the deeplabcut models to use when running image analysis. + By default, the models are assumed to exist in the project folder. + + plotting: bool, str, default=False + Plots the predictions made by the model on the analyzed images. Results will be + stored in a folder named `LabeledImages_{scorer}`, where scorer is the name + of the model used to analyze the images. This folder will be in the same + directory as the file containing the predictions (either the given `destfolder`, + or the folder containing the first image to analyze). + + If provided it must be either ``True``, ``False``, ``"bodypart"``, or + ``"individual"``. Setting to ``True`` defaults as ``"bodypart"`` for + multi-animal projects. If a detector is used, the predicted bounding boxes + will also be plotted. + + pcutoff: float, optional, default=None + The cutoff score when plotting pose predictions. Must be None or in + (0, 1). If None, the pcutoff is read from the project configuration file. + + bbox_pcutoff: float, optional, default=None + The cutoff score when plotting bounding box predictions. Must be + None or in (0, 1). If None, it is read from the project configuration file. + + plot_skeleton: bool, default=False + If a skeleton is defined in the project's config.yaml, whether + to plot the skeleton connecting the predicted bodyparts on the images. + + torch_kwargs: + Any extra parameters to pass to the PyTorch API, such as ``ctd_conditions`` + + Returns + ------- + A dictionary mapping image paths (as strings) to model predictions. + + Examples + -------- + If you want to analyze all frames in /analysis/project/my_images + >>> import deeplabcut + >>> deeplabcut.analyze_images( + >>> "/analysis/project/reaching-task/config.yaml", + >>> "/analysis/project/my_images", + >>> ) + >>> + + If you want to analyze two specific images with your shuffle 3 model: + >>> import deeplabcut + >>> deeplabcut.analyze_images( + >>> "/analysis/project/reaching-task/config.yaml", + >>> images=["image_001.png", "img_002.jpg"], + >>> shuffle=3, + >>> ) + >>> + + If you want to analyze frames in a folder, save them and plot predictions: + >>> import deeplabcut + >>> deeplabcut.analyze_images( + >>> "/analysis/project/reaching-task/config.yaml", + >>> "/analysis/project/my_images", + >>> shuffle=3, + >>> destfolder="/analysis/project/my_images_analyzed", + >>> plotting=True, + >>> ) + >>> + -------- + """ + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import analyze_images + + return analyze_images( + config=config, + images=images, + frame_type=frame_type, + output_dir=destfolder, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshot_index=snapshot_index, + detector_snapshot_index=detector_snapshot_index, + modelprefix=modelprefix, + device=device, + save_as_csv=save_as_csv, + max_individuals=max_individuals, + plotting=plotting, + pcutoff=pcutoff, + bbox_pcutoff=bbox_pcutoff, + plot_skeleton=plot_skeleton, + **torch_kwargs, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def analyze_time_lapse_frames( + config: str, + directory: str, + frametype: str = ".png", + shuffle: int = 1, + trainingsetindex: int = 0, + gputouse: int | None = None, + device: str | None = None, + save_as_csv: bool = False, + modelprefix: str = "", + engine: Engine | None = None, +): + """Analyzed all images (of type = frametype) in a folder and stores the output in + one file. + + You can crop the frames (before analysis), by changing 'cropping'=True and setting + 'x1','x2','y1','y2' in the config file. + + Output: The labels are stored as MultiIndex Pandas Array, which contains the name + of the network, body part name, (x, y) label position in pixels, and the likelihood + for each frame per body part. These arrays are stored in an efficient Hierarchical + Data Format (HDF) in the same directory, where the video is stored. However, if the + flag save_as_csv is set to True, the data can also be exported in comma-separated + values format (.csv), which in turn can be imported in many programs, such as + MATLAB, R, Prism, etc. + + Parameters + ---------- + config : string + Full path of the config.yaml file as a string. + + directory: string + Full path to directory containing the frames that shall be analyzed + + frametype: string, optional + Checks for the file extension of the frames. Only images with this extension are + analyzed. The default is ``.png`` + + shuffle: int, optional + An integer specifying the shuffle index of the training dataset used for + training the network. The default is 1. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. By default the first (note + that TrainingFraction is a list in config.yaml). + + gputouse: int, optional. + Only for TensorFlow models. For PyTorch models, please use `device`. Natural + number indicating the number of your GPU (see number in nvidia-smi). If you do + not have a GPU put None. See: + https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + device: str, optional + The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. For + more information, see https://pytorch.org/docs/stable/notes/cuda.html + + save_as_csv: bool, optional + Saves the predictions in a .csv file. The default is ``False``; if provided if + must be either ``True`` or ``False`` + + Examples + -------- + If you want to analyze all frames in /analysis/project/timelapseexperiment1 + >>> import deeplabcut + >>> deeplabcut.analyze_time_lapse_frames( + >>> '/analysis/project/reaching-task/config.yaml', + >>> '/analysis/project/timelapseexperiment1' + >>> ) + + -------- + + Note: for test purposes one can extract all frames from a video with ffmeg, e.g. + >>> ffmpeg -i testvideo.avi "thumb%04d.png" + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import analyze_time_lapse_frames + + return analyze_time_lapse_frames( + config, + directory, + frametype=frametype, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + save_as_csv=save_as_csv, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import analyze_images + + return analyze_images( + config=config, + images=directory, + output_dir=directory, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + device=_gpu_to_use_to_device(gputouse, device), + save_as_csv=save_as_csv, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def convert_detections2tracklets( + config: str, + videos: list[str], + video_extensions: str | Sequence[str] | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + overwrite: bool = False, + destfolder: str | None = None, + ignore_bodyparts: list[str] | None = None, + inferencecfg: dict | None = None, + modelprefix: str = "", + greedy: bool = False, + calibrate: bool = False, + window_size: int = 0, + identity_only: int = False, + track_method: str = "", + engine: Engine | None = None, +): + """This should be called at the end of deeplabcut.analyze_videos for multianimal + projects! + + Parameters + ---------- + config : string + Full path of the config.yaml file as a string. + + videos : list + A list of strings containing the full paths to videos for analysis or a path to the directory, + where all the videos with same extension are stored. + + video_extensions : str | Sequence[str] | None, optional, default=None + Controls how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). + + shuffle: int, optional + An integer specifying the shuffle index of the training dataset used for training the network. T + he default is 1. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + + overwrite: bool, optional. + Overwrite tracks file i.e. recompute tracks from full detections and overwrite. + + destfolder: string, optional + Specifies the destination folder for analysis data (default is the path of the video). + Note that for subsequent analysis this + folder also needs to be passed. + + ignore_bodyparts: optional + List of body part names that should be ignored during tracking (advanced). + By default, all the body parts are used. + + inferencecfg: Default is None. + Configuration file for inference (assembly of individuals). Ideally + should be obtained from cross validation (during evaluation). By default + the parameters are loaded from inference_cfg.yaml, but these get_level_values + can be overwritten. + + calibrate: bool, optional (default=False) + If True, use training data to calibrate the animal assembly procedure. + This improves its robustness to wrong body part links, + but requires very little missing data. + + window_size: int, optional (default=0) + Recurrent connections in the past `window_size` frames are + prioritized during assembly. By default, no temporal coherence cost + is added, and assembly is driven mainly by part affinity costs. + + identity_only: bool, optional (default=False) + If True and animal identity was learned by the model, + assembly and tracking rely exclusively on identity prediction. + + track_method: string, optional + Specifies the tracker used to generate the pose estimation data. + For multiple animals, must be either 'box', 'skeleton', or 'ellipse' + and will be taken from the config.yaml file if none is given. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Examples + -------- + If you want to convert detections to tracklets: + >>> import deeplabcut + >>> deeplabcut.convert_detections2tracklets( + >>> "/analysis/project/reaching-task/config.yaml", + >>> ["/analysis/project/video1.mp4"], + >>> video_extensions='.mp4', + >>> ) + + If you want to convert detections to tracklets based on box_tracker: + >>> import deeplabcut + >>> deeplabcut.convert_detections2tracklets( + >>> "/analysis/project/reaching-task/config.yaml", + >>> ["/analysis/project/video1.mp4"], + >>> video_extensions=".mp4", + >>> track_method="box", + >>> ) + + -------- + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import convert_detections2tracklets + + return convert_detections2tracklets( + config, + videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + overwrite=overwrite, + destfolder=destfolder, + ignore_bodyparts=ignore_bodyparts, + inferencecfg=inferencecfg, + modelprefix=modelprefix, + greedy=greedy, + calibrate=calibrate, + window_size=window_size, + identity_only=identity_only, + track_method=track_method, + ) + + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import convert_detections2tracklets + + if greedy or calibrate or window_size: + raise NotImplementedError( + f"The 'greedy', 'calibrate' and 'window_size' option are not yet implemented with {engine}" + ) + + return convert_detections2tracklets( + config, + videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + overwrite=overwrite, + destfolder=destfolder, + ignore_bodyparts=ignore_bodyparts, + inferencecfg=inferencecfg, + modelprefix=modelprefix, + identity_only=identity_only, + track_method=track_method, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def extract_maps( + config, + shuffle: int = 0, + trainingsetindex: int = 0, + gputouse: int | None = None, + device: str | None = None, + rescale: bool = False, + Indices: list[int] | None = None, + modelprefix: str = "", + engine: Engine | None = None, +): + """Extracts the scoremap, locref, partaffinityfields (if available). + + Returns a dictionary indexed by: trainingsetfraction, snapshotindex, and imageindex + for those keys, each item contains: (image, scmap, locref, paf, bpt_names, + partaffinity_graph, imagename, True/False if this image was in trainingset). + + ---------- + config : string + Full path of the config.yaml file as a string. + + shuffle: integer + integers specifying shuffle index of the training dataset. The default is 0. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. By default the first (note + that TrainingFraction is a list in config.yaml). This variable can also be set + to "all". + + gputouse: int or None, optional, default=None + For the TensorFlow engine (for the PyTorch engine see ``device``). Specifies + the GPU to use (see number in ``nvidia-smi``). If you do not have a GPU put + ``None``. See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + device: str or None, optional, default=None + The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. See + https://pytorch.org/docs/stable/notes/cuda.html for more information. + + rescale: bool, default False + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale and prediction + will be compared to the resized ground truth. + The error will be reported in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Examples + -------- + If you want to extract the data for image 0 and 103 (of the training set) for model trained with shuffle 0. + >>> deeplabcut.extract_maps(configfile,0,Indices=[0,103]) + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import extract_maps + + return extract_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + rescale=rescale, + Indices=Indices, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import extract_maps + + return extract_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + device=_gpu_to_use_to_device(gputouse, device), + rescale=rescale, + indices=Indices, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def visualize_scoremaps(image: np.ndarray, scmap: np.ndarray): + """Plots scoremaps as an image overlay. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + + Returns: + The figure and axis on which the image scoremap was plot. + """ + return visualization.visualize_scoremaps(image, scmap) + + +def visualize_locrefs( + image: np.ndarray, + scmap: np.ndarray, + locref_x: np.ndarray, + locref_y: np.ndarray, + step: int = 5, + zoom_width: int = 0, +): + """Plots a scoremap and the corresponding location refinement field on an image. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + locref_x: The x-coordinate of the location refinement field, of shape (h, w) + locref_y: The y-coordinate of the location refinement field, of shape (h, w) + step: The step with which to plot the location refinement field. + zoom_width: The zoom width with which to plot the scoremaps. + + Returns: + The figure and axis on which the image scoremap and locref field were plot. + """ + return visualization.visualize_locrefs(image, scmap, locref_x, locref_y, step=step, zoom_width=zoom_width) + + +def visualize_paf( + image: np.ndarray, + paf: np.ndarray, + step: int = 5, + colors: list | None = None, +): + """Plots the PAF on top of the image. + + Args: + image: Shape (height, width, channels). The image on which the model was run. + paf: Shape (height, width, 2 * len(paf_graph)). The PAF output by the model. + step: The step with which to plot the scoremaps. + colors: The colormap to use. + + Returns: + The figure and axis on which the image PAF was plot. + """ + return visualization.visualize_paf(image, paf, step=step, colors=colors) + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +def extract_save_all_maps( + config, + shuffle: int = 1, + trainingsetindex: int = 0, + comparison_bodyparts: str | list[str] = "all", + extract_paf: bool = True, + all_paf_in_one: bool = True, + gputouse: int | None = None, + device: str | None = None, + rescale: bool = False, + Indices: list[int] | None = None, + modelprefix: str = "", + dest_folder: str = None, + snapshot_index: int | str | None = None, + detector_snapshot_index: int | str | None = None, + engine: Engine | None = None, +): + """ + Extracts the scoremap, location refinement field and part affinity field prediction of the model. The maps + will be rescaled to the size of the input image and stored in the corresponding model folder in /evaluation-results. + + ---------- + config : string + Full path of the config.yaml file as a string. + + shuffle: integer + integers specifying shuffle index of the training dataset. The default is 1. + + trainingsetindex: int, optional + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". + + comparison_bodyparts: list of bodyparts, Default is "all". + The average error will be computed for those body parts only (Has to be a subset of the body parts). + + extract_paf : bool + Extract part affinity fields by default. + Note that turning it off will make the function much faster. + + all_paf_in_one : bool + By default, all part affinity fields are displayed on a single frame. + If false, individual fields are shown on separate frames. + + gputouse: int or None, optional, default=None + For the TensorFlow engine (for the PyTorch engine see ``device``). Specifies + the GPU to use (see number in ``nvidia-smi``). If you do not have a GPU put + ``None``. See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + + device: str or None, optional, default=None + The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. See + https://pytorch.org/docs/stable/notes/cuda.html for more information. + + Indices: default None + For which images shall the scmap/locref and paf be computed? Give a list of images + + nplots_per_row: int, optional (default=None) + Number of plots per row in grid plots. By default, calculated to approximate a squared grid of plots + + snapshot_index: Only for PyTorch models. Index (starting at 0) of the snapshot we + want to extract maps with. To evaluate the last one, use -1. To extract maps + for all snapshots, use "all". Default uses the value set in the project config. + + detector_snapshot_index: Only for TD PyTorch models. If defined, uses the detector + with the given index for pose estimation. To extract maps for all detector + snapshots, use "all". Default uses the value set in the project config. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Examples + -------- + Calculated maps for images 0, 1 and 33. + >>> deeplabcut.extract_save_all_maps('/analysis/project/reaching-task/config.yaml', shuffle=1,Indices=[0,1,33]) + + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import extract_save_all_maps + + return extract_save_all_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparisonbodyparts=comparison_bodyparts, + extract_paf=extract_paf, + all_paf_in_one=all_paf_in_one, + gputouse=gputouse, + rescale=rescale, + Indices=Indices, + modelprefix=modelprefix, + dest_folder=dest_folder, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import extract_save_all_maps + + return extract_save_all_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparison_bodyparts=comparison_bodyparts, + extract_paf=extract_paf, + all_paf_in_one=all_paf_in_one, + device=_gpu_to_use_to_device(gputouse, device), + rescale=rescale, + indices=Indices, + modelprefix=modelprefix, + snapshot_index=snapshot_index, + detector_snapshot_index=detector_snapshot_index, + dest_folder=dest_folder, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def export_model( + cfg_path: str, + shuffle: int = 1, + trainingsetindex: int = 0, + snapshotindex: int | None = None, + iteration: int = None, + TFGPUinference: bool = True, + overwrite: bool = False, + make_tar: bool = True, + wipepaths: bool = False, + without_detector: bool = False, + modelprefix: str = "", + engine: Engine | None = None, +) -> None: + """Export DeepLabCut models for the model zoo or for live inference. + + Saves the pose configuration, snapshot files, and frozen TF graph of the model to + directory named exported-models within the project directory (and an + `exported-models-pytorch` directory for PyTorch models). + + Parameters + ----------- + + cfg_path : string + path to the DLC Project config.yaml file + + shuffle : int, optional + the shuffle of the model to export. default = 1 + + trainingsetindex : int, optional + the index of the training fraction for the model you wish to export. default = 1 + + snapshotindex : int, optional + the snapshot index for the weights you wish to export. If None, + uses the snapshotindex as defined in 'config.yaml'. Default = None + + iteration : int, optional + The model iteration (active learning loop) you wish to export. If None, + the iteration listed in the config file is used. + + TFGPUinference : bool, optional + use the tensorflow inference model? Default = True + For inference using DeepLabCut-live, it is recommended to set TFGPIinference=False + + overwrite : bool, optional + if the model you wish to export has already been exported, whether to overwrite. default = False + + make_tar : bool, optional + Do you want to compress the exported directory to a tar file? Default = True + This is necessary to export to the model zoo, but not for live inference. + + wipepaths : bool, optional + Removes the actual path of your project and the init_weights from pose_cfg. + + without_detector: bool, optional + PyTorch engine only. Exports top-down models without the detector. + + engine: Engine, optional, default = None. + The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally + not be done. + + Example: + -------- + Export the first stored snapshot for model trained with shuffle 3: + >>> deeplabcut.export_model('/analysis/project/reaching-task/config.yaml',shuffle=3, snapshotindex=-1) + -------- + """ + if engine is None: + engine = get_shuffle_engine( + _load_config(cfg_path), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import export_model + + return export_model( + cfg_path=cfg_path, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshotindex=snapshotindex, + iteration=iteration, + TFGPUinference=TFGPUinference, + overwrite=overwrite, + make_tar=make_tar, + wipepaths=wipepaths, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis.export import export_model + + return export_model( + config=cfg_path, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshotindex=snapshotindex, + iteration=iteration, + overwrite=overwrite, + wipe_paths=wipepaths, + without_detector=without_detector, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def _update_device(gpu_to_use: int | None, torch_kwargs: dict) -> None: + if "device" not in torch_kwargs and gpu_to_use is not None: + device = _gpu_to_use_to_device(gpu_to_use, device=None) + if device is not None: + torch_kwargs["device"] = device + + +def _gpu_to_use_to_device(gpu_to_use: int | None, device: str | None) -> str | None: + if device is None and gpu_to_use is not None: + if isinstance(gpu_to_use, int): + device = f"cuda:{gpu_to_use}" + else: + device = gpu_to_use + + return device + + +def _load_config(config: str) -> dict: + config_path = Path(config) + if not config_path.exists(): + raise FileNotFoundError(f"Config {config} is not found. Please make sure that the file exists.") + + with open(config) as f: + project_config = YAML(typ="safe", pure=True).load(f) + + return project_config diff --git a/deeplabcut/core/__init__.py b/deeplabcut/core/__init__.py new file mode 100644 index 0000000000..117d127147 --- /dev/null +++ b/deeplabcut/core/__init__.py @@ -0,0 +1,10 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# diff --git a/deeplabcut/core/config.py b/deeplabcut/core/config.py new file mode 100644 index 0000000000..db222def3f --- /dev/null +++ b/deeplabcut/core/config.py @@ -0,0 +1,73 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Simple helper methods related to configuration files stored in yaml files.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +from ruamel.yaml import YAML + + +def read_config_as_dict(config_path: str | Path) -> dict: + """ + Args: + config_path: the path to the configuration file to load + + Returns: + The configuration file with pure Python classes + """ + with open(config_path) as f: + cfg = YAML(typ="safe", pure=True).load(f) + + return cfg + + +def write_config(config_path: str | Path, config: dict, overwrite: bool = True) -> None: + """Writes a pose configuration file to disk. + + Args: + config_path: the path where the config should be saved + config: the config to save + overwrite: whether to overwrite the file if it already exists + + Raises: + FileExistsError if overwrite=True and the file already exists + """ + if not overwrite and Path(config_path).exists(): + raise FileExistsError(f"Cannot write to {config_path} - set overwrite=True to force") + + with open(config_path, "w") as file: + YAML().dump(config, file) + + +def pretty_print( + config: dict, + indent: int = 0, + print_fn: Callable[[str], None] | None = None, +) -> None: + """Prints a model configuration in a pretty and readable way. + + Args: + config: the config to print + indent: the base indent on all keys + print_fn: custom function to call (simply calls ``print`` if None) + """ + if print_fn is None: + print_fn = print + + for k, v in config.items(): + if isinstance(v, dict): + print_fn(f"{indent * ' '}{k}:") + pretty_print(v, indent + 2, print_fn=print_fn) + else: + print_fn(f"{indent * ' '}{k}: {v}") diff --git a/deeplabcut/core/conversion_table.py b/deeplabcut/core/conversion_table.py new file mode 100644 index 0000000000..d40ac940c4 --- /dev/null +++ b/deeplabcut/core/conversion_table.py @@ -0,0 +1,80 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Defines conversion tables mapping DeepLabCut project bodyparts to SA bodyparts.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class ConversionTable: + """Maps DLC project bodyparts to the corresponding SuperAnimal bodyparts. + + The conversion table must satisfy the following conditions (checked by validate): + - All SuperAnimal bodyparts must be valid (defined for the SuperAnimal model) + - All project bodyparts must be valid (defined for the DLC project) + """ + + super_animal: str + project_bodyparts: list[str] + super_animal_bodyparts: list[str] + table: dict[str, str] + + def __post_init__(self): + """Validates the table.""" + self.validate() + + def to_array(self) -> np.ndarray: + """ + Returns: + An array mapping the indices of SuperAnimal bodyparts + + Raises: + ValueError: If the conversion table is misconfigured. + """ + self.validate() + sa_indices = {sa_bpt: i for i, sa_bpt in enumerate(self.super_animal_bodyparts)} + sa_bpt_ordering = [self.table[bpt] for bpt in self.converted_bodyparts()] + return np.array([sa_indices[sa_bpt] for sa_bpt in sa_bpt_ordering]) + + def converted_bodyparts(self) -> list[str]: + """Returns: The project bodyparts included in this ordered""" + return [bpt for bpt in self.project_bodyparts if bpt in self.table] + + def validate(self) -> None: + """ + Raises: + ValueError: If the conversion table is misconfigured. + """ + project_bpts = set(self.project_bodyparts) + sa_bpts = set(self.super_animal_bodyparts) + + mapped_sa = set(self.table.values()) + mapped_project = set(self.table.keys()) + + # check all mapped SuperAnimal bodyparts are in the config + if len(mapped_sa.difference(sa_bpts)) != 0: + extra_bodyparts = set(mapped_sa).difference(sa_bpts) + raise ValueError( + f"Some bodyparts in your mapping are not in the {self.super_animal} " + f"model: {extra_bodyparts}. Available bodyparts are {' '.join(sa_bpts)}" + ) + + # check all given bodyparts are in the project configuration + if len(mapped_project.difference(project_bpts)) != 0: + extra_bodyparts = mapped_project.difference(project_bpts) + raise ValueError( + "Some bodyparts in your mapping are not in your project configuration: " + f"{extra_bodyparts}. Defined bodyparts are {' '.join(project_bpts)}" + ) diff --git a/deeplabcut/core/crossvalutils.py b/deeplabcut/core/crossvalutils.py new file mode 100644 index 0000000000..e62f19ddcc --- /dev/null +++ b/deeplabcut/core/crossvalutils.py @@ -0,0 +1,457 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + + +import os +import pickle +import shutil +from collections import defaultdict +from copy import deepcopy + +import networkx as nx +import numpy as np +import pandas as pd +from scipy.spatial import cKDTree +from sklearn.metrics.cluster import contingency_matrix +from tqdm import tqdm + +from deeplabcut.core.inferenceutils import ( + Assembler, + _parse_ground_truth_data, + evaluate_assembly, +) +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions + + +def _set_up_evaluation(data): + params = dict() + params["joint_names"] = data["metadata"]["all_joints_names"] + params["num_joints"] = len(params["joint_names"]) + partaffinityfield_graph = data["metadata"]["PAFgraph"] + params["paf"] = np.arange(len(partaffinityfield_graph)) + params["paf_graph"] = params["paf_links"] = [partaffinityfield_graph[l] for l in params["paf"]] + params["bpts"] = params["ibpts"] = range(params["num_joints"]) + params["imnames"] = [fn for fn in list(data) if fn != "metadata"] + return params + + +def _form_original_path(path): + root, filename = os.path.split(path) + base, ext = os.path.splitext(filename) + return os.path.join(root, filename.split("c")[0] + ext) + + +def _unsorted_unique(array): + _, inds = np.unique(array, return_index=True) + return np.asarray(array)[np.sort(inds)] + + +def find_closest_neighbors(query: np.ndarray, ref: np.ndarray, k: int = 3) -> np.ndarray: + """Greedy matching of predicted keypoints to ground truth keypoints. + + Args: + query: the query keypoints + ref: the reference keypoints + k: The list of k-th nearest neighbors to return. + + Returns: + an array of shape (len(query), ) containing the index of the closest + reference keypoint for each query keypoint + """ + n_preds = ref.shape[0] + tree = cKDTree(ref) + dist, inds = tree.query(query, k=k) + idx = np.argsort(dist[:, 0]) + neighbors = np.full(len(query), -1, dtype=int) + picked = {tree.n} + for i, ind in enumerate(inds[idx]): + for j in ind: + if j not in picked: + picked.add(j) + neighbors[idx[i]] = j + break + if len(picked) == (n_preds + 1): + break + return neighbors + + +def _calc_separability(vals_left, vals_right, n_bins=101, metric="jeffries", max_sensitivity=False): + if metric not in ("jeffries", "auc"): + raise ValueError("`metric` should be either 'jeffries' or 'auc'.") + + bins = np.linspace(0, 1, n_bins) + hist_left = np.histogram(vals_left, bins=bins)[0] + hist_left = hist_left / hist_left.sum() + hist_right = np.histogram(vals_right, bins=bins)[0] + hist_right = hist_right / hist_right.sum() + tpr = np.cumsum(hist_right) + if metric == "jeffries": + sep = np.sqrt(2 * (1 - np.sum(np.sqrt(hist_left * hist_right)))) # Jeffries-Matusita distance + else: + sep = np.trapz(np.cumsum(hist_left), tpr) + if max_sensitivity: + threshold = bins[max(1, np.argmax(tpr > 0))] + else: + threshold = bins[np.argmin(1 - np.cumsum(hist_left) + tpr)] + return sep, threshold + + +def _calc_within_between_pafs( + data, + metadata, + per_edge=True, + train_set_only=True, +): + data = deepcopy(data) + train_inds = set(metadata["data"]["trainIndices"]) + graph = data["metadata"]["PAFgraph"] + within_train = defaultdict(list) + within_test = defaultdict(list) + between_train = defaultdict(list) + between_test = defaultdict(list) + for i, (key, dict_) in enumerate(data.items()): + if key == "metadata": + continue + + is_train = i in train_inds + if train_set_only and not is_train: + continue + + df = dict_["groundtruth"][2] + try: + df.drop("single", level="individuals", inplace=True) + except KeyError: + pass + bpts = df.index.get_level_values("bodyparts").unique().to_list() + coords_gt = ( + df.unstack(["individuals", "coords"]) + .reindex(bpts, level="bodyparts") + .to_numpy() + .reshape((len(bpts), -1, 2)) + ) + if np.isnan(coords_gt).all(): + continue + + coords = dict_["prediction"]["coordinates"][0] + # Get animal IDs and corresponding indices in the arrays of detections + lookup = dict() + for i, (coord, coord_gt) in enumerate(zip(coords, coords_gt, strict=False)): + inds = np.flatnonzero(np.all(~np.isnan(coord), axis=1)) + inds_gt = np.flatnonzero(np.all(~np.isnan(coord_gt), axis=1)) + if inds.size and inds_gt.size: + neighbors = find_closest_neighbors(coord_gt[inds_gt], coord[inds], k=3) + found = neighbors != -1 + lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]], strict=False)) + + costs = dict_["prediction"]["costs"] + for k, v in costs.items(): + paf = v["m1"] + mask_within = np.zeros(paf.shape, dtype=bool) + s, t = graph[k] + if s not in lookup or t not in lookup: + continue + lu_s = lookup[s] + lu_t = lookup[t] + common_id = set(lu_s).intersection(lu_t) + for id_ in common_id: + mask_within[lu_s[id_], lu_t[id_]] = True + within_vals = paf[mask_within] + between_vals = paf[~mask_within] + if is_train: + within_train[k].extend(within_vals) + between_train[k].extend(between_vals) + else: + within_test[k].extend(within_vals) + between_test[k].extend(between_vals) + if not per_edge: + within_train = np.concatenate([*within_train.values()]) + within_test = np.concatenate([*within_test.values()]) + between_train = np.concatenate([*between_train.values()]) + between_test = np.concatenate([*between_test.values()]) + return (within_train, within_test), (between_train, between_test) + + +def _benchmark_paf_graphs( + config, + inference_cfg, + data, + paf_inds, + greedy=False, + add_discarded=True, + identity_only=False, + calibration_file="", + oks_sigma=0.1, + margin=0, + symmetric_kpts=None, + split_inds=None, +): + metadata = data.pop("metadata") + multi_bpts_orig = auxfun_multianimal.extractindividualsandbodyparts(config)[2] + multi_bpts = [j for j in metadata["all_joints_names"] if j in multi_bpts_orig] + n_multi = len(multi_bpts) + data_ = {"metadata": metadata} + for k, v in data.items(): + data_[k] = v["prediction"] + ass = Assembler( + data_, + max_n_individuals=inference_cfg["topktoretain"], + n_multibodyparts=n_multi, + greedy=greedy, + pcutoff=inference_cfg.get("pcutoff", 0.1), + min_affinity=inference_cfg.get("pafthreshold", 0.1), + add_discarded=add_discarded, + identity_only=identity_only, + ) + if calibration_file: + ass.calibrate(calibration_file) + + params = ass.metadata + image_paths = params["imnames"] + bodyparts = params["joint_names"] + idx = data[image_paths[0]]["groundtruth"][2].unstack("coords").reindex(bodyparts, level="bodyparts").index + mask_multi = idx.get_level_values("individuals") != "single" + if not mask_multi.all(): + idx = idx.drop("single", level="individuals") + individuals = idx.get_level_values("individuals").unique() + n_individuals = len(individuals) + map_ = dict(zip(individuals, range(n_individuals), strict=False)) + + # Form ground truth beforehand + ground_truth = [] + for i, imname in enumerate(image_paths): + temp = data[imname]["groundtruth"][2].reindex(multi_bpts, level="bodyparts") + ground_truth.append(temp.to_numpy().reshape((-1, 2))) + ground_truth = np.stack(ground_truth) + temp = np.ones((*ground_truth.shape[:2], 3)) + temp[..., :2] = ground_truth + temp = temp.reshape((temp.shape[0], n_individuals, -1, 3)) + ass_true_dict = _parse_ground_truth_data(temp) + ids = np.vectorize(map_.get)(idx.get_level_values("individuals").to_numpy()) + ground_truth = np.insert(ground_truth, 2, ids, axis=2) + + # Assemble animals on the full set of detections + paf_inds = sorted(paf_inds, key=len) + n_graphs = len(paf_inds) + all_scores = [] + all_metrics = [] + all_assemblies = [] + for j, paf in enumerate(paf_inds, start=1): + print(f"Graph {j}|{n_graphs}") + ass.paf_inds = paf + ass.assemble() + all_assemblies.append((ass.assemblies, ass.unique, ass.metadata["imnames"])) + if split_inds is not None: + oks = [] + + # get the indices of the images in the training set + dataset_idx = [data[image_name]["index"] for image_name in image_paths] + for inds in split_inds: + ass_gt = {k: v for k, v in ass_true_dict.items() if dataset_idx[k] in inds} + ass_pred = {k: v for k, v in ass.assemblies.items() if dataset_idx[k] in inds} + + oks.append( + evaluate_assembly( + ass_pred, + ass_gt, + oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + greedy_matching=inference_cfg.get("greedy_oks", False), + ) + ) + else: + oks = evaluate_assembly( + ass.assemblies, + ass_true_dict, + oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + greedy_matching=inference_cfg.get("greedy_oks", False), + ) + all_metrics.append(oks) + scores = np.full((len(image_paths), 2), np.nan) + for i, imname in enumerate(tqdm(image_paths)): + gt = ground_truth[i] + gt = gt[~np.isnan(gt).any(axis=1)] + if len(np.unique(gt[:, 2])) < 2: # Only consider frames with 2+ animals + continue + + # Count the number of unassembled bodyparts + n_dets = len(gt) + animals = ass.assemblies.get(i) + if animals is None: + if n_dets: + scores[i, 0] = 1 + else: + animals = [np.c_[animal.data, np.ones(animal.data.shape[0]) * n] for n, animal in enumerate(animals)] + hyp = np.concatenate(animals) + hyp = hyp[~np.isnan(hyp).any(axis=1)] + scores[i, 0] = max(0, (n_dets - hyp.shape[0]) / n_dets) + neighbors = find_closest_neighbors(gt[:, :2], hyp[:, :2]) + valid = neighbors != -1 + id_gt = gt[valid, 2] + id_hyp = hyp[neighbors[valid], -1] + mat = contingency_matrix(id_gt, id_hyp) + purity = mat.max(axis=0).sum() / mat.sum() + scores[i, 1] = purity + all_scores.append((scores, paf)) + + dfs = [] + for score, inds in all_scores: + df = pd.DataFrame(score, columns=["miss", "purity"]) + df["ngraph"] = len(inds) + dfs.append(df) + big_df = pd.concat(dfs) + group = big_df.groupby("ngraph") + return (all_scores, group.agg(["mean", "std"]).T, all_metrics, all_assemblies) + + +def _get_n_best_paf_graphs( + data, + metadata, + full_graph, + n_graphs=10, + root=None, + which="best", + ignore_inds=None, + metric="auc", +): + if which not in ("best", "worst"): + raise ValueError('`which` must be either "best" or "worst"') + + (within_train, _), (between_train, _) = _calc_within_between_pafs( + data, + metadata, + train_set_only=True, + ) + # Handle unlabeled bodyparts... + existing_edges = set(k for k, v in within_train.items() if v) + if ignore_inds is not None: + existing_edges = existing_edges.difference(ignore_inds) + existing_edges = list(existing_edges) + + if not any(between_train.values()): + # Only 1 animal, let us return the full graph indices only + return ([existing_edges], dict(zip(existing_edges, [0] * len(existing_edges), strict=False))) + + scores, _ = zip( + *[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges], strict=False + ) + + # Find minimal skeleton + G = nx.Graph() + for edge, score in zip(existing_edges, scores, strict=False): + if np.isfinite(score): + G.add_edge(*full_graph[edge], weight=score) + if which == "best": + order = np.asarray(existing_edges)[np.argsort(scores)[::-1]] + if root is None: + root = [] + for edge in nx.maximum_spanning_edges(G, data=False): + root.append(full_graph.index(sorted(edge))) + else: + order = np.asarray(existing_edges)[np.argsort(scores)] + if root is None: + root = [] + for edge in nx.minimum_spanning_edges(G, data=False): + root.append(full_graph.index(sorted(edge))) + + n_edges = len(existing_edges) - len(root) + lengths = np.linspace(0, n_edges, min(n_graphs, n_edges + 1), dtype=int)[1:] + order = order[np.isin(order, root, invert=True)] + paf_inds = [root] + for length in lengths: + paf_inds.append(root + list(order[:length])) + return paf_inds, dict(zip(existing_edges, scores, strict=False)) + + +def cross_validate_paf_graphs( + config, + inference_config, + full_data_file, + metadata_file, + output_name="", + pcutoff=0.1, + oks_sigma=0.1, + margin=0, + greedy=False, + add_discarded=True, + calibrate=False, + overwrite_config=True, + n_graphs=10, + paf_inds=None, + symmetric_kpts=None, +): + cfg = auxiliaryfunctions.read_config(config) + inf_cfg = auxiliaryfunctions.read_plainconfig(inference_config) + inf_cfg_temp = inf_cfg.copy() + inf_cfg_temp["pcutoff"] = pcutoff + + with open(full_data_file, "rb") as file: + data = pickle.load(file) + with open(metadata_file, "rb") as file: + metadata = pickle.load(file) + + params = _set_up_evaluation(data) + to_ignore = auxfun_multianimal.filter_unwanted_paf_connections(cfg, params["paf_graph"]) + best_graphs = _get_n_best_paf_graphs( + data, + metadata, + params["paf_graph"], + ignore_inds=to_ignore, + n_graphs=n_graphs, + ) + paf_scores = best_graphs[1] + if paf_inds is None: + paf_inds = best_graphs[0] + + if calibrate: + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) + calibration_file = os.path.join( + cfg["project_path"], + str(trainingsetfolder), + "CollectedData_" + cfg["scorer"] + ".h5", + ) + else: + calibration_file = "" + + results = _benchmark_paf_graphs( + cfg, + inf_cfg_temp, + data, + paf_inds, + greedy, + add_discarded, + oks_sigma=oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + calibration_file=calibration_file, + split_inds=[ + metadata["data"]["trainIndices"], + metadata["data"]["testIndices"], + ], + ) + # Select optimal PAF graph + df = results[1] + size_opt = np.argmax((1 - df.loc["miss", "mean"]) * df.loc["purity", "mean"]) + pose_config = inference_config.replace("inference_cfg", "pose_cfg") + if not overwrite_config: + shutil.copy(pose_config, pose_config.replace(".yaml", "_old.yaml")) + inds = list(paf_inds[size_opt]) + auxiliaryfunctions.edit_config(pose_config, {"paf_best": [int(ind) for ind in inds]}) + if output_name: + with open(output_name, "wb") as file: + pickle.dump([results], file) + return results[:3], paf_scores, results[3][size_opt] + + +# Backwards compatibility +_find_closest_neighbors = find_closest_neighbors diff --git a/deeplabcut/core/debug/__init__.py b/deeplabcut/core/debug/__init__.py new file mode 100644 index 0000000000..bbc583fe09 --- /dev/null +++ b/deeplabcut/core/debug/__init__.py @@ -0,0 +1,46 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from collections.abc import Sequence + +from .debug_logger import ( + DLC_ALL_LIBS_SPECS, + DebugSection, + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + RecordedLog, + build_debug_report, + collect_debug_sections, + collect_executable_summary, + collect_version_summary, + format_debug_report, + get_debug_recorder, + install_debug_recorder, + log_timing, +) + +__all__: Sequence[str] = ( + "DLC_ALL_LIBS_SPECS", + "ExecutableSpec", + "DebugSection", + "InMemoryDebugRecorder", + "LibrarySpec", + "RecordedLog", + "build_debug_report", + "collect_debug_sections", + "collect_executable_summary", + "collect_version_summary", + "format_debug_report", + "get_debug_recorder", + "install_debug_recorder", + "log_timing", +) diff --git a/deeplabcut/core/debug/_debug_utils.py b/deeplabcut/core/debug/_debug_utils.py new file mode 100644 index 0000000000..95e8ef4713 --- /dev/null +++ b/deeplabcut/core/debug/_debug_utils.py @@ -0,0 +1,87 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from __future__ import annotations + +import os +import shutil +import subprocess +from collections.abc import Sequence +from pathlib import Path + + +def _env_flag(name: str, default: bool = False) -> bool: + """Parse a boolean environment variable. + + Accepted truthy values: + 1, true, yes, on + + Accepted falsy values: + 0, false, no, off + """ + value = os.getenv(name) + if value is None: + return default + + value = value.strip().lower() + if value in {"1", "true", "yes", "on"}: + return True + if value in {"0", "false", "no", "off"}: + return False + return default + + +def _env_optional_float(name: str, default: float | None = None) -> float | None: + """Parse an optional float environment variable. + + Empty strings / unset values return ``default``. + Invalid values also fall back to ``default``. + """ + value = os.getenv(name) + if value is None: + return default + + value = value.strip() + if not value: + return default + + try: + return float(value) + except ValueError: + return default + + +def _which(command: str) -> str: + try: + resolved = shutil.which(command) + return str(Path(resolved).resolve()) if resolved else "not-found" + except Exception: + return "not-found" + + +def _command_version(command: str, version_args: Sequence[str] = ("-version",)) -> str: + try: + completed = subprocess.run( + [command, *version_args], + check=False, + capture_output=True, + text=True, + timeout=3, + ) + except Exception: + return "unavailable" + + text = (completed.stdout or completed.stderr or "").strip() + if not text: + return "unavailable" + + first_line = text.splitlines()[0].strip() + return first_line or "unavailable" diff --git a/deeplabcut/core/debug/debug_logger.py b/deeplabcut/core/debug/debug_logger.py new file mode 100644 index 0000000000..a04b72e5b4 --- /dev/null +++ b/deeplabcut/core/debug/debug_logger.py @@ -0,0 +1,595 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from __future__ import annotations + +import logging +import platform +import sys +import threading +import traceback +from collections import deque +from collections.abc import Iterable +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +from importlib import metadata +from pathlib import Path +from time import perf_counter_ns + +from ._debug_utils import ( + _command_version, + _env_flag, + _env_optional_float, + _which, +) + +_DEBUG_HANDLER_ATTR = "_dlc_debug_recorder" +LOG_QUEUE_MAXLEN = 1000 + +# NOTE @C-Achard 2026-05-13: we may want to centralize env vars in a config/settings module in the future +DLC_LOG_TIMING = _env_flag("DLC_LOG_TIMING", default=False) +DLC_LOG_TIMING_THRESHOLD_MS = _env_optional_float("DLC_LOG_TIMING_THRESHOLD_MS", default=None) + + +def reload_debug_settings_from_env() -> None: + """Reload debug/timing settings from environment variables.""" + global DLC_LOG_TIMING, DLC_LOG_TIMING_THRESHOLD_MS + + DLC_LOG_TIMING = _env_flag("DLC_LOG_TIMING", default=False) + DLC_LOG_TIMING_THRESHOLD_MS = _env_optional_float( + "DLC_LOG_TIMING_THRESHOLD_MS", + default=None, + ) + + +@contextmanager +def log_timing( + logger: logging.Logger, + label: str, + *, + level: int = logging.DEBUG, + threshold_ms: float | None = None, +): + """Lightweight scoped timer for debug instrumentation. + + Uses perf_counter_ns() for monotonic timing. + Logs only if logger is enabled for the requested level. + Optionally suppresses tiny timings below ``threshold_ms``. + """ + if not logger.isEnabledFor(level) or not DLC_LOG_TIMING: + yield + return + + effective_threshold_ms = threshold_ms if threshold_ms is not None else DLC_LOG_TIMING_THRESHOLD_MS + t0 = perf_counter_ns() + try: + yield + finally: + dt_ms = (perf_counter_ns() - t0) / 1e6 + if effective_threshold_ms is None or dt_ms >= effective_threshold_ms: + logger.log(level, "%s took %.3f ms", label, dt_ms) + + +@dataclass(frozen=True) +class RecordedLog: + created: float + level: str + logger_name: str + message: str + exc_text: str | None = None + + +class InMemoryDebugRecorder(logging.Handler): + """Lightweight, fail-open in-memory log recorder. + + Safety properties: + - bounded memory via deque(maxlen=...) + - no file/network I/O + - swallow-all-errors in emit() + - does not log from inside itself + - stores only small text snapshots + """ + + def __init__(self, *, capacity: int = LOG_QUEUE_MAXLEN, level: int = logging.DEBUG): + super().__init__(level=level) + self._records: deque[RecordedLog] = deque(maxlen=max(1, int(capacity))) + self._lock = threading.Lock() + self._dropped = 0 + + @property + def dropped_count(self) -> int: + with self._lock: + return self._dropped + + def emit(self, record: logging.LogRecord) -> None: + try: + # Never call logging from here. + # Never inspect application objects. + msg = self._safe_message(record) + exc_text = self._safe_exception_text(record) + + snap = RecordedLog( + created=float(getattr(record, "created", 0.0) or 0.0), + level=str(getattr(record, "levelname", "UNKNOWN")), + logger_name=str(getattr(record, "name", "")), + message=msg, + exc_text=exc_text, + ) + + with self._lock: + self._records.append(snap) + + except Exception: + # Fail open: never let diagnostics interfere with runtime behavior. + try: + self._dropped += 1 + except Exception: + pass + + def clear(self) -> None: + try: + with self._lock: + self._records.clear() + self._dropped = 0 + except Exception: + pass + + def snapshot(self) -> list[RecordedLog]: + try: + with self._lock: + return list(self._records) + except Exception: + return [] + + def render_text(self, *, limit: int = 200) -> str: + lines: list[str] = [] + try: + records = self.snapshot()[-max(1, int(limit)) :] + if not records: + if self._dropped: + return f"[debug-recorder] no captured logs, {self._dropped} internal failures" + return "" + + base = records[0].created + for rec in records: + ts = datetime.fromtimestamp(rec.created).strftime("%H:%M:%S.%f")[:-3] + if DLC_LOG_TIMING: + rel_ms = (rec.created - base) * 1000.0 + lines.append(f"{ts} (+{rel_ms:8.1f} ms) | {rec.level:<8} | {rec.logger_name} | {rec.message}") + else: + lines.append(f"{ts} | {rec.level:<8} | {rec.logger_name} | {rec.message}") + if rec.exc_text: + lines.append(rec.exc_text.rstrip()) + + if self._dropped: + lines.append(f"[debug-recorder] dropped internal failures: {self._dropped}") + except Exception: + return "[debug-recorder] failed to render logs" + return "\n".join(lines) + + @staticmethod + def _safe_message(record: logging.LogRecord) -> str: + try: + return record.getMessage() + except Exception: + try: + return str(record.msg) + except Exception: + return "" + + @staticmethod + def _safe_exception_text(record: logging.LogRecord) -> str | None: + try: + if not record.exc_info: + return None + return "".join(traceback.format_exception(*record.exc_info)) + except Exception: + return "" + + +@dataclass(frozen=True) +class DebugSection: + title: str + items: dict[str, str] + + +def install_debug_recorder( + *, + logger_name: str = "deeplabcut", + capacity: int = LOG_QUEUE_MAXLEN, + handler_level: int = logging.INFO, + ensure_logger_level: int | None = None, +) -> InMemoryDebugRecorder: + """Attach a single in-memory recorder to the requested logger namespace. + + Idempotent: repeated calls return the same recorder. + + Parameters + ---------- + logger_name: + Logger namespace to attach the recorder to. + capacity: + Maximum number of captured records. By default, uses LOG_QUEUE_MAXLEN. + handler_level: + Minimum level stored by the recorder itself. + ensure_logger_level: + Controls whether to adjust the target logger level. + + - None: never modify the logger level + - int: lower the logger only if its effective level is more restrictive + """ + + root_logger = logging.getLogger(logger_name) + + existing = getattr(root_logger, _DEBUG_HANDLER_ATTR, None) + if isinstance(existing, InMemoryDebugRecorder): + return existing + + recorder = InMemoryDebugRecorder(capacity=capacity, level=handler_level) + recorder.set_name("deeplabcut-debug-recorder") + + # Important: + # - attach only to a DLC-owned logger namespace, not the global root logger + # - keep propagation unchanged + # - logger level adjustment, if any, is handled below; "auto" initializes + # an unset logger to ``handler_level`` rather than forcing DEBUG + root_logger.addHandler(recorder) + + if isinstance(ensure_logger_level, int): + # Only lower verbosity if explicitly requested. + if root_logger.getEffectiveLevel() > ensure_logger_level: + root_logger.setLevel(ensure_logger_level) + + setattr(root_logger, _DEBUG_HANDLER_ATTR, recorder) + return recorder + + +def get_debug_recorder(*, logger_name: str = "deeplabcut") -> InMemoryDebugRecorder | None: + logger = logging.getLogger(logger_name) + recorder = getattr(logger, _DEBUG_HANDLER_ATTR, None) + return recorder if isinstance(recorder, InMemoryDebugRecorder) else None + + +# -------------------------- +# Environment / version info +# -------------------------- + + +@dataclass(frozen=True) +class LibrarySpec: + """Small description of a library to report.""" + + key: str + dist_name: str | None = None + module_name: str | None = None + prefer_module_version: bool = False + + def resolved_dist_name(self) -> str: + return self.dist_name or self.key + + def resolved_module_name(self) -> str: + return self.module_name or self.key + + +DLC_CORE_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("deeplabcut"), + LibrarySpec("torch"), + LibrarySpec("torchvision"), + LibrarySpec("numpy"), + LibrarySpec("pandas"), + LibrarySpec("scipy"), + LibrarySpec("h5py"), + LibrarySpec("tables"), + LibrarySpec("opencv-python", dist_name="opencv-python", module_name="cv2", prefer_module_version=True), +) +DLC_GUI_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("PySide6"), + LibrarySpec("shiboken6"), + LibrarySpec("qtpy", dist_name="QtPy"), + LibrarySpec("qdarkstyle"), + LibrarySpec("napari"), + LibrarySpec("napari-deeplabcut", dist_name="napari-deeplabcut", module_name="napari_deeplabcut"), +) +DLC_TF_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("tensorflow"), + LibrarySpec("tf_keras", dist_name="tf-keras"), + LibrarySpec("tensorpack"), + LibrarySpec("tf_slim", dist_name="tf-slim"), +) +DLC_ALL_LIBS_SPECS: tuple[LibrarySpec, ...] = DLC_CORE_LIBS + DLC_GUI_LIBS + DLC_TF_LIBS + + +def _normalize_library_specs( + libraries: Iterable[LibrarySpec | str] | None, +) -> tuple[LibrarySpec, ...]: + if libraries is None: + return DLC_ALL_LIBS_SPECS + + normalized: list[LibrarySpec] = [] + for item in libraries: + if isinstance(item, LibrarySpec): + normalized.append(item) + else: + normalized.append(LibrarySpec(str(item))) + return tuple(normalized) + + +def _version(dist_name: str) -> str: + try: + return metadata.version(dist_name) + except Exception: + return "not-installed" + + +def _module_path(module_name: str) -> str: + try: + mod = __import__(module_name) + p = getattr(mod, "__file__", None) + return str(Path(p).resolve()) if p else "unknown" + except Exception: + return "unknown" + + +def _safe_tail(pathlike: object) -> str: + """Redact user-specific absolute paths. + + Keeps only the last 2 path components when possible. + """ + try: + p = Path(str(pathlike)) + parts = p.parts + if len(parts) >= 2: + return str(Path(*parts[-2:]).as_posix()) + return str(p.as_posix()) + except Exception: + return str(pathlike) + + +def _module_version(module_name: str) -> str: + try: + mod = __import__(module_name) + version = getattr(mod, "__version__", None) + if version: + return str(version) + return "unknown" + except Exception: + return "not-installed" + + +def collect_version_summary( + *, + libraries: Iterable[LibrarySpec | str] | None = None, + include_module_paths: bool = False, +) -> dict[str, str]: + specs = _normalize_library_specs(libraries) + summary: dict[str, str] = {} + + for spec in specs: + key = spec.key + module_name = spec.resolved_module_name() + + if spec.prefer_module_version: + version = _module_version(module_name) + if version in {"not-installed", "unknown"}: + version = _version(spec.resolved_dist_name()) + else: + version = _version(spec.resolved_dist_name()) + + summary[key] = version + + if include_module_paths: + summary[f"{key}_module_path"] = _safe_tail(_module_path(module_name)) + + return summary + + +@dataclass(frozen=True) +class ExecutableSpec: + """Small description of an external executable to report. + + Parameters + ---------- + key: + Label used in the output report. + command: + Executable name or absolute path to resolve. + version_args: + Arguments used to query the executable version. + """ + + key: str + command: str | None = None + version_args: tuple[str, ...] = ("-version",) + + def resolved_command(self) -> str: + return self.command or self.key + + +DEFAULT_EXECUTABLE_SPECS: tuple[ExecutableSpec, ...] = (ExecutableSpec("ffmpeg"),) + + +def _normalize_executable_specs( + executables: Iterable[ExecutableSpec | str] | None, +) -> tuple[ExecutableSpec, ...]: + if executables is None: + return DEFAULT_EXECUTABLE_SPECS + + normalized: list[ExecutableSpec] = [] + for item in executables: + if isinstance(item, ExecutableSpec): + normalized.append(item) + else: + normalized.append(ExecutableSpec(str(item))) + return tuple(normalized) + + +def collect_executable_summary( + *, + executables: Iterable[ExecutableSpec | str] | None = None, + include_paths: bool = True, +) -> dict[str, str]: + specs = _normalize_executable_specs(executables) + summary: dict[str, str] = {} + + for spec in specs: + key = spec.key + command = spec.resolved_command() + summary[key] = _command_version(command, spec.version_args) + if include_paths: + summary[f"{key}_path"] = _safe_tail(_which(command)) + + return summary + + +# -------------------------- +# Report formatting +# -------------------------- + + +def format_debug_report( + *, + sections: Iterable[DebugSection], + logs_text: str, +) -> str: + lines: list[str] = [] + + for section in sections: + lines.append(f"## {section.title}") + if section.items: + for k, v in section.items.items(): + lines.append(f"- {k}: {v}") + else: + lines.append("- ") + lines.append("") + + lines.append("## Recent logs") + lines.append("```text") + lines.append(logs_text or "") + lines.append("```") + + return "\n".join(lines) + + +def build_debug_report( + *, + recorder: InMemoryDebugRecorder | None, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, +) -> str: + logs_text = recorder.render_text(limit=log_limit) if recorder is not None else "" + + sections = collect_debug_sections( + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + ) + + return format_debug_report( + sections=sections, + logs_text=logs_text, + ) + + +def collect_runtime_summary() -> dict[str, str]: + return { + "python": sys.version.replace("\n", " "), + "platform": platform.platform(), + "executable": _safe_tail(sys.executable), + } + + +def _section_has_useful_values(items: dict[str, str]) -> bool: + for value in items.values(): + if value not in {"not-installed", "unknown", "not-found", "unavailable"}: + return True + return False + + +def collect_debug_sections( + *, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, +) -> list[DebugSection]: + sections: list[DebugSection] = [] + + # Always include the runtime section first + sections.append( + DebugSection( + title="Runtime", + items=collect_runtime_summary(), + ) + ) + + # Default grouped report using your built-in constants + if libraries is None: + sections.append( + DebugSection( + title="DeepLabCut core libraries", + items=collect_version_summary( + libraries=DLC_CORE_LIBS, + include_module_paths=include_module_paths, + ), + ) + ) + + sections.append( + DebugSection( + title="GUI libraries", + items=collect_version_summary( + libraries=DLC_GUI_LIBS, + include_module_paths=include_module_paths, + ), + ) + ) + + tf_items = collect_version_summary( + libraries=DLC_TF_LIBS, + include_module_paths=include_module_paths, + ) + if tf_items and _section_has_useful_values(tf_items): + sections.append( + DebugSection( + title="TensorFlow libraries", + items=tf_items, + ) + ) + else: + # Custom input + sections.append( + DebugSection( + title="Libraries", + items=collect_version_summary( + libraries=libraries, + include_module_paths=include_module_paths, + ), + ) + ) + + exec_items = collect_executable_summary( + executables=executables, + include_paths=include_executable_paths, + ) + if exec_items: # report if unavailable + sections.append( + DebugSection( + title="External tools", + items=exec_items, + ), + ) + + return sections diff --git a/deeplabcut/core/engine.py b/deeplabcut/core/engine.py new file mode 100644 index 0000000000..1f7a51d60b --- /dev/null +++ b/deeplabcut/core/engine.py @@ -0,0 +1,50 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Defines the deep learning frameworks available.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +@dataclass(frozen=True) +class EngineDataMixin: + aliases: tuple[str] + model_folder_name: str + pose_cfg_name: str + results_folder_name: str + + +class Engine(EngineDataMixin, Enum): + PYTORCH = ( + ("pytorch", "torch"), + "dlc-models-pytorch", + "pytorch_config.yaml", + "evaluation-results-pytorch", + ) + TF = ( + ("tensorflow", "tf"), + "dlc-models", + "pose_cfg.yaml", + "evaluation-results", + ) + + @classmethod + def _missing_(cls, value): + if isinstance(value, str): + for member in cls: + if value.lower() in member.aliases: + return member + return None + + def __repr__(self) -> str: + return f"Engine.{self.name}" diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py new file mode 100644 index 0000000000..b7bb82f108 --- /dev/null +++ b/deeplabcut/core/inferenceutils.py @@ -0,0 +1,1266 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +import heapq +import itertools +import multiprocessing +import operator +import pickle +import warnings +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass +from math import erf, sqrt +from typing import Any + +import networkx as nx +import numpy as np +import pandas as pd +from scipy.optimize import linear_sum_assignment +from scipy.spatial import cKDTree +from scipy.spatial.distance import cdist, pdist +from scipy.special import softmax +from scipy.stats import chi2, gaussian_kde +from tqdm import tqdm + + +def _conv_square_to_condensed_indices(ind_row, ind_col, n): + if ind_row == ind_col: + raise ValueError("There are no diagonal elements in condensed matrices.") + + if ind_row < ind_col: + ind_row, ind_col = ind_col, ind_row + return n * ind_col - ind_col * (ind_col + 1) // 2 + ind_row - 1 - ind_col + + +Position = tuple[float, float] + + +@dataclass(frozen=True) +class Joint: + pos: Position + confidence: float = 1.0 + label: int = None + idx: int = None + group: int = -1 + + +class Link: + def __init__(self, j1, j2, affinity=1): + self.j1 = j1 + self.j2 = j2 + self.affinity = affinity + self._length = sqrt((j1.pos[0] - j2.pos[0]) ** 2 + (j1.pos[1] - j2.pos[1]) ** 2) + + def __repr__(self): + return f"Link {self.idx}, affinity={self.affinity:.2f}, length={self.length:.2f}" + + @property + def confidence(self): + return self.j1.confidence * self.j2.confidence + + @property + def idx(self): + return self.j1.idx, self.j2.idx + + @property + def length(self): + return self._length + + @length.setter + def length(self, length): + self._length = length + + def to_vector(self): + return [*self.j1.pos, *self.j2.pos] + + +class Assembly: + def __init__(self, size): + self.data = np.full((size, 4), np.nan) + self.confidence = 0 # 0 by default, overwritten otherwise with `add_joint` + self._affinity = 0 + self._links = [] + self._visible = set() + self._idx = set() + self._dict = dict() + + def __len__(self): + return len(self._visible) + + def __contains__(self, assembly): + return bool(self._visible.intersection(assembly._visible)) + + def __add__(self, other): + if other in self: + raise ValueError("Assemblies contain shared joints.") + + assembly = Assembly(self.data.shape[0]) + for link in self._links + other._links: + assembly.add_link(link) + return assembly + + @classmethod + def from_array(cls, array): + n_bpts, n_cols = array.shape + + # if a single coordinate is NaN for a bodypart, set all to NaN + array[np.isnan(array).any(axis=-1)] = np.nan + + ass = cls(size=n_bpts) + ass.data[:, :n_cols] = array + visible = np.flatnonzero(~np.isnan(array).any(axis=1)) + if n_cols < 3: # Only xy coordinates are being set + ass.data[visible, 2] = 1 # Set detection confidence to 1 + ass._visible.update(visible) + return ass + + @property + def xy(self): + return self.data[:, :2] + + @property + def extent(self): + bbox = np.empty(4) + bbox[:2] = np.nanmin(self.xy, axis=0) + bbox[2:] = np.nanmax(self.xy, axis=0) + return bbox + + @property + def area(self): + x1, y1, x2, y2 = self.extent + return (x2 - x1) * (y2 - y1) + + @property + def confidence(self): + return np.nanmean(self.data[:, 2]) + + @confidence.setter + def confidence(self, confidence): + self.data[:, 2] = confidence + + @property + def soft_identity(self): + data = self.data[~np.isnan(self.data).any(axis=1)] + unq, idx, cnt = np.unique(data[:, 3], return_inverse=True, return_counts=True) + avg = np.bincount(idx, weights=data[:, 2]) / cnt + soft = softmax(avg) + return dict(zip(unq.astype(int), soft, strict=False)) + + @property + def affinity(self): + n_links = self.n_links + if not n_links: + return 0 + return self._affinity / n_links + + @property + def n_links(self): + return len(self._links) + + def intersection_with(self, other): + x11, y11, x21, y21 = self.extent + x12, y12, x22, y22 = other.extent + x1 = max(x11, x12) + y1 = max(y11, y12) + x2 = min(x21, x22) + y2 = min(y21, y22) + if x2 < x1 or y2 < y1: + return 0 + ll = np.array([x1, y1]) + ur = np.array([x2, y2]) + xy1 = self.xy[~np.isnan(self.xy).any(axis=1)] + xy2 = other.xy[~np.isnan(other.xy).any(axis=1)] + in1 = np.all((xy1 >= ll) & (xy1 <= ur), axis=1).sum() + in2 = np.all((xy2 >= ll) & (xy2 <= ur), axis=1).sum() + return min(in1 / len(self), in2 / len(other)) + + def add_joint(self, joint): + if joint.label in self._visible or joint.label is None: + return False + self.data[joint.label] = *joint.pos, joint.confidence, joint.group + self._visible.add(joint.label) + self._idx.add(joint.idx) + return True + + def remove_joint(self, joint): + if joint.label not in self._visible: + return False + self.data[joint.label] = np.nan + self._visible.remove(joint.label) + self._idx.remove(joint.idx) + return True + + def add_link(self, link, store_dict=False): + if store_dict: + # Selective copy; deepcopy is >5x slower + self._dict = { + "data": self.data.copy(), + "_affinity": self._affinity, + "_links": self._links.copy(), + "_visible": self._visible.copy(), + "_idx": self._idx.copy(), + } + i1, i2 = link.idx + if i1 in self._idx and i2 in self._idx: + self._affinity += link.affinity + self._links.append(link) + return False + if link.j1.label in self._visible and link.j2.label in self._visible: + return False + self.add_joint(link.j1) + self.add_joint(link.j2) + self._affinity += link.affinity + self._links.append(link) + return True + + def calc_pairwise_distances(self): + return pdist(self.xy, metric="sqeuclidean") + + +class Assembler: + def __init__( + self, + data, + *, + max_n_individuals, + n_multibodyparts, + graph=None, + paf_inds=None, + greedy=False, + pcutoff=0.1, + min_affinity=0.05, + min_n_links=2, + max_overlap=0.8, + identity_only=False, + nan_policy="little", + force_fusion=False, + add_discarded=False, + window_size=0, + method="m1", + ): + self.data = data + self.metadata = self.parse_metadata(self.data) + self.max_n_individuals = max_n_individuals + self.n_multibodyparts = n_multibodyparts + self.n_uniquebodyparts = self.n_keypoints - n_multibodyparts + self.greedy = greedy + self.pcutoff = pcutoff + self.min_affinity = min_affinity + self.min_n_links = min_n_links + self.max_overlap = max_overlap + self._has_identity = "identity" in self[0] + if identity_only and not self._has_identity: + warnings.warn("The network was not trained with identity; setting `identity_only` to False.", stacklevel=2) + self.identity_only = identity_only & self._has_identity + self.nan_policy = nan_policy + self.force_fusion = force_fusion + self.add_discarded = add_discarded + self.window_size = window_size + self.method = method + self.graph = graph or self.metadata["paf_graph"] + self.paf_inds = paf_inds or self.metadata["paf"] + self._gamma = 0.01 + self._trees = dict() + self.safe_edge = False + self._kde = None + self.assemblies = dict() + self.unique = dict() + + def __getitem__(self, item): + return self.data[self.metadata["imnames"][item]] + + @classmethod + def empty( + cls, + max_n_individuals, + n_multibodyparts, + n_uniquebodyparts, + graph, + paf_inds, + greedy=False, + pcutoff=0.1, + min_affinity=0.05, + min_n_links=2, + max_overlap=0.8, + identity_only=False, + nan_policy="little", + force_fusion=False, + add_discarded=False, + window_size=0, + method="m1", + ): + # Dummy data + n_bodyparts = n_multibodyparts + n_uniquebodyparts + data = { + "metadata": { + "all_joints_names": ["" for _ in range(n_bodyparts)], + "PAFgraph": graph, + "PAFinds": paf_inds, + }, + "0": {}, + } + return cls( + data, + max_n_individuals=max_n_individuals, + n_multibodyparts=n_multibodyparts, + graph=graph, + paf_inds=paf_inds, + greedy=greedy, + pcutoff=pcutoff, + min_affinity=min_affinity, + min_n_links=min_n_links, + max_overlap=max_overlap, + identity_only=identity_only, + nan_policy=nan_policy, + force_fusion=force_fusion, + add_discarded=add_discarded, + window_size=window_size, + method=method, + ) + + @property + def n_keypoints(self): + return self.metadata["num_joints"] + + def calibrate(self, train_data_file): + df = pd.read_hdf(train_data_file) + try: + df.drop("single", level="individuals", axis=1, inplace=True) + except KeyError: + pass + n_bpts = len(df.columns.get_level_values("bodyparts").unique()) + if n_bpts == 1: + warnings.warn("There is only one keypoint; skipping calibration...", stacklevel=2) + return + + xy = df.to_numpy().reshape((-1, n_bpts, 2)) + frac_valid = np.mean(~np.isnan(xy), axis=(1, 2)) + # Only keeps skeletons that are more than 90% complete + xy = xy[frac_valid >= 0.9] + if not xy.size: + warnings.warn("No complete poses were found. Skipping calibration...", stacklevel=2) + return + + # TODO Normalize dists by longest length? + # TODO Smarter imputation technique (Bayesian? Grassmann averages?) + dists = np.vstack([pdist(data, "sqeuclidean") for data in xy]) + mu = np.nanmean(dists, axis=0) + missing = np.isnan(dists) + dists = np.where(missing, mu, dists) + try: + kde = gaussian_kde(dists.T) + kde.mean = mu + self._kde = kde + self.safe_edge = True + except np.linalg.LinAlgError: + # Covariance matrix estimation fails due to numerical singularities + warnings.warn("The assembler could not be robustly calibrated. Continuing without it...", stacklevel=2) + + def calc_assembly_mahalanobis_dist(self, assembly, return_proba=False, nan_policy="little"): + if self._kde is None: + raise ValueError("Assembler should be calibrated first with training data.") + + dists = assembly.calc_pairwise_distances() - self._kde.mean + mask = np.isnan(dists) + # Distance is undefined if the assembly is empty + if not len(assembly) or mask.all(): + if return_proba: + return np.inf, 0 + return np.inf + + if nan_policy == "little": + inds = np.flatnonzero(~mask) + dists = dists[inds] + inv_cov = self._kde.inv_cov[np.ix_(inds, inds)] + # Correct distance to account for missing observations + factor = self._kde.d / len(inds) + else: + # Alternatively, reduce contribution of missing values to the Mahalanobis + # distance to zero by substituting the corresponding means. + dists[mask] = 0 + mask.fill(False) + inv_cov = self._kde.inv_cov + factor = 1 + dot = dists @ inv_cov + mahal = factor * sqrt(np.sum((dot * dists), axis=-1)) + if return_proba: + proba = 1 - chi2.cdf(mahal, np.sum(~mask)) + return mahal, proba + return mahal + + def calc_link_probability(self, link): + if self._kde is None: + raise ValueError("Assembler should be calibrated first with training data.") + + i = link.j1.label + j = link.j2.label + ind = _conv_square_to_condensed_indices(i, j, self.n_multibodyparts) + mu = self._kde.mean[ind] + sigma = self._kde.covariance[ind, ind] + z = (link.length**2 - mu) / sigma + return 2 * (1 - 0.5 * (1 + erf(abs(z) / sqrt(2)))) + + @staticmethod + def _flatten_detections(data_dict): + ind = 0 + coordinates = data_dict["coordinates"][0] + confidence = data_dict["confidence"] + ids = data_dict.get("identity", None) + if ids is None: + ids = [np.ones(len(arr), dtype=int) * -1 for arr in confidence] + else: + ids = [arr.argmax(axis=1) for arr in ids] + for i, (coords, conf, id_) in enumerate(zip(coordinates, confidence, ids, strict=False)): + if not np.any(coords): + continue + for xy, p, g in zip(coords, conf, id_, strict=False): + joint = Joint(tuple(xy), p.item(), i, ind, g) + ind += 1 + yield joint + + def extract_best_links(self, joints_dict, costs, trees=None): + links = [] + for ind in self.paf_inds: + s, t = self.graph[ind] + dets_s = joints_dict.get(s, None) + dets_t = joints_dict.get(t, None) + if dets_s is None or dets_t is None: + continue + if ind not in costs: + continue + lengths = costs[ind]["distance"] + if np.isinf(lengths).all(): + continue + aff = costs[ind][self.method].copy() + aff[np.isnan(aff)] = 0 + + if trees: + vecs = np.vstack([[*det_s.pos, *det_t.pos] for det_s in dets_s for det_t in dets_t]) + dists = [] + for n, tree in enumerate(trees, start=1): + d, _ = tree.query(vecs) + dists.append(np.exp(-self._gamma * n * d)) + w = np.mean(dists, axis=0) + aff *= w.reshape(aff.shape) + + if self.greedy: + conf = np.asarray([[det_s.confidence * det_t.confidence for det_t in dets_t] for det_s in dets_s]) + rows, cols = np.where((conf >= self.pcutoff * self.pcutoff) & (aff >= self.min_affinity)) + candidates = sorted( + zip(rows, cols, aff[rows, cols], lengths[rows, cols], strict=False), + key=lambda x: x[2], + reverse=True, + ) + i_seen = set() + j_seen = set() + for i, j, w, _l in candidates: + if i not in i_seen and j not in j_seen: + i_seen.add(i) + j_seen.add(j) + links.append(Link(dets_s[i], dets_t[j], w)) + if len(i_seen) == self.max_n_individuals: + break + else: # Optimal keypoint pairing + inds_s = sorted(range(len(dets_s)), key=lambda x: dets_s[x].confidence, reverse=True)[ + : self.max_n_individuals + ] + inds_t = sorted(range(len(dets_t)), key=lambda x: dets_t[x].confidence, reverse=True)[ + : self.max_n_individuals + ] + keep_s = [ind for ind in inds_s if dets_s[ind].confidence >= self.pcutoff] + keep_t = [ind for ind in inds_t if dets_t[ind].confidence >= self.pcutoff] + aff = aff[np.ix_(keep_s, keep_t)] + rows, cols = linear_sum_assignment(aff, maximize=True) + for row, col in zip(rows, cols, strict=False): + w = aff[row, col] + if w >= self.min_affinity: + links.append(Link(dets_s[keep_s[row]], dets_t[keep_t[col]], w)) + return links + + def _fill_assembly(self, assembly, lookup, assembled, safe_edge, nan_policy): + stack = [] + visited = set() + tabu = [] + counter = itertools.count() + + def push_to_stack(i): + for j, link in lookup[i].items(): + if j in assembly._idx: + continue + if link.idx in visited: + continue + heapq.heappush(stack, (-link.affinity, next(counter), link)) + visited.add(link.idx) + + for idx in assembly._idx: + push_to_stack(idx) + + while stack and len(assembly) < self.n_multibodyparts: + _, _, best = heapq.heappop(stack) + i, j = best.idx + if i in assembly._idx: + new_ind = j + elif j in assembly._idx: + new_ind = i + else: + continue + if new_ind in assembled: + continue + if safe_edge: + d_old = self.calc_assembly_mahalanobis_dist(assembly, nan_policy=nan_policy) + success = assembly.add_link(best, store_dict=True) + if not success: + assembly._dict = dict() + continue + d = self.calc_assembly_mahalanobis_dist(assembly, nan_policy=nan_policy) + if d < d_old: + push_to_stack(new_ind) + try: + _, _, link = heapq.heappop(tabu) + heapq.heappush(stack, (-link.affinity, next(counter), link)) + except IndexError: + pass + else: + heapq.heappush(tabu, (d - d_old, next(counter), best)) + assembly.__dict__.update(assembly._dict) + assembly._dict = dict() + else: + assembly.add_link(best) + push_to_stack(new_ind) + + def build_assemblies(self, links): + lookup = defaultdict(dict) + for link in links: + i, j = link.idx + lookup[i][j] = link + lookup[j][i] = link + + assemblies = [] + assembled = set() + + # Fill the subsets with unambiguous, complete individuals + G = nx.Graph([link.idx for link in links]) + for chain in nx.connected_components(G): + if len(chain) == self.n_multibodyparts: + edges = [tuple(sorted(edge)) for edge in G.edges(chain)] + assembly = Assembly(self.n_multibodyparts) + for link in links: + i, j = link.idx + if (i, j) in edges: + success = assembly.add_link(link) + if success: + lookup[i].pop(j) + lookup[j].pop(i) + assembled.update(assembly._idx) + assemblies.append(assembly) + + if len(assemblies) == self.max_n_individuals: + return assemblies, assembled + + for link in sorted(links, key=lambda x: x.affinity, reverse=True): + if any(i in assembled for i in link.idx): + continue + assembly = Assembly(self.n_multibodyparts) + assembly.add_link(link) + self._fill_assembly(assembly, lookup, assembled, self.safe_edge, self.nan_policy) + for link in assembly._links: + i, j = link.idx + lookup[i].pop(j) + lookup[j].pop(i) + assembled.update(assembly._idx) + assemblies.append(assembly) + + # Fuse superfluous assemblies + n_extra = len(assemblies) - self.max_n_individuals + if n_extra > 0: + if self.safe_edge: + ds_old = [self.calc_assembly_mahalanobis_dist(assembly) for assembly in assemblies] + while len(assemblies) > self.max_n_individuals: + ds = [] + for i, j in itertools.combinations(range(len(assemblies)), 2): + if assemblies[j] not in assemblies[i]: + temp = assemblies[i] + assemblies[j] + d = self.calc_assembly_mahalanobis_dist(temp) + delta = d - max(ds_old[i], ds_old[j]) + ds.append((i, j, delta, d, temp)) + if not ds: + break + min_ = sorted(ds, key=lambda x: x[2]) + i, j, delta, d, new = min_[0] + if delta < 0 or len(min_) == 1: + assemblies[i] = new + assemblies.pop(j) + ds_old[i] = d + ds_old.pop(j) + else: + break + elif self.force_fusion: + assemblies = sorted(assemblies, key=len) + for nrow in range(n_extra): + assembly = assemblies[nrow] + candidates = [a for a in assemblies[nrow:] if assembly not in a] + if not candidates: + continue + if len(candidates) == 1: + candidate = candidates[0] + else: + dists = [] + for cand in candidates: + d = cdist(assembly.xy, cand.xy) + dists.append(np.nanmin(d)) + candidate = candidates[np.argmin(dists)] + ind = assemblies.index(candidate) + assemblies[ind] += assembly + else: + store = dict() + for assembly in assemblies: + if len(assembly) != self.n_multibodyparts: + for i in assembly._idx: + store[i] = assembly + used = [link for assembly in assemblies for link in assembly._links] + unconnected = [link for link in links if link not in used] + for link in unconnected: + i, j = link.idx + try: + if store[j] not in store[i]: + temp = store[i] + store[j] + store[i].__dict__.update(temp.__dict__) + assemblies.remove(store[j]) + for idx in store[j]._idx: + store[idx] = store[i] + except KeyError: + pass + + # Second pass without edge safety + for assembly in assemblies: + if len(assembly) != self.n_multibodyparts: + self._fill_assembly(assembly, lookup, assembled, False, "") + assembled.update(assembly._idx) + + return assemblies, assembled + + def _assemble(self, data_dict, ind_frame): + joints = list(self._flatten_detections(data_dict)) + if not joints: + return None, None + + bag = defaultdict(list) + for joint in joints: + bag[joint.label].append(joint) + + assembled = set() + + if self.n_uniquebodyparts: + unique = np.full((self.n_uniquebodyparts, 3), np.nan) + for n, ind in enumerate(range(self.n_multibodyparts, self.n_keypoints)): + dets = bag[ind] + if not dets: + continue + if len(dets) > 1: + det = max(dets, key=lambda x: x.confidence) + else: + det = dets[0] + # Mark the unique body parts as assembled anyway so + # they are not used later on to fill assemblies. + assembled.update(d.idx for d in dets) + if det.confidence <= self.pcutoff and not self.add_discarded: + continue + unique[n] = *det.pos, det.confidence + if np.isnan(unique).all(): + unique = None + else: + unique = None + + if not any(i in bag for i in range(self.n_multibodyparts)): + return None, unique + + if self.n_multibodyparts == 1: + assemblies = [] + for joint in bag[0]: + if joint.confidence >= self.pcutoff: + ass = Assembly(self.n_multibodyparts) + ass.add_joint(joint) + assemblies.append(ass) + return assemblies, unique + + if self.max_n_individuals == 1: + get_attr = operator.attrgetter("confidence") + ass = Assembly(self.n_multibodyparts) + for ind in range(self.n_multibodyparts): + joints = bag[ind] + if not joints: + continue + ass.add_joint(max(joints, key=get_attr)) + return [ass], unique + + if self.identity_only: + assemblies = [] + get_attr = operator.attrgetter("group") + temp = sorted( + (joint for joint in joints if np.isfinite(joint.confidence)), + key=get_attr, + ) + groups = itertools.groupby(temp, get_attr) + for _, group in groups: + ass = Assembly(self.n_multibodyparts) + for joint in sorted(group, key=lambda x: x.confidence, reverse=True): + if joint.confidence >= self.pcutoff and joint.label < self.n_multibodyparts: + ass.add_joint(joint) + if len(ass): + assemblies.append(ass) + assembled.update(ass._idx) + else: + trees = [] + for j in range(1, self.window_size + 1): + tree = self._trees.get(ind_frame - j, None) + if tree is not None: + trees.append(tree) + + links = self.extract_best_links(bag, data_dict["costs"], trees) + if self._kde: + for link in links[::-1]: + p = max(self.calc_link_probability(link), 0.001) + link.affinity *= p + if link.affinity < self.min_affinity: + links.remove(link) + + if self.window_size >= 1 and links: + # Store selected edges for subsequent frames + vecs = np.vstack([link.to_vector() for link in links]) + self._trees[ind_frame] = cKDTree(vecs) + + assemblies, assembled_ = self.build_assemblies(links) + assembled.update(assembled_) + + # Remove invalid assemblies + discarded = set(joint for joint in joints if joint.idx not in assembled and np.isfinite(joint.confidence)) + for assembly in assemblies[::-1]: + if 0 < assembly.n_links < self.min_n_links or not len(assembly): + for link in assembly._links: + discarded.update((link.j1, link.j2)) + assemblies.remove(assembly) + if 0 < self.max_overlap < 1: # Non-maximum pose suppression + if self._kde is not None: + scores = [-self.calc_assembly_mahalanobis_dist(ass) for ass in assemblies] + else: + scores = [ass._affinity for ass in assemblies] + lst = list(zip(scores, assemblies, strict=False)) + assemblies = [] + while lst: + temp = max(lst, key=lambda x: x[0]) + lst.remove(temp) + assemblies.append(temp[1]) + for pair in lst[::-1]: + if temp[1].intersection_with(pair[1]) >= self.max_overlap: + lst.remove(pair) + if len(assemblies) > self.max_n_individuals: + assemblies = sorted(assemblies, key=len, reverse=True) + for assembly in assemblies[self.max_n_individuals :]: + for link in assembly._links: + discarded.update((link.j1, link.j2)) + assemblies = assemblies[: self.max_n_individuals] + + if self.add_discarded and discarded: + # Fill assemblies with unconnected body parts + for joint in sorted(discarded, key=lambda x: x.confidence, reverse=True): + if self.safe_edge: + for assembly in assemblies: + if joint.label in assembly._visible: + continue + d_old = self.calc_assembly_mahalanobis_dist(assembly) + assembly.add_joint(joint) + d = self.calc_assembly_mahalanobis_dist(assembly) + if d < d_old: + break + assembly.remove_joint(joint) + else: + dists = [] + for i, assembly in enumerate(assemblies): + if joint.label in assembly._visible: + continue + d = cdist(assembly.xy, np.atleast_2d(joint.pos)) + dists.append((i, np.nanmin(d))) + if not dists: + continue + min_ = sorted(dists, key=lambda x: x[1]) + ind, _ = min_[0] + assemblies[ind].add_joint(joint) + + return assemblies, unique + + def assemble(self, chunk_size=1, n_processes=None): + self.assemblies = dict() + self.unique = dict() + # Spawning (rather than forking) multiple processes does not + # work nicely with the GUI or interactive sessions. + # In that case, we fall back to the serial assembly. + if chunk_size == 0 or multiprocessing.get_start_method() == "spawn": + for i, data_dict in enumerate(tqdm(self)): + assemblies, unique = self._assemble(data_dict, i) + if assemblies: + self.assemblies[i] = assemblies + if unique is not None: + self.unique[i] = unique + else: + global wrapped # Hack to make the function pickable + + def wrapped(i): + return i, self._assemble(self[i], i) + + n_frames = len(self.metadata["imnames"]) + with multiprocessing.Pool(n_processes) as p: + with tqdm(total=n_frames) as pbar: + for i, (assemblies, unique) in p.imap_unordered(wrapped, range(n_frames), chunksize=chunk_size): + if assemblies: + self.assemblies[i] = assemblies + if unique is not None: + self.unique[i] = unique + pbar.update() + + def from_pickle(self, pickle_path): + with open(pickle_path, "rb") as file: + data = pickle.load(file) + self.unique = data.pop("single", {}) + self.assemblies = data + + @staticmethod + def parse_metadata(data): + params = dict() + params["joint_names"] = data["metadata"]["all_joints_names"] + params["num_joints"] = len(params["joint_names"]) + params["paf_graph"] = data["metadata"]["PAFgraph"] + params["paf"] = data["metadata"].get("PAFinds", np.arange(len(params["joint_names"]))) + params["bpts"] = params["ibpts"] = range(params["num_joints"]) + params["imnames"] = [fn for fn in list(data) if fn != "metadata"] + return params + + def to_h5(self, output_name): + data = np.full( + ( + len(self.metadata["imnames"]), + self.max_n_individuals, + self.n_multibodyparts, + 4, + ), + fill_value=np.nan, + ) + for ind, assemblies in self.assemblies.items(): + for n, assembly in enumerate(assemblies): + data[ind, n] = assembly.data + index = pd.MultiIndex.from_product( + [ + ["scorer"], + map(str, range(self.max_n_individuals)), + map(str, range(self.n_multibodyparts)), + ["x", "y", "likelihood"], + ], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + temp = data[..., :3].reshape((data.shape[0], -1)) + df = pd.DataFrame(temp, columns=index) + df.to_hdf(output_name, key="ass") + + def to_pickle(self, output_name): + data = dict() + for ind, assemblies in self.assemblies.items(): + data[ind] = [ass.data for ass in assemblies] + if self.unique: + data["single"] = self.unique + with open(output_name, "wb") as file: + pickle.dump(data, file, pickle.HIGHEST_PROTOCOL) + + +@dataclass +class MatchedPrediction: + """A match between a prediction and a ground truth assembly. + + The ground truth assembly should be None f the prediction was not matched to any GT, + and the OKS should be 0. + + Attributes: + prediction: A prediction made by a pose model. + score: The confidence score for the prediction. + ground_truth: If None, then this prediction is not matched to any ground truth + (this can happen when there are more predicted individuals than GT). + Otherwise, the ground truth assembly to which this prediction is matched. + oks: The OKS score between the prediction and the ground truth pose. + """ + + prediction: Assembly + score: float + ground_truth: Assembly | None + oks: float + + +def calc_object_keypoint_similarity( + xy_pred, + xy_true, + sigma, + margin=0, + symmetric_kpts=None, +): + visible_gt = ~np.isnan(xy_true).all(axis=1) + if visible_gt.sum() < 2: # At least 2 points needed to calculate scale + return np.nan + + true = xy_true[visible_gt] + scale_squared = np.prod(np.ptp(true, axis=0) + np.spacing(1) + margin * 2) + if np.isclose(scale_squared, 0): + return np.nan + + k_squared = (2 * sigma) ** 2 + denom = 2 * scale_squared * k_squared + if isinstance(sigma, np.ndarray): + denom = denom[visible_gt] + + if symmetric_kpts is None: + pred = xy_pred[visible_gt] + pred[np.isnan(pred)] = np.inf + dist_squared = np.sum((pred - true) ** 2, axis=1) + oks = np.exp(-dist_squared / denom) + return np.mean(oks) + else: + oks = [] + xy_preds = [xy_pred] + combos = (pair for l in range(len(symmetric_kpts)) for pair in itertools.combinations(symmetric_kpts, l + 1)) + for pairs in combos: + # Swap corresponding keypoints + tmp = xy_pred.copy() + for pair in pairs: + tmp[pair, :] = tmp[pair[::-1], :] + xy_preds.append(tmp) + for xy_pred in xy_preds: + pred = xy_pred[visible_gt] + pred[np.isnan(pred)] = np.inf + dist_squared = np.sum((pred - true) ** 2, axis=1) + oks.append(np.mean(np.exp(-dist_squared / denom))) + return max(oks) + + +def match_assemblies( + predictions: list[Assembly], + ground_truth: list[Assembly], + sigma: float, + margin: int = 0, + symmetric_kpts: list[tuple[int, int]] | None = None, + greedy_matching: bool = False, + greedy_oks_threshold: float = 0.0, +) -> tuple[int, list[MatchedPrediction]]: + """Matches assemblies to ground truth predictions. + + Returns: + int: the total number of valid ground truth assemblies + list[MatchedPrediction]: a list containing all valid predictions, potentially + matched to ground truth assemblies. + """ + # Only consider assemblies of at least two keypoints + predictions = [a for a in predictions if len(a) > 1] + ground_truth = [a for a in ground_truth if len(a) > 1] + num_ground_truth = len(ground_truth) + + # Sort predictions by score + inds_pred = np.argsort([ins.affinity if ins.n_links else ins.confidence for ins in predictions])[::-1] + predictions = np.asarray(predictions)[inds_pred] + + # indices of unmatched ground truth assemblies + matched = [ + MatchedPrediction( + prediction=p, + score=(p.affinity if p.n_links else p.confidence), + ground_truth=None, + oks=0.0, + ) + for p in predictions + ] + + # Greedy assembly matching like in pycocotools + if greedy_matching: + matched_gt_indices = set() + for idx, pred in enumerate(predictions): + oks = [ + calc_object_keypoint_similarity( + pred.xy, + gt.xy, + sigma, + margin, + symmetric_kpts, + ) + for gt in ground_truth + ] + if np.all(np.isnan(oks)): + continue + + ind_best = np.nanargmax(oks) + + # if this gt already matched, and not a crowd, continue + if ind_best in matched_gt_indices: + continue + + # Only match the pred to the GT if the OKS value is above a given threshold + if oks[ind_best] < greedy_oks_threshold: + continue + + matched_gt_indices.add(ind_best) + matched[idx].ground_truth = ground_truth[ind_best] + matched[idx].oks = oks[ind_best] + + # Global rather than greedy assembly matching + else: + inds_true = list(range(len(ground_truth))) + mat = np.zeros((len(predictions), len(ground_truth))) + for i, a_pred in enumerate(predictions): + for j, a_true in enumerate(ground_truth): + oks = calc_object_keypoint_similarity( + a_pred.xy, + a_true.xy, + sigma, + margin, + symmetric_kpts, + ) + if ~np.isnan(oks): + mat[i, j] = oks + rows, cols = linear_sum_assignment(mat, maximize=True) + for row, col in zip(rows, cols, strict=False): + matched[row].ground_truth = ground_truth[col] + matched[row].oks = mat[row, col] + _ = inds_true.remove(col) + + return num_ground_truth, matched + + +def parse_ground_truth_data_file(h5_file): + df = pd.read_hdf(h5_file) + try: + df.drop("single", axis=1, level="individuals", inplace=True) + except KeyError: + pass + # Cast columns of dtype 'object' to float to avoid TypeError + # further down in _parse_ground_truth_data. + cols = df.select_dtypes(include="object").columns + if cols.to_list(): + df[cols] = df[cols].astype("float") + n_individuals = len(df.columns.get_level_values("individuals").unique()) + n_bodyparts = len(df.columns.get_level_values("bodyparts").unique()) + data = df.to_numpy().reshape((df.shape[0], n_individuals, n_bodyparts, -1)) + return _parse_ground_truth_data(data) + + +def _parse_ground_truth_data(data): + gt = dict() + for i, arr in enumerate(data): + temp = [] + for row in arr: + if np.isnan(row[:, :2]).all(): + continue + ass = Assembly.from_array(row) + temp.append(ass) + if not temp: + continue + gt[i] = temp + return gt + + +def find_outlier_assemblies(dict_of_assemblies, criterion="area", qs=(5, 95)): + if not hasattr(Assembly, criterion): + raise ValueError(f"Invalid criterion {criterion}.") + + if len(qs) != 2: + raise ValueError("Two percentiles (for lower and upper bounds) should be given.") + + tuples = [] + for frame_ind, assemblies in dict_of_assemblies.items(): + for assembly in assemblies: + tuples.append((frame_ind, getattr(assembly, criterion))) + frame_inds, vals = zip(*tuples, strict=False) + vals = np.asarray(vals) + lo, up = np.percentile(vals, qs, interpolation="nearest") + inds = np.flatnonzero((vals < lo) | (vals > up)).tolist() + return list(set(frame_inds[i] for i in inds)) + + +def _compute_precision_and_recall( + num_gt_assemblies: int, + oks_values: np.ndarray, + oks_threshold: float, + recall_thresholds: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """Computes the precision and recall scores at a given OKS threshold. + + Args: + num_gt_assemblies: the number of ground truth assemblies (used to compute false + negatives + true positives). + oks_values: the OKS value to the matched GT assembly for each prediction + oks_threshold: the OKS threshold at which recall and precision are being + computed + recall_thresholds: the recall thresholds to use to compute scores + + Returns: + The precision and recall arrays at each recall threshold + """ + tp = np.cumsum(oks_values >= oks_threshold) + fp = np.cumsum(oks_values < oks_threshold) + rc = tp / num_gt_assemblies + pr = tp / (fp + tp + np.spacing(1)) + recall = rc[-1] + + # Guarantee precision decreases monotonically, see + # https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173 + for i in range(len(pr) - 1, 0, -1): + if pr[i] > pr[i - 1]: + pr[i - 1] = pr[i] + + inds_rc = np.searchsorted(rc, recall_thresholds, side="left") + precision = np.zeros(inds_rc.shape) + valid = inds_rc < len(pr) + precision[valid] = pr[inds_rc[valid]] + return precision, recall + + +def evaluate_assembly_greedy( + assemblies_gt: dict[Any, list[Assembly]], + assemblies_pred: dict[Any, list[Assembly]], + oks_sigma: float, + oks_thresholds: Iterable[float], + margin: int | float = 0, + symmetric_kpts: list[tuple[int, int]] | None = None, +) -> dict: + """Runs greedy mAP evaluation, as done by pycocotools. + + Args: + assemblies_gt: A dictionary mapping image ID (e.g. filepath) to ground truth + assemblies. Should contain all the same keys as ``assemblies_pred``. + assemblies_pred: A dictionary mapping image ID (e.g. filepath) to predicted + assemblies. Should contain all the same keys as ``assemblies_gt``. + oks_sigma: The sigma to use to compute OKS values for keypoints . + oks_thresholds: The OKS thresholds at which to compute precision & recall. + margin: The margin to use to compute bounding boxes from keypoints. + symmetric_kpts: The symmetric keypoints in the dataset. + """ + recall_thresholds = np.linspace( # np.linspace(0, 1, 101) + start=0.0, stop=1.00, num=int(np.round((1.00 - 0.0) / 0.01)) + 1, endpoint=True + ) + precisions = [] + recalls = [] + for oks_t in oks_thresholds: + all_matched = [] + total_gt_assemblies = 0 + for ind, gt_assembly in assemblies_gt.items(): + pred_assemblies = assemblies_pred.get(ind, []) + num_gt_assemblies, matched = match_assemblies( + pred_assemblies, + gt_assembly, + oks_sigma, + margin, + symmetric_kpts, + greedy_matching=True, + greedy_oks_threshold=oks_t, + ) + all_matched.extend(matched) + total_gt_assemblies += num_gt_assemblies + + if len(all_matched) == 0: + precisions.append(0.0) + recalls.append(0.0) + continue + + # Global sort of assemblies (across all images) by score + scores = np.asarray([-m.score for m in all_matched]) + sorted_pred_indices = np.argsort(scores, kind="mergesort") + oks = np.asarray([match.oks for match in all_matched])[sorted_pred_indices] + + # Compute prediction and recall + p, r = _compute_precision_and_recall(total_gt_assemblies, oks, oks_t, recall_thresholds) + precisions.append(p) + recalls.append(r) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "precisions": precisions, + "recalls": recalls, + "mAP": precisions.mean(), + "mAR": recalls.mean(), + } + + +def evaluate_assembly( + ass_pred_dict, + ass_true_dict, + oks_sigma=0.072, + oks_thresholds=None, + margin=0, + symmetric_kpts=None, + greedy_matching=False, + with_tqdm: bool = True, +): + if oks_thresholds is None: + oks_thresholds = np.linspace(0.5, 0.95, 10) + if greedy_matching: + return evaluate_assembly_greedy( + ass_true_dict, + ass_pred_dict, + oks_sigma=oks_sigma, + oks_thresholds=oks_thresholds, + margin=margin, + symmetric_kpts=symmetric_kpts, + ) + + # sigma is taken as the median of all COCO keypoint standard deviations + all_matched = [] + total_gt_assemblies = 0 + + gt_assemblies = ass_true_dict.items() + if with_tqdm: + gt_assemblies = tqdm(gt_assemblies) + + for ind, gt_assembly in gt_assemblies: + pred_assemblies = ass_pred_dict.get(ind, []) + num_gt, matched = match_assemblies( + pred_assemblies, + gt_assembly, + oks_sigma, + margin, + symmetric_kpts, + greedy_matching, + ) + all_matched.extend(matched) + total_gt_assemblies += num_gt + + if not all_matched: + return { + "precisions": np.array([]), + "recalls": np.array([]), + "mAP": 0.0, + "mAR": 0.0, + } + + conf_pred = np.asarray([match.score for match in all_matched]) + idx = np.argsort(-conf_pred, kind="mergesort") + # Sort matching score (OKS) in descending order of assembly affinity + oks = np.asarray([match.oks for match in all_matched])[idx] + recall_thresholds = np.linspace(0, 1, 101) + precisions = [] + recalls = [] + for t in oks_thresholds: + p, r = _compute_precision_and_recall(total_gt_assemblies, oks, t, recall_thresholds) + precisions.append(p) + recalls.append(r) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "precisions": precisions, + "recalls": recalls, + "mAP": precisions.mean(), + "mAR": recalls.mean(), + } diff --git a/deeplabcut/core/metrics/__init__.py b/deeplabcut/core/metrics/__init__.py new file mode 100644 index 0000000000..94397de57a --- /dev/null +++ b/deeplabcut/core/metrics/__init__.py @@ -0,0 +1,13 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from .api import compute_metrics, prepare_evaluation_data +from .bbox import compute_bbox_metrics +from .identity import compute_identity_scores diff --git a/deeplabcut/core/metrics/api.py b/deeplabcut/core/metrics/api.py new file mode 100644 index 0000000000..a00fc617b9 --- /dev/null +++ b/deeplabcut/core/metrics/api.py @@ -0,0 +1,179 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""API methods to get metrics for deep learning models.""" + +from __future__ import annotations + +import numpy as np + +import deeplabcut.core.metrics.distance_metrics as distance_metrics + + +def compute_metrics( + ground_truth: dict[str, np.ndarray], + predictions: dict[str, np.ndarray], + single_animal: bool = False, + unique_bodypart_gt: dict[str, np.ndarray] | None = None, + unique_bodypart_poses: dict[str, np.ndarray] | None = None, + pcutoff: float = -1, + oks_bbox_margin: int = 0, + oks_sigma: float | np.ndarray = 0.1, + per_keypoint_rmse: bool = False, + compute_detection_rmse: bool = True, +) -> dict: + """Computes pose estimation performance metrics. + + Given ground truth pose labels and predictions on a dataset, computes RMSE and pose + mAP/mAR using OKS. + + The image paths in the ground_truth dict must be the same as the ones in the + predictions dict. + + Single animal RMSE is computed by simply calculating the Euclidean distance between + each ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. OKS (or object keypoint similarity) is + a similarity metric for keypoints (you can read more about it and its definition + here: https://cocodataset.org/#keypoints-eval). RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + greater than a small threshold. Predictions that cannot be matched to any ground + truth with non-zero OKS are not used to compute RMSE. + + Args: + ground_truth: The ground truth pose for which to compute metrics in the dataset. + This should be a dictionary mapping strings (image UIDs, such as image + paths) to ground truth pose for the image. The pose arrays should be + in the format (num_individuals, num_bodyparts, 3), where the 3 values are + x, y and visibility. The ``num_individuals`` corresponds to the number of + individuals labeled in each image. + predictions: The predicted poses for which to compute metrics in the dataset. + This should be a dictionary mapping strings (image UIDs, such as image + paths) to pose predictions for the image. The pose arrays should be + in the format (num_predictions, num_bodyparts, 3), where the 3 values are + x, y and score. The number of predictions can be different to the number of + ground truth individuals labeled for an image. + single_animal: Whether the metrics are being computed on a single-animal or + multi-animal dataset. This has an impact on RMSE computation. + unique_bodypart_gt: If unique bodyparts are defined for the dataset, they should + be contained in this dict in the same format as the ``ground_truth`` dict. + unique_bodypart_poses: If unique bodyparts are defined for the dataset, the + predictions should be contained in this dict in the same format as the + ``predictions`` dict. + pcutoff: The threshold to compute the "rmse_cutoff" score (RMSE of all + predictions with score above the cutoff). + oks_bbox_margin: The margin to add around keypoints to compute the area for OKS + computation. + oks_sigma: The OKS sigma to use to compute pose. + per_keypoint_rmse: Compute per-keypoint RMSE values. + compute_detection_rmse: Computes detection RMSE (without animal assembly) if the + predictions are from a multi-animal model. + + Returns: + A dictionary containing keys "rmse", "rmse_cutoff", "mAP" and "mAR" mapping + to those metrics on the given dataset. + + If unique bodyparts are given, two extra keys "rmse_unique_bodyparts" and + "rmse_pcutoff_unique_bodyparts" are also returned, containing the metrics for + the unique bodyparts head. + + If `per_keypoint_evaluation=True`, "keypoint_rmse", "keypoint_rmse_cutoff" (and + optionally "unique_keypoint_rmse" and "unique_keypoint_rmse_cutoff") keys are + added, containing a list of floats representing the RMSE for each keypoint. + + Examples: + >>> # Define the p-cutoff, prediction, and target DataFrames + >>> pcutoff = 0.5 + >>> ground_truth = {"img0": np.array([[[1.0, 1.0, 2.0], ...], ...]), ...} + >>> predictions = {"img0": np.array([[[2.0, 1.0, 0.4], ...], ...]), ...} + >>> scores = compute_metrics(ground_truth, predictions, pcutoff=pcutoff) + >>> print(scores) + { + "rmse": 1.0, + "rmse_pcutoff": 0.0, + 'mAP': 84.2, + 'mAR': 74.5 + } # Sample output scores + """ + data = prepare_evaluation_data(ground_truth, predictions) + oks_scores = distance_metrics.compute_oks( + data=data, + oks_sigma=oks_sigma, + oks_bbox_margin=oks_bbox_margin, + ) + + data_unique = None + if unique_bodypart_gt is not None: + assert unique_bodypart_poses is not None + data_unique = prepare_evaluation_data(unique_bodypart_gt, unique_bodypart_poses) + + rmse_scores = distance_metrics.compute_rmse( + data, + single_animal, + pcutoff, + data_unique=data_unique, + per_keypoint_results=per_keypoint_rmse, + ) + results = dict(**rmse_scores, **oks_scores) + + if compute_detection_rmse and not single_animal: + det_rmse, det_rmse_p = distance_metrics.compute_detection_rmse( + data, + pcutoff, + data_unique=data_unique, + ) + results["rmse_detections"] = det_rmse + results["rmse_detections_pcutoff"] = det_rmse_p + + return results + + +def prepare_evaluation_data( + ground_truth: dict[str, np.ndarray], + predictions: dict[str, np.ndarray], +) -> list[tuple[np.ndarray, np.ndarray]]: + """Prepares predictions and ground truth pose to compute metrics. + + Only keeps ground truth and predicted assemblies with at least 2 valid keypoints. + Sets the coordinates for all keypoints that aren't visible (for ground truth, + visibility <= 0 and for predictions score <= 0) to ``np.nan``. + + Sorts valid predictions by score. + + Args: + ground_truth: For each image, the GT of shape (n_idv, n_bpt, 3). + predictions: For each image, the pose predictions of shape (n_pred, n_bpt, 3). + + Returns: + A list containing (ground truth pose, predicted pose) for each image in the + dataset, where the predicted pose is sorted from highest to lowest score. + """ + pose_data = [] + for image, gt in ground_truth.items(): + gt = gt.copy() + gt[gt[..., 2] <= 0] = np.nan + + # only keep ground truth pose with at least one keypoint + gt_mask = np.any(np.all(~np.isnan(gt), axis=-1), axis=-1) + gt = gt[gt_mask] + + pred = predictions[image][..., :3].copy() # PAF have 5 values; keep xy + score + pred[pred[..., 2] < 0] = np.nan + + # only keep predicted pose with at least two keypoints + pred_mask = np.any(np.all(~np.isnan(pred), axis=-1), axis=-1) + pred = pred[pred_mask] + + scores = np.nanmean(pred[:, :, 2], axis=-1) + pred_order = np.argsort(-scores, kind="mergesort") + pose_data.append((gt, pred[pred_order])) + + return pose_data diff --git a/deeplabcut/core/metrics/bbox.py b/deeplabcut/core/metrics/bbox.py new file mode 100644 index 0000000000..5cc7a85e76 --- /dev/null +++ b/deeplabcut/core/metrics/bbox.py @@ -0,0 +1,166 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Bounding box metrics. + +Metrics are currently computed using pycocotools, which can be installed with `pypi` +(see https://github.com/ppwwyyxx/cocoapi/tree/master). +""" + +from __future__ import annotations + +from datetime import datetime +from unittest.mock import Mock, patch + +import numpy as np + +try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + with_pycocotools = True +except ModuleNotFoundError: + with_pycocotools = False + + +@patch("pycocotools.coco.print", Mock()) +@patch("pycocotools.cocoeval.print", Mock()) +def compute_bbox_metrics( + ground_truth: dict[str, dict], + detections: dict[str, dict], +) -> dict[str, float]: + """Computes bbox mAP and mAR metrics for bounding boxes. + + Args: + ground_truth: A dictionary mapping image UIDs (such as image paths or filenames) + to a ground truth labels dict. The labels dict should contain the keys + "width" (image width), "height" (image height) and "bboxes" (a numpy array + of shape (num_gt_bboxes, 4) containing the ground truth bounding boxes in + format xywh). + detections: A dictionary mapping image UIDs (such as image paths or filenames) + to a predicted bounding box dict. The detections dict should contain the + keys "bboxes" (a numpy array of shape (num_detected_bboxes, 4) containing + the predicted bounding boxes in format xywh) and "scores" (a numpy array of + length num_detected_bboxes containing the confidence score for each + predicted bounding box). + + Returns: + The bounding box mAP/mAR metrics in a dictionary. + + Raises: + ModuleNotFoundError: if ``pycocotools`` is not installed + ValueError: if there are mismatches in the keys of ground_truth and detections + """ + if not with_pycocotools: + raise ModuleNotFoundError("pycocotools not installed! can't compute bbox mAP") + + if len(detections) != len(ground_truth): + raise ValueError() + + coco = COCO() + coco.dataset["annotations"] = [] + coco.dataset["categories"] = [{"id": 1, "name": "animals", "supercategory": "obj"}] + coco.dataset["images"] = [] + coco.dataset["info"] = { + "description": "Generated by DeepLabCut", + "year": datetime.now().year, + "date_created": datetime.now().strftime("%Y-%m-%d"), + } + predictions = [] + for idx, (img, gt) in enumerate(ground_truth.items()): + img_id = idx + 1 + coco.dataset["images"].append( + { + "id": img_id, + "file_name": img, + "width": gt["width"], + "height": gt["height"], + } + ) + for bbox in gt["bboxes"][:, :4]: + ann_id = len(coco.dataset["annotations"]) + 1 + coco.dataset["annotations"].append( + { + "id": ann_id, + "image_id": img_id, + "category_id": 1, + "area": max(1, (bbox[2] * bbox[3]).item()), + "bbox": bbox, + "iscrowd": 0, + } + ) + + for bbox, score in zip(detections[img]["bboxes"], detections[img]["scores"], strict=False): + predictions.append(np.array([img_id, *bbox, score, 1])) + + if len(predictions) == 0: + return { + "mAP@50:95": 0.0, + "mAP@50": 0.0, + "mAP@75": 0.0, + "mAR@50:95": 0.0, + "mAR@50": 0.0, + "mAR@75": 0.0, + } + + predictions = np.stack(predictions, axis=0) + coco.createIndex() + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="bbox") + coco_eval.evaluate() + coco_eval.accumulate() + return { + name: val + for name, val in [ + _get_metric(coco_eval, recall=False), + _get_metric(coco_eval, recall=False, iou_threshold=0.5), + _get_metric(coco_eval, recall=False, iou_threshold=0.75), + _get_metric(coco_eval, recall=True), + _get_metric(coco_eval, recall=True, iou_threshold=0.5), + _get_metric(coco_eval, recall=True, iou_threshold=0.75), + ] + } + + +def _get_metric( + coco_eval: COCOeval, + recall: bool = False, + iou_threshold: float | None = None, + area_rng: str = "all", + max_dets: int = 100, +) -> tuple[str, float]: + metric_name = "mAR" if recall else "mAP" + if iou_threshold is not None: + thresh = f"{int(100 * iou_threshold)}" + else: + low, high = coco_eval.params.iouThrs[0], coco_eval.params.iouThrs[-1] + thresh = f"{int(100 * low)}:{int(100 * high)}" + + aind = [i for i, aRng in enumerate(coco_eval.params.areaRngLbl) if aRng == area_rng] + mind = [i for i, mDet in enumerate(coco_eval.params.maxDets) if mDet == max_dets] + if recall: + s = coco_eval.eval["recall"] + if iou_threshold is not None: + t = np.where(iou_threshold == coco_eval.params.iouThrs)[0] + s = s[t] + s = s[:, :, aind, mind] + else: + s = coco_eval.eval["precision"] + if iou_threshold is not None: + t = np.where(iou_threshold == coco_eval.params.iouThrs)[0] + s = s[t] + s = s[:, :, :, aind, mind] + + if len(s[s > -1]) == 0: + mean_s = -1 + else: + mean_s = 100 * np.mean(s[s > -1]).item() + + return f"{metric_name}@{thresh}", mean_s diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py new file mode 100644 index 0000000000..78d3f00573 --- /dev/null +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -0,0 +1,463 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Implementations of methods to compute distance metrics such as RMSE or OKS.""" + +from __future__ import annotations + +import numpy as np + +import deeplabcut.core.metrics.matching as matching +from deeplabcut.core.crossvalutils import find_closest_neighbors +from deeplabcut.core.inferenceutils import calc_object_keypoint_similarity + + +def compute_oks_matrix( + ground_truth: np.ndarray, + predictions: np.ndarray, + oks_sigma: float | np.ndarray, + oks_bbox_margin: float = 0.0, +) -> np.ndarray: + """Computes the OKS score for each (prediction, gt) pair in an image. + + Args: + ground_truth: The GT poses for an image, shape (n_individuals, n_kpts, 2) + predictions: The predicted poses in the image, shape (n_pred, n_kpts, 2) + oks_sigma: The sigma value to use to compute OKS + oks_bbox_margin: The margin to add around keypoints when computing the area. + FIXME(niels) We should allow the use of ground truth bboxes to get area + + Returns: + A matrix of shape (n_pred, n_kpts) where entry (i, j) is the OKS between + prediction i and ground truth j. + """ + oks_matrix = np.zeros((len(predictions), len(ground_truth))) + for pred_idx, pred in enumerate(predictions): + for gt_idx, gt in enumerate(ground_truth): + oks_matrix[pred_idx, gt_idx] = calc_object_keypoint_similarity( + pred[:, :2], + gt[:, :2], + sigma=oks_sigma, + margin=oks_bbox_margin, + ) + + return oks_matrix + + +def compute_oks( + data: list[tuple[np.ndarray, np.ndarray]], + oks_bbox_margin: float = 0.0, + oks_sigma: float | np.ndarray = 0.1, + oks_thresholds: np.ndarray | None = None, + oks_recall_thresholds: np.ndarray | None = None, +) -> dict[str, float]: + """Computes the OKS for pose at different thresholds. + + Args: + data: The data for which to compute OKS mAP: a list containing (gt_poses, + predicted_poses) tuples, where gt_pose is an array of shape + (num_gt_individuals, num_bpts, 3) and predicted_poses is an array of shape + (num_predictions, num_bpts, 3). For the GT, the 3 coordinates are (x, y, + visibility) while for the pose they are (x, y, confidence score). + oks_sigma: The OKS sigma to use to compute pose. + oks_bbox_margin: The margin to add around keypoints to compute the area for OKS + computation. + oks_thresholds: The OKS thresholds at which to compute AP. If None, defaults to + (0.5, 0.55, 0.6, ..., 0.9, 0.95). + oks_recall_thresholds: The recall thresholds to use to compute mAP. If None, + defaults to the same default values used in pycocotools. + + Returns: + A dictionary containing mAP and mAR scores. + """ + if oks_thresholds is None: + oks_thresholds = np.linspace(0.5, 0.95, 10) + + if oks_recall_thresholds is None: + oks_recall_thresholds = np.linspace( + start=0.0, + stop=1.00, + num=int(np.round((1.00 - 0.0) / 0.01)) + 1, + endpoint=True, + ) + + total_gt = 0 + pose_data = [] + for gt, pred in data: + # filter data to only keep individuals with at least 2 valid keypoints + gt = gt[np.sum(np.all(~np.isnan(gt), axis=-1), axis=-1) > 1] + pred = pred[np.sum(np.all(~np.isnan(pred), axis=-1), axis=-1) > 1] + + oks_matrix = compute_oks_matrix( + gt[:, :, :2], + pred[:, :, :2], + oks_sigma=oks_sigma, + oks_bbox_margin=oks_bbox_margin, + ) + + total_gt += len(gt) + pose_data.append((gt, pred, oks_matrix)) + + precisions, recalls = [], [] + for oks_threshold in oks_thresholds: + matches = [] + for gt, pred, oks_matrix in pose_data: + image_matches = matching.match_greedy_oks( + gt, + pred, + oks_matrix=oks_matrix, + oks_threshold=oks_threshold, + ) + matches.extend(image_matches) + + if len(matches) == 0: # no predictions -> precision 0, recall 0 + return {"mAP": 0, "mAR": 0} + + scores = np.asarray([m.score for m in matches]) + match_order = np.argsort(-scores, kind="mergesort") + oks_values = np.asarray([m.oks for m in matches]) + oks_values = oks_values[match_order] + + tp = np.cumsum(oks_values >= oks_threshold) + fp = np.cumsum(oks_values < oks_threshold) + rc = tp / total_gt + pr = tp / (fp + tp + np.spacing(1)) + recall = rc[-1] + + # Guarantee precision decreases monotonically, see + # https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173 + for i in range(len(pr) - 1, 0, -1): + if pr[i] > pr[i - 1]: + pr[i - 1] = pr[i] + + inds_rc = np.searchsorted(rc, oks_recall_thresholds, side="left") + precision = np.zeros(inds_rc.shape) + valid = inds_rc < len(pr) + precision[valid] = pr[inds_rc[valid]] + + precisions.append(precision) + recalls.append(recall) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "mAP": 100 * precisions.mean().item(), + "mAR": 100 * recalls.mean().item(), + } + + +def match_predictions_for_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + single_animal: bool, + oks_bbox_margin: float = 0.0, +) -> list[matching.PotentialMatch]: + """Matches GT keypoints to predictions to compute RMSE. + + Single animal RMSE is computed by simply calculating the distance between each + ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + non-zero (greater than a small threshold). Predictions that cannot be matched to + any ground truth with non-zero OKS are not used to compute RMSE. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + single_animal: Whether this is a single animal dataset. + oks_bbox_margin: When single_animal is False, predictions are matched to GT + using OKS. This is the margin used to apply when computing the bbox from + the pose to compute OKS. + + Returns: + A list containing the predictions matched to ground truth. + + Raises: + ValueError: If `single_animal=True` but more than one ground truth/predicted + keypoint is found for an entry + """ + matches = [] + for gt, pred in data: + if single_animal: + if gt.shape[0] > 1 or pred.shape[0] > 1: + raise ValueError( + "At most 1 individual and 1 prediction can be given when computing " + f"single animal RMSE. Found gt={gt.shape}, pred={pred.shape}" + ) + + image_matches = [] + if gt.shape[0] == 1 and pred.shape[0] == 1: + match = matching.PotentialMatch.from_pose(pred[0]) + match.match(gt[0], oks=float("nan")) # OKS not needed for RMSE + image_matches.append(match) + else: + oks_matrix = compute_oks_matrix( + gt[:, :, :2], + pred[:, :, :2], + oks_sigma=0.1, + oks_bbox_margin=oks_bbox_margin, + ) + image_matches = matching.match_greedy_oks( + gt, + pred, + oks_matrix=oks_matrix, + oks_threshold=1e-6, + ) + + matches.extend(image_matches) + + return matches + + +def compute_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + single_animal: bool, + pcutoff: float | list[float], + data_unique: list[tuple[np.ndarray, np.ndarray]] | None = None, + per_keypoint_results: bool = False, + oks_bbox_margin: float = 0.0, +) -> dict[str, float]: + """Computes the RMSE for pose predictions. + + Single animal RMSE is computed by simply calculating the distance between each + ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + non-zero (greater than a small threshold). Predictions that cannot be matched to + any ground truth with non-zero OKS are not used to compute RMSE. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + single_animal: Whether this is a single animal dataset. + pcutoff: The p-cutoff to use to compute RMSE. If a list, the cutoff for each + bodypart is set individually. The list must have length num_bodyparts + + num_unique_bodyparts. + data_unique: Unique bodypart ground truth and predictions to include in RMSE + computations, if there are any such bodyparts. + per_keypoint_results: Whether to compute the RMSE for each individual keypoint. + oks_bbox_margin: When single_animal is False, predictions are matched to GT + using OKS. This is the margin used to apply when computing the bbox from + the pose to compute OKS. + + Returns: + A dictionary matching metric names to values. It will at least have "rmse" and + "rmse_cutoff" keys. If `per_keypoint_results=True` and there is at least one + non-NaN pixel error it will also contain "rmse_keypoint_X" and + "rmse_cutoff_keypoint_X" keys for each bodypart, where X is the index of the + bodypart. + + Raises: + ValueError: If `single_animal=True` but more than one ground truth/predicted + keypoint is found for an entry + """ + matches = match_predictions_for_rmse(data, single_animal, oks_bbox_margin) + pixel_errors, keypoint_scores = None, None + if len(matches) > 0: + pixel_errors = np.stack([m.pixel_errors() for m in matches]) + keypoint_scores = np.stack([m.keypoint_scores() for m in matches]) + + error, support, cutoff_error, cutoff_support = 0, 0, 0, 0 + if pixel_errors is not None: + bpt_cutoffs = pcutoff + if not isinstance(pcutoff, (int, float)): + bpt_cutoffs = pcutoff[: pixel_errors.shape[1]] + + error, support, cutoff_error, cutoff_support = collect_pixel_errors( + pixel_errors, + keypoint_scores, + bpt_cutoffs, + ) + + unique_pixel_errors, unique_keypoint_scores = None, None + if data_unique is not None: + u_matches = match_predictions_for_rmse(data_unique, single_animal=True) + if len(u_matches) > 0: + unique_pixel_errors = np.stack([m.pixel_errors() for m in u_matches]) + unique_keypoint_scores = np.stack([m.keypoint_scores() for m in u_matches]) + + bpt_cutoffs = pcutoff + if not isinstance(pcutoff, (int, float)): + bpt_cutoffs = pcutoff[-unique_pixel_errors.shape[1] :] + u_error, u_support, u_cutoff_error, u_cutoff_support = collect_pixel_errors( + unique_pixel_errors, + unique_keypoint_scores, + bpt_cutoffs, + ) + error += u_error + support += u_support + cutoff_error += u_cutoff_error + cutoff_support += u_cutoff_support + + results = dict(rmse=float("nan"), rmse_pcutoff=float("nan")) + if support > 0: + results["rmse"] = float(error / support) + if cutoff_support > 0: + results["rmse_pcutoff"] = float(cutoff_error / cutoff_support) + + if per_keypoint_results: + bodypart_errors = [("rmse_keypoint", pixel_errors)] + if unique_pixel_errors is not None: + bodypart_errors.append(("rmse_unique_keypoint", unique_pixel_errors)) + + for key_prefix, bpt_errors in bodypart_errors: + for idx, keypoint_error in enumerate(bpt_errors.T): + rmse = float("nan") + if np.any(~np.isnan(keypoint_error)): + rmse = np.nanmean(keypoint_error).item() + results[f"{key_prefix}_{idx}"] = float(rmse) + + return results + + +def compute_detection_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + pcutoff: float | list[float], + data_unique: list[tuple[np.ndarray, np.ndarray]] | None = None, +) -> tuple[float, float]: + """Computes the detection RMSE for pose predictions. + + The detection RMSE score does not take individual assemblies into account. It only + judges the performance of the detections, matching each predicted keypoint to the + closest ground truth for each bodypart. + + This is the same way multi-animal RMSE was computed in DeepLabCut 2.X. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + pcutoff: The p-cutoff to use to compute RMSE. If a list, the cutoff for each + bodypart is set individually. The list must have length num_bodyparts + + num_unique_bodyparts. + data_unique: Unique bodypart ground truth and predictions to include in RMSE + computations, if there are any such bodyparts. + + Returns: + The detection RMSE and detection RMSE after removing all detections with a + score below the pcutoff. + """ + distances = [] + distances_cutoff = [] + for image_gt, image_pred in data: + image_gt = image_gt.transpose((1, 0, 2)) # to (num_bpts, num_gt_individuals, 3) + image_pred = image_pred.transpose((1, 0, 2)) # to (num_bpts, num_pred, 3) + + for bpt_index, (bpt_gt, bpt_pred) in enumerate(zip(image_gt, image_pred, strict=False)): + # filter NaNs and invalid values + bpt_gt = bpt_gt[~np.any(np.isnan(bpt_gt), axis=1)] + bpt_pred = bpt_pred[~np.any(np.isnan(bpt_pred), axis=1)] + if len(bpt_gt) == 0 or len(bpt_pred) == 0: + continue + + if isinstance(pcutoff, (int, float)): + bpt_pcutoff = pcutoff + else: + bpt_pcutoff = pcutoff[bpt_index] + + # assignment of predicted bodyparts to ground truth + neighbors = find_closest_neighbors(bpt_gt, bpt_pred, k=3) + for gt_index, pred_index in enumerate(neighbors): + if pred_index != -1: + gt = bpt_gt[gt_index] + pred = bpt_pred[pred_index] + dist = np.linalg.norm(gt[:2] - pred[:2]) + distances.append(dist) + + score = bpt_pred[pred_index, 2] + if score >= bpt_pcutoff: + distances_cutoff.append(dist) + + if data_unique is not None: + for image_gt, image_pred in data_unique: + assert len(image_gt) <= 1 and len(image_pred) <= 1, ( + f"Unique GT an predictions must have length 0 or 1! Found {image_gt.shape}, {image_pred.shape}." + ) + + if len(image_gt) == 1 and len(image_pred) == 1: + unique_gt, unique_pred = image_gt[0], image_pred[0] + num_unique = unique_gt.shape[0] + unique_cutoffs = pcutoff + if not isinstance(pcutoff, (int, float)): + unique_cutoffs = pcutoff[-num_unique:] + + for bpt_index, (gt, pred) in enumerate(zip(unique_gt, unique_pred, strict=False)): + dist = np.linalg.norm(gt[:2] - pred[:2]) + distances.append(dist) + + score = pred[2] + if isinstance(pcutoff, (int, float)): + bpt_pcutoff = unique_cutoffs + else: + bpt_pcutoff = unique_cutoffs[bpt_index] + + if score >= bpt_pcutoff: + distances_cutoff.append(dist) + + rmse, rmse_cutoff = float("nan"), float("nan") + if len(distances) == 0: + return rmse, rmse_cutoff + + distances = np.stack(distances) + if np.any(~np.isnan(distances)): + rmse = float(np.nanmean(distances).item()) + + if len(distances_cutoff) > 0: + distances_cutoff = np.stack(distances_cutoff) + if np.any(~np.isnan(distances_cutoff)): + rmse_cutoff = float(np.nanmean(distances_cutoff).item()) + + return rmse, rmse_cutoff + + +def collect_pixel_errors( + pixel_errors: np.ndarray, + keypoint_scores: np.ndarray, + pcutoff: float, +) -> tuple[float, int, float, int]: + """Collects pixel errors for RMSE computation. + + Args: + pixel_errors: The pixel errors to collect, of shape (num_matches, num_bodyparts) + keypoint_scores: The scores corresponding to the pixel errors, of shape + (num_matches, num_bodyparts). + pcutoff: The pcutoff to use when computing cutoff RMSE. + + Returns: error, support, cutoff_error, support_cutoff + error: The sum of all pixel errors. + support: The number of valid pixel errors. + cutoff_error: The sum of all pixel errors with score > pcutoff. + support_cutoff: The number of valid pixel errors with score > pcutoff. + """ + error = 0.0 + cutoff_error = 0.0 + support = np.sum(~np.isnan(pixel_errors)).item() + support_cutoff = 0 + if support > 0: + error += np.nansum(pixel_errors).item() + + cutoff_mask = keypoint_scores >= pcutoff + cutoff_pixel_errors = pixel_errors[cutoff_mask] + support_cutoff = np.sum(~np.isnan(cutoff_pixel_errors)).item() + if support_cutoff > 0: + cutoff_error = np.nansum(cutoff_pixel_errors).item() + + return error, support, cutoff_error, support_cutoff diff --git a/deeplabcut/core/metrics/identity.py b/deeplabcut/core/metrics/identity.py new file mode 100644 index 0000000000..684b797213 --- /dev/null +++ b/deeplabcut/core/metrics/identity.py @@ -0,0 +1,91 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Implementations of methods to compute identity prediction accuracy.""" + +from __future__ import annotations + +import numpy as np +from sklearn.metrics import accuracy_score + +from deeplabcut.core.crossvalutils import find_closest_neighbors + + +def compute_identity_scores( + individuals: list[str], + bodyparts: list[str], + predictions: dict[str, np.ndarray], + identity_scores: dict[str, np.ndarray], + ground_truth: dict[str, np.ndarray], +) -> dict[str, float]: + """ + FIXME: With DLCRNet all heatmap "peaks" above 0.01 were kept, with 1 keypoint and + 1 identity score map per peak. Then, for each ground truth keypoint, we selected + the prediction closest to it, and evaluated the identity score in that position. + This is no longer the case, as we're now evaluating after assembly. So we only + have num_individuals assemblies. + + Args: + individuals: + bodyparts: + predictions: (num_assemblies, num_bodyparts, 3) + identity_scores: (num_assemblies, num_bodyparts, num_individuals) + ground_truth: (num_individuals, num_bodyparts, 3) + + Returns: + + """ + if not len(predictions) == len(ground_truth): + raise ValueError("Mismatch between number of predictions and ground truth") + + all_bpts = np.asarray(len(individuals) * bodyparts) + ids = np.full((len(predictions), len(all_bpts), 2), np.nan) + for i, (image, pred) in enumerate(predictions.items()): + for j in range(len(individuals)): + for k in range(len(bodyparts)): + bpt_idx = len(bodyparts) * j + k + ids[i, bpt_idx, 0] = j + + # set keypoints that aren't visible to NaN + gt = ground_truth[image].copy() + gt[gt[..., 2] <= 0, :2] = np.nan + gt = gt[..., :2] + + id_scores = identity_scores[image] + + # reorder to (bodypart, individual, ...) + gt = gt.transpose((1, 0, 2)) + pred = pred.transpose((1, 0, 2))[..., :2] + id_scores = id_scores.transpose((1, 0, 2)) + for bpt, bpt_gt, bpt_pred, bpt_id_scores in zip(bodyparts, gt, pred, id_scores, strict=True): + # assign ground truth keypoints to the closest prediction, so the ID score + # is the closest possible to the ID score computed with "ground truth" + indices_gt = np.flatnonzero(np.all(~np.isnan(bpt_gt), axis=1)) + + # Remove NaN predictions from the bodypart predictions + indices_pred = np.all(np.isfinite(bpt_pred), axis=1) + bpt_pred = bpt_pred[indices_pred] + bpt_id_scores = bpt_id_scores[indices_pred] + + neighbors = find_closest_neighbors(bpt_gt[indices_gt], bpt_pred, k=3) + found = neighbors != -1 + indices = np.flatnonzero(all_bpts == bpt) + # Get the predicted identity of each bodypart by taking the argmax + ids[i, indices[indices_gt[found]], 1] = np.argmax(bpt_id_scores[neighbors[found]], axis=1) + + ids = ids.reshape((len(predictions), len(individuals), len(bodyparts), 2)) + results = {} + for i, bpt in enumerate(bodyparts): + temp = ids[:, :, i].reshape((-1, 2)) + valid = np.isfinite(temp).all(axis=1) + y_true, y_pred = temp[valid].T + results[f"{bpt}_accuracy"] = accuracy_score(y_true, y_pred) + + return results diff --git a/deeplabcut/core/metrics/matching.py b/deeplabcut/core/metrics/matching.py new file mode 100644 index 0000000000..791bcae97f --- /dev/null +++ b/deeplabcut/core/metrics/matching.py @@ -0,0 +1,167 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Algorithms to match predictions to ground truth labels.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class PotentialMatch: + """A potential match between predicted pose and ground truth pose. + + Args: + pose: An array of shape (num_bodyparts, 3) + score: The score for the prediction. This could be the mean of the confidence + score for each bodypart, or another value representing how confident the + model is that this assembly is correct. + gt: None if no ground truth pose was matched to the prediction. If defined, the + ground truth to which the prediction is matched. It should be of shape + (num_bodyparts, 3), where the 3 values are x, y and visibility. + oks: The OKS score between the pose and the ground truth. + """ + + pose: np.ndarray + score: float + gt: np.ndarray | None = None + oks: float = 0.0 + + def keypoint_scores(self) -> np.ndarray: + """Returns: The confidence score for each bodypart in the predicted pose.""" + return self.pose[:, 2].copy() + + def pixel_errors(self) -> np.ndarray: + """ + Returns: + The distance (in pixels) between each predicted and ground truth bodypart. + If this prediction is unmatched, returns an array of length num_bodyparts + containing all NaNs. + """ + if self.gt is None: + return np.full(len(self.pose), np.nan) + + return np.linalg.norm(self.pose[:, :2] - self.gt[:, :2], axis=1) + + def match(self, gt: np.ndarray, oks: float) -> None: + """Adds a ground truth match to this PotentialMatch. + + Args: + gt: The ground truth to which the prediction is matched. The ground truth + pose should be of shape (num_bodyparts, 3), where the 3 values are x, y + and visibility. + oks: The OKS similarity between the ground truth and this. + """ + self.gt = gt + self.oks = oks + + @classmethod + def from_pose(cls, pose: np.ndarray) -> PotentialMatch: + assert len(pose.shape) == 2 # Must be pose for a single individual + scores = pose[:, 2] + if np.all(np.isnan(scores)): + raise ValueError(f"Cannot create a Match from a pose prediction where all scores are nan (pose={pose})") + + return PotentialMatch(pose=pose, score=np.nanmean(scores).item()) + + +def match_greedy_oks( + ground_truth: np.ndarray, + predictions: np.ndarray, + oks_matrix: np.ndarray, + oks_threshold: float = 0.0, +) -> list[PotentialMatch]: + """Greedy matching of ground truth individuals to predicted individuals using OKS. + + This is done in the same way as done in pycocotools. The predictions must be sorted + by score before being passed to this function. + + Args: + ground_truth: The ground truth labels for an image, of shape (n_idv, n_bpt, 2) + predictions: The predictions for an image, of shape (n_idv, n_bpt, 2) + oks_matrix: A matrix of shape (n_pred, n_kpts) where entry (i, j) is the OKS + between prediction i and ground truth j. + oks_threshold: The min. OKS for a prediction to be matched to a GT pose + + Returns: + A list containing a PotentialMatch for each predicted pose in the given + predictions. + """ + matches = [PotentialMatch.from_pose(pose=pred) for pred in predictions] + matched_gt_indices = set() + for idx, _pred in enumerate(predictions): + oks = oks_matrix[idx] + if np.all(np.isnan(oks)): + continue + + ind_best = np.nanargmax(oks) + + # if this gt already matched, continue + if ind_best in matched_gt_indices: + continue + + # Only match the pred to the GT if the OKS value is above a given threshold + if oks[ind_best] < oks_threshold: + continue + + matched_gt_indices.add(ind_best) + matches[idx].match(gt=ground_truth[ind_best], oks=oks[ind_best]) + + return matches + + +def match_greedy_rmse( + ground_truth: np.ndarray, + predictions: np.ndarray, + keep_assemblies: bool = True, +) -> list[PotentialMatch]: + """Greedy matching of ground truth individuals to predicted individuals using RMSE. + + The predictions must be sorted by score before being passed to this function. + + Args: + ground_truth: The ground truth labels for an image, of shape (n_idv, n_bpt, 2) + predictions: The predictions for an image, of shape (n_idv, n_bpt, 2) + keep_assemblies: Whether to match predicted keypoints to ground truth keypoints + while enforcing that all bodyparts for a predicted individual are matched + to bodyparts from the same ground truth assembly. When set to False, this + corresponds to detection RMSE score. + + Returns: + A list containing a PotentialMatch for each predicted pose in the given + predictions. + """ + if not keep_assemblies: + raise NotImplementedError() + + matches = [PotentialMatch.from_pose(pose=pred) for pred in predictions] + matched_gt_indices = set() + for idx, pred in enumerate(predictions): + bpt_distances = np.linalg.norm(pred[:, :2] - ground_truth[:, :, :2], axis=-1) + if np.all(np.isnan(bpt_distances)): + continue + + distances = np.nanmean(bpt_distances, axis=-1) + ind_best = np.nanargmin(distances) + + # if this gt already matched, continue + if ind_best in matched_gt_indices: + continue + + matched_gt_indices.add(ind_best) + matches[idx].match( + gt=ground_truth[ind_best], + oks=float("nan"), # don't compute OKS here + ) + + return matches diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py new file mode 100644 index 0000000000..75934dc26b --- /dev/null +++ b/deeplabcut/core/trackingutils.py @@ -0,0 +1,819 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +import abc +import math +import warnings +from collections import defaultdict + +import numpy as np +from filterpy.common import kinematic_kf +from filterpy.kalman import KalmanFilter +from matplotlib import patches +from numba import jit +from numba.core.errors import NumbaPerformanceWarning +from scipy.optimize import linear_sum_assignment +from scipy.stats import mode +from tqdm import tqdm + +warnings.simplefilter("ignore", category=NumbaPerformanceWarning) + +TRACK_METHODS = { + "box": "_bx", + "ctd": "_ctd", + "skeleton": "_sk", + "ellipse": "_el", + "transformer": "_tr", +} + + +def calc_iou(bbox1, bbox2): + x1 = max(bbox1[0], bbox2[0]) + y1 = max(bbox1[1], bbox2[1]) + x2 = min(bbox1[2], bbox2[2]) + y2 = min(bbox1[3], bbox2[3]) + w = max(0, x2 - x1) + h = max(0, y2 - y1) + wh = w * h + return wh / ((bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) + (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) - wh) + + +class BaseTracker: + """Base class for a constant-velocity Kalman filter-based tracker.""" + + n_trackers = 0 + + def __init__(self, dim, dim_z): + self.kf = kinematic_kf( + dim, + 1, + dim_z=dim_z, + order_by_dim=False, + ) + self.id = self.__class__.n_trackers + self.__class__.n_trackers += 1 + self.time_since_update = 0 + self.age = 0 + self.hits = 0 + self.hit_streak = 0 + + def update(self, z): + self.time_since_update = 0 + self.hits += 1 + self.hit_streak += 1 + self.kf.update(z) + + def predict(self): + self.kf.predict() + self.age += 1 + if self.time_since_update > 0: + self.hit_streak = 0 + self.time_since_update += 1 + return self.state + + @property + def state(self): + return self.kf.x.squeeze()[: self.kf.dim_z] + + @state.setter + def state(self, state): + self.kf.x[: self.kf.dim_z] = state + + +class Ellipse: + def __init__(self, x, y, width, height, theta): + self.x = x + self.y = y + self.width = width + self.height = height + self.theta = theta # in radians + self._geometry = None + + @property + def parameters(self): + return self.x, self.y, self.width, self.height, self.theta + + @property + def aspect_ratio(self): + return max(self.width, self.height) / min(self.width, self.height) + + def calc_similarity_with(self, other_ellipse): + max_dist = max(self.height, self.width, other_ellipse.height, other_ellipse.width) + dist = math.sqrt((self.x - other_ellipse.x) ** 2 + (self.y - other_ellipse.y) ** 2) + + if max_dist == 0: + max_dist = 1 + + cost1 = 1 - min(dist / max_dist, 1) + cost2 = abs(math.cos(self.theta - other_ellipse.theta)) + return 0.8 * cost1 + 0.2 * cost2 * cost1 + + def contains_points(self, xy, tol=0.1): + ca = math.cos(self.theta) + sa = math.sin(self.theta) + x_demean = xy[:, 0] - self.x + y_demean = xy[:, 1] - self.y + return ( + ((ca * x_demean + sa * y_demean) ** 2 / (0.5 * self.width) ** 2) + + ((sa * x_demean - ca * y_demean) ** 2 / (0.5 * self.height) ** 2) + ) <= 1 + tol + + def draw(self, show_axes=True, ax=None, **kwargs): + import matplotlib.pyplot as plt + from matplotlib.lines import Line2D + from matplotlib.transforms import Affine2D + + if ax is None: + ax = plt.subplot(111, aspect="equal") + el = patches.Ellipse( + xy=(self.x, self.y), + width=self.width, + height=self.height, + angle=np.rad2deg(self.theta), + **kwargs, + ) + ax.add_patch(el) + if show_axes: + major = Line2D([-self.width / 2, self.width / 2], [0, 0], lw=3, zorder=3) + minor = Line2D([0, 0], [-self.height / 2, self.height / 2], lw=3, zorder=3) + trans = Affine2D().rotate(self.theta).translate(self.x, self.y) + ax.transData + major.set_transform(trans) + minor.set_transform(trans) + ax.add_artist(major) + ax.add_artist(minor) + + +class EllipseFitter: + def __init__(self, sd=2): + self.sd = sd + self.x = None + self.y = None + self.params = None + self._coeffs = None + + def fit(self, xy): + self.x, self.y = xy[np.isfinite(xy).all(axis=1)].T + if len(self.x) < 3: + return None + if self.sd: + self.params = self._fit_error(self.x, self.y, self.sd) + else: + self._coeffs = self._fit(self.x, self.y) + self.params = self.calc_parameters(self._coeffs) + if not np.isnan(self.params).any(): + return Ellipse(*self.params) + return None + + @staticmethod + @jit(nopython=True) + def _fit(x, y): + """Least Squares ellipse fitting algorithm Fit an ellipse to a set of X- and + Y-coordinates. See Halir and Flusser, 1998 for implementation details. + + :param x: ndarray, 1D trajectory + :param y: ndarray, 1D trajectory + :return: 1D ndarray of 6 coefficients of the general quadratic curve: ax^2 + + 2bxy + cy^2 + 2dx + 2fy + g = 0 + """ + D1 = np.vstack((x * x, x * y, y * y)) + D2 = np.vstack((x, y, np.ones_like(x))) + S1 = D1 @ D1.T + S2 = D1 @ D2.T + S3 = D2 @ D2.T + T = -np.linalg.inv(S3) @ S2.T + temp = S1 + S2 @ T + M = np.zeros_like(temp) + M[0] = temp[2] * 0.5 + M[1] = -temp[1] + M[2] = temp[0] * 0.5 + E, V = np.linalg.eig(M) + cond = 4 * V[0] * V[2] - V[1] ** 2 + a1 = V[:, cond > 0][:, 0] + a2 = T @ a1 + return np.hstack((a1, a2)) + + @staticmethod + @jit(nopython=True) + def _fit_error(x, y, sd): + """Fit a sd-sigma covariance error ellipse to the data. + + :param x: ndarray, 1D input of X coordinates + :param y: ndarray, 1D input of Y coordinates + :param sd: int, size of the error ellipse in 'standard deviation' + :return: ellipse center, semi-axes length, angle to the X-axis + """ + cov = np.cov(x, y) + E, V = np.linalg.eigh(cov) # Returns the eigenvalues in ascending order + # r2 = chi2.ppf(2 * norm.cdf(sd) - 1, 2) + # height, width = np.sqrt(E * r2) + height, width = 2 * sd * np.sqrt(E) + a, b = V[:, 1] + rotation = math.atan2(b, a) % np.pi + return [np.mean(x), np.mean(y), width, height, rotation] + + @staticmethod + @jit(nopython=True) + def calc_parameters(coeffs): + """ + Calculate ellipse center coordinates, semi-axes lengths, and + the counterclockwise angle of rotation from the x-axis to the ellipse major axis. + Visit http://mathworld.wolfram.com/Ellipse.html + for how to estimate ellipse parameters. + + :param coeffs: list of fitting coefficients + :return: center: 1D ndarray, semi-axes: 1D ndarray, angle: float + """ + # The general quadratic curve has the form: + # ax^2 + 2bxy + cy^2 + 2dx + 2fy + g = 0 + a, b, c, d, f, g = coeffs + b *= 0.5 + d *= 0.5 + f *= 0.5 + + # Ellipse center coordinates + x0 = (c * d - b * f) / (b * b - a * c) + y0 = (a * f - b * d) / (b * b - a * c) + + # Semi-axes lengths + num = 2 * (a * f * f + c * d * d + g * b * b - 2 * b * d * f - a * c * g) + den1 = (b * b - a * c) * (np.sqrt((a - c) ** 2 + 4 * b * b) - (a + c)) + den2 = (b * b - a * c) * (-np.sqrt((a - c) ** 2 + 4 * b * b) - (a + c)) + major = np.sqrt(num / den1) + minor = np.sqrt(num / den2) + + # Angle to the horizontal + if b == 0: + if a < c: + phi = 0 + else: + phi = np.pi / 2 + else: + if a < c: + phi = np.arctan(2 * b / (a - c)) / 2 + else: + phi = np.pi / 2 + np.arctan(2 * b / (a - c)) / 2 + + return [x0, y0, 2 * major, 2 * minor, phi] + + +class EllipseTracker(BaseTracker): + def __init__(self, params): + super().__init__(dim=5, dim_z=5) + self.kf.R[2:, 2:] *= 10.0 + # High uncertainty to the unobservable initial velocities + self.kf.P[5:, 5:] *= 1000.0 + self.kf.P *= 10.0 + self.kf.Q[5:, 5:] *= 0.01 + self.state = params + + @BaseTracker.state.setter + def state(self, params): + state = np.asarray(params).reshape((-1, 1)) + super(EllipseTracker, type(self)).state.fset(self, state) + + +class SkeletonTracker(BaseTracker): + def __init__(self, n_bodyparts): + super().__init__(dim=n_bodyparts * 2, dim_z=n_bodyparts) + self.kf.Q[self.kf.dim_z :, self.kf.dim_z :] *= 10 + self.kf.R[self.kf.dim_z :, self.kf.dim_z :] *= 0.01 + self.kf.P[self.kf.dim_z :, self.kf.dim_z :] *= 1000 + + def update(self, pose): + flat = pose.reshape((-1, 1)) + empty = np.isnan(flat).squeeze() + if empty.any(): + H = self.kf.H.copy() + H[empty] = 0 + flat[empty] = 0 + self.kf.update(flat, H=H) + else: + super().update(flat) + + @BaseTracker.state.setter + def state(self, pose): + curr_pose = pose.copy() + empty = np.isnan(curr_pose).all(axis=1) + if empty.any(): + fill = np.nanmean(pose, axis=0) + curr_pose[empty] = fill + super(SkeletonTracker, type(self)).state.fset(self, curr_pose.reshape((-1, 1))) + + +class BoxTracker(BaseTracker): + def __init__(self, bbox): + super().__init__(dim=4, dim_z=4) + self.kf = KalmanFilter(dim_x=7, dim_z=4) + self.kf.F = np.array( + [ + [1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1], + ] + ) + self.kf.H = np.array( + [ + [1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + ] + ) + self.kf.R[2:, 2:] *= 10.0 + # Give high uncertainty to the unobservable initial velocities + self.kf.P[4:, 4:] *= 1000.0 + self.kf.P *= 10.0 + self.kf.Q[-1, -1] *= 0.01 + self.kf.Q[4:, 4:] *= 0.01 + self.state = bbox + + def update(self, bbox): + super().update(self.convert_bbox_to_z(bbox)) + + def predict(self): + if (self.kf.x[6] + self.kf.x[2]) <= 0: + self.kf.x[6] *= 0.0 + return super().predict() + + @property + def state(self): + return self.convert_x_to_bbox(self.kf.x)[0] + + @state.setter + def state(self, bbox): + state = self.convert_bbox_to_z(bbox) + super(BoxTracker, type(self)).state.fset(self, state) + + @staticmethod + def convert_x_to_bbox(x, score=None): + """Takes a bounding box in the centre form [x,y,s,r] and returns it in the form + [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right.""" + w = np.sqrt(x[2] * x[3]) + h = x[2] / w + if score is None: + return np.array([x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0]).reshape((1, 4)) + else: + return np.array([x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0, score]).reshape((1, 5)) + + @staticmethod + def convert_bbox_to_z(bbox): + """Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form + [x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is + the aspect ratio.""" + w = bbox[2] - bbox[0] + h = bbox[3] - bbox[1] + x = bbox[0] + w / 2.0 + y = bbox[1] + h / 2.0 + s = w * h # scale is just area + r = w / float(h) + return np.array([x, y, s, r]).reshape((4, 1)) + + +class SORTBase(metaclass=abc.ABCMeta): + def __init__(self): + self.n_frames = 0 + self.trackers = [] + + @abc.abstractmethod + def track(self): + pass + + +class SORTEllipse(SORTBase): + def __init__(self, max_age, min_hits, iou_threshold, sd=2): + self.max_age = max_age + self.min_hits = min_hits + self.iou_threshold = iou_threshold + self.fitter = EllipseFitter(sd) + EllipseTracker.n_trackers = 0 + super().__init__() + + def track(self, poses, identities=None): + self.n_frames += 1 + + trackers = np.zeros((len(self.trackers), 6)) + for i in range(len(trackers)): + trackers[i, :5] = self.trackers[i].predict() + empty = np.isnan(trackers).any(axis=1) + trackers = trackers[~empty] + for ind in np.flatnonzero(empty)[::-1]: + self.trackers.pop(ind) + + ellipses = [] + pred_ids = [] + for i, pose in enumerate(poses): + el = self.fitter.fit(pose) + if el is not None: + ellipses.append(el) + if identities is not None: + pred_ids.append(mode(identities[i])[0][0]) + if not len(trackers): + matches = np.empty((0, 2), dtype=int) + unmatched_detections = np.arange(len(ellipses)) + unmatched_trackers = np.empty((0, 6), dtype=int) + else: + ellipses_trackers = [Ellipse(*t[:5]) for t in trackers] + cost_matrix = np.zeros((len(ellipses), len(ellipses_trackers))) + for i, el in enumerate(ellipses): + for j, el_track in enumerate(ellipses_trackers): + cost = el.calc_similarity_with(el_track) + if identities is not None: + match = 2 if pred_ids[i] == self.trackers[j].id_ else 1 + cost *= match + cost_matrix[i, j] = cost + row_indices, col_indices = linear_sum_assignment(cost_matrix, maximize=True) + unmatched_detections = [i for i, _ in enumerate(ellipses) if i not in row_indices] + unmatched_trackers = [j for j, _ in enumerate(trackers) if j not in col_indices] + matches = [] + for row, col in zip(row_indices, col_indices, strict=False): + val = cost_matrix[row, col] + # diff = val - cost_matrix + # diff[row, col] += val + # if ( + # val < self.iou_threshold + # or np.any(diff[row] <= 0.2) + # or np.any(diff[:, col] <= 0.2) + # ): + if val < self.iou_threshold: + unmatched_detections.append(row) + unmatched_trackers.append(col) + else: + matches.append([row, col]) + if not len(matches): + matches = np.empty((0, 2), dtype=int) + else: + matches = np.stack(matches) + unmatched_trackers = np.asarray(unmatched_trackers) + unmatched_detections = np.asarray(unmatched_detections) + + animalindex = [] + for t, tracker in enumerate(self.trackers): + if t not in unmatched_trackers: + ind = matches[matches[:, 1] == t, 0][0] + animalindex.append(ind) + tracker.update(ellipses[ind].parameters) + else: + animalindex.append(-1) + + for i in unmatched_detections: + trk = EllipseTracker(ellipses[i].parameters) + if identities is not None: + trk.id_ = mode(identities[i])[0][0] + self.trackers.append(trk) + animalindex.append(i) + + i = len(self.trackers) + ret = [] + for trk in reversed(self.trackers): + d = trk.state + if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits): + ret.append( + np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape(1, -1) + ) # for DLC we also return the original animalid + # +1 as MOT benchmark requires positive >> this is removed for DLC! + i -= 1 + # remove dead tracklet + if trk.time_since_update > self.max_age: + self.trackers.pop(i) + + if len(ret) > 0: + return np.concatenate(ret) + return np.empty((0, 7)) + + +class SORTSkeleton(SORTBase): + def __init__(self, n_bodyparts, max_age=20, min_hits=3, oks_threshold=0.5): + self.n_bodyparts = n_bodyparts + self.max_age = max_age + self.min_hits = min_hits + self.oks_threshold = oks_threshold + SkeletonTracker.n_trackers = 0 + super().__init__() + + @staticmethod + def weighted_hausdorff(x, y): + # Modified from scipy source code: + # - to restrict its use to 2D + # - to get rid of shuffling (since arrays are only (nbodyparts * 3) element long) + # TODO - factor in keypoint confidence (and weight by # of observations??) + cmax = 0 + for i in range(x.shape[0]): + no_break_occurred = True + cmin = np.inf + for j in range(y.shape[0]): + d = (x[i, 0] - y[j, 0]) ** 2 + (x[i, 1] - y[j, 1]) ** 2 + if d < cmax: + no_break_occurred = False + break + if d < cmin: + cmin = d + if cmin != np.inf and cmin > cmax and no_break_occurred: + cmax = cmin + return np.sqrt(cmax) + + @staticmethod + def object_keypoint_similarity(x, y): + mask = ~np.isnan(x * y).all(axis=1) # Intersection visible keypoints + xx = x[mask] + yy = y[mask] + dist = np.linalg.norm(xx - yy, axis=1) + scale = np.sqrt(np.product(np.ptp(yy, axis=0))) # square root of bounding box area + oks = np.exp(-0.5 * (dist / (0.05 * scale)) ** 2) + return np.mean(oks) + + def calc_pairwise_hausdorff_dist(self, poses, poses_ref): + mat = np.zeros((len(poses), len(poses_ref))) + for i, pose in enumerate(poses): + for j, pose_ref in enumerate(poses_ref): + mat[i, j] = self.weighted_hausdorff(pose, pose_ref) + return mat + + def calc_pairwise_oks(self, poses, poses_ref): + mat = np.zeros((len(poses), len(poses_ref))) + for i, pose in enumerate(poses): + for j, pose_ref in enumerate(poses_ref): + mat[i, j] = self.object_keypoint_similarity(pose, pose_ref) + return mat + + def track(self, poses): + self.n_frames += 1 + + if not len(self.trackers): + for pose in poses: + tracker = SkeletonTracker(self.n_bodyparts) + tracker.state = pose + self.trackers.append(tracker) + + poses_ref = [] + for _, tracker in enumerate(self.trackers): + pose_ref = tracker.predict() + poses_ref.append(pose_ref.reshape((-1, 2))) + + # mat = self.calc_pairwise_oks(poses, poses_ref) + mat = self.calc_pairwise_hausdorff_dist(poses, poses_ref) + row_indices, col_indices = linear_sum_assignment(mat, maximize=False) + + unmatched_poses = [p for p, _ in enumerate(poses) if p not in row_indices] + unmatched_trackers = [t for t, _ in enumerate(poses_ref) if t not in col_indices] + # Remove matched detections with low OKS + # matches = [] + # for row, col in zip(row_indices, col_indices): + # if mat[row, col] < self.oks_threshold: + # unmatched_poses.append(row) + # unmatched_trackers.append(col) + # else: + # matches.append([row, col]) + # if not len(matches): + # matches = np.empty((0, 2), dtype=int) + # else: + # matches = np.stack(matches) + matches = np.c_[row_indices, col_indices] + + animalindex = [] + for t, tracker in enumerate(self.trackers): + if t not in unmatched_trackers: + ind = matches[matches[:, 1] == t, 0][0] + animalindex.append(ind) + tracker.update(poses[ind]) + else: + animalindex.append(-1) + + for i in unmatched_poses: + tracker = SkeletonTracker(self.n_bodyparts) + tracker.state = poses[i] + self.trackers.append(tracker) + animalindex.append(i) + + states = [] + i = len(self.trackers) + for tracker in reversed(self.trackers): + i -= 1 + if tracker.time_since_update > self.max_age: + self.trackers.pop() + continue + state = tracker.predict() + states.append(np.r_[state, [tracker.id, int(animalindex[i])]]) + if len(states) > 0: + return np.stack(states) + return np.empty((0, self.n_bodyparts * 2 + 2)) + + +class SORTBox(SORTBase): + def __init__(self, max_age, min_hits, iou_threshold): + self.max_age = max_age + self.min_hits = min_hits + self.iou_threshold = iou_threshold + BoxTracker.n_trackers = 0 + super().__init__() + + def track(self, dets): + self.n_frames += 1 + + trackers = np.zeros((len(self.trackers), 5)) + for i in range(len(trackers)): + trackers[i, :4] = self.trackers[i].predict() + empty = np.isnan(trackers).any(axis=1) + trackers = trackers[~empty] + for ind in np.flatnonzero(empty)[::-1]: + self.trackers.pop(ind) + + matched, unmatched_dets, unmatched_trks = self.match_detections_to_trackers(dets, trackers, self.iou_threshold) + + # update matched trackers with assigned detections + animalindex = [] + for t, trk in enumerate(self.trackers): + if t not in unmatched_trks: + d = matched[np.where(matched[:, 1] == t)[0], 0] + animalindex.append(d[0]) + trk.update(dets[d, :][0]) # update coordinates + else: + animalindex.append("nix") # lost trk! + + # create and initialise new trackers for unmatched detections + for i in unmatched_dets: + trk = BoxTracker(dets[i, :]) + self.trackers.append(trk) + animalindex.append(i) + + i = len(self.trackers) + ret = [] + for trk in reversed(self.trackers): + d = trk.state + if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits): + ret.append( + np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape(1, -1) + ) # for DLC we also return the original animalid + # +1 as MOT benchmark requires positive >> this is removed for DLC! + i -= 1 + # remove dead tracklet + if trk.time_since_update > self.max_age: + self.trackers.pop(i) + + if len(ret) > 0: + return np.concatenate(ret) + return np.empty((0, 5)) + + @staticmethod + def match_detections_to_trackers(detections, trackers, iou_threshold): + """Assigns detections to tracked object (both represented as bounding boxes) + + Returns 3 lists of matches, unmatched_detections and unmatched_trackers + """ + if not len(trackers): + return ( + np.empty((0, 2), dtype=int), + np.arange(len(detections)), + np.empty((0, 5), dtype=int), + ) + iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32) + + for d, det in enumerate(detections): + for t, trk in enumerate(trackers): + iou_matrix[d, t] = calc_iou(det, trk) + row_indices, col_indices = linear_sum_assignment(-iou_matrix) + + unmatched_detections = [] + for d, _ in enumerate(detections): + if d not in row_indices: + unmatched_detections.append(d) + unmatched_trackers = [] + for t, _ in enumerate(trackers): + if t not in col_indices: + unmatched_trackers.append(t) + + # filter out matched with low IOU + matches = [] + for row, col in zip(row_indices, col_indices, strict=False): + if iou_matrix[row, col] < iou_threshold: + unmatched_detections.append(row) + unmatched_trackers.append(col) + else: + matches.append([row, col]) + if not len(matches): + matches = np.empty((0, 2), dtype=int) + else: + matches = np.stack(matches) + return matches, np.array(unmatched_detections), np.array(unmatched_trackers) + + +def fill_tracklets(tracklets, trackers, animals, imname): + for content in trackers: + tracklet_id, pred_id = content[-2:].astype(int) + if tracklet_id not in tracklets: + tracklets[tracklet_id] = {} + if pred_id != -1: + tracklets[tracklet_id][imname] = np.asarray(animals[pred_id]) + else: # Resort to the tracker prediction + xy = np.asarray(content[:-2]) + pred = np.insert(xy, range(2, len(xy) + 1, 2), 1) + tracklets[tracklet_id][imname] = np.asarray(pred) + + +def calc_bboxes_from_keypoints(data, slack=0, offset=0): + data = np.asarray(data) + if data.shape[-1] < 3: + raise ValueError("Data should be of shape (n_animals, n_bodyparts, 3)") + + if data.ndim != 3: + data = np.expand_dims(data, axis=0) + bboxes = np.full((data.shape[0], 5), np.nan) + bboxes[:, :2] = np.nanmin(data[..., :2], axis=1) - slack # X1, Y1 + bboxes[:, 2:4] = np.nanmax(data[..., :2], axis=1) + slack # X2, Y2 + bboxes[:, -1] = np.nanmean(data[..., 2], axis=1) # Average confidence + bboxes[:, [0, 2]] += offset + return bboxes + + +def reconstruct_all_ellipses(data, sd): + """Reconstructs ellipses for multiple individuals based on their body part + coordinates across multiple frames. Each ellipse is fitted to the coordinates using + an `EllipseFitter`. + + Parameters + ---------- + data : pandas.DataFrame + A multi-level DataFrame containing body part coordinates and likelihood values. + The index represents frames, and the columns follow a multi-level structure: + - Level 0: Scorer + - Level 1: Individuals + - Level 2: Body parts + - Level 3: Coordinates ("x" and "y") and "likelihood". + sd : float + The standard deviation used by the `EllipseFitter` for fitting ellipses. + + Returns + ------- + numpy.ndarray + A 3D array of shape (A, F, 5), where: + - A is the number of individuals (excluding "single" if present). + - F is the number of frames. + - Each row contains ellipse parameters [cx, cy, width, height, angle]. + + Notes + ----- + - The method drops the "likelihood" column from the input DataFrame as it is not + relevant for ellipse fitting. + - If the "single" individual is present, it is excluded from the reconstruction process. + - The `EllipseFitter` is used to fit ellipses to the body part coordinates for each + individual in each frame. + - NaN values are assigned when no valid ellipse can be fitted. + """ + xy = data.droplevel("scorer", axis=1).drop("likelihood", axis=1, level=-1) + if "single" in xy: + xy.drop("single", axis=1, level="individuals", inplace=True) + animals = xy.columns.get_level_values("individuals").unique() + nrows = xy.shape[0] + ellipses = np.full((len(animals), nrows, 5), np.nan) + fitter = EllipseFitter(sd) + for n, animal in enumerate(animals): + data = xy.xs(animal, axis=1, level="individuals").values.reshape((nrows, -1, 2)) + for i, coords in enumerate(tqdm(data)): + el = fitter.fit(coords.astype(np.float64)) + if el is not None: + ellipses[n, i] = el.parameters + return ellipses + + +def _track_individuals(individuals, min_hits=1, max_age=5, similarity_threshold=0.6, track_method="ellipse"): + if track_method not in TRACK_METHODS: + raise ValueError(f"Unknown {track_method} tracker.") + + if track_method == "ellipse": + tracker = SORTEllipse(max_age, min_hits, similarity_threshold) + elif track_method == "box": + tracker = SORTBox(max_age, min_hits, similarity_threshold) + else: + n_bodyparts = individuals[0][0].shape[0] + tracker = SORTSkeleton(n_bodyparts, max_age, min_hits, similarity_threshold) + + tracklets = defaultdict(dict) + all_hyps = dict() + for i, (multi, single) in enumerate(tqdm(individuals)): + if single is not None: + tracklets["single"][i] = single + if multi is None: + continue + if track_method == "box": + # TODO: get cropping parameters and utilize! + xy = calc_bboxes_from_keypoints(multi) + else: + xy = multi[..., :2] + hyps = tracker.track(xy) + all_hyps[i] = hyps + for hyp in hyps: + tracklet_id, pred_id = hyp[-2:].astype(int) + if pred_id != -1: + tracklets[tracklet_id][i] = multi[pred_id] + return tracklets, all_hyps diff --git a/deeplabcut/core/visualization.py b/deeplabcut/core/visualization.py new file mode 100644 index 0000000000..d9796f1c14 --- /dev/null +++ b/deeplabcut/core/visualization.py @@ -0,0 +1,234 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Visualization methods for.""" + +from __future__ import annotations + +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +def form_figure(nx, ny) -> tuple[plt.Figure, plt.Axes]: + """Forms a figure on which to plot images.""" + fig, ax = plt.subplots(frameon=False) + ax.set_xlim(0, nx) + ax.set_ylim(0, ny) + ax.axis("off") + ax.invert_yaxis() + fig.tight_layout() + return fig, ax + + +def visualize_scoremaps( + image: np.ndarray, + scmap: np.ndarray, +) -> tuple[plt.Figure, plt.Axes]: + """Plots scoremaps as an image overlay. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + + Returns: + The figure and axis on which the image scoremap was plot. + """ + ny, nx = np.shape(image)[:2] + fig, ax = form_figure(nx, ny) + ax.imshow(image) + ax.imshow(scmap, alpha=0.5) + return fig, ax + + +def visualize_locrefs( + image: np.ndarray, + scmap: np.ndarray, + locref_x: np.ndarray, + locref_y: np.ndarray, + step: int = 5, + zoom_width: int = 0, +) -> tuple[plt.Figure, plt.Axes]: + """Plots a scoremap and the corresponding location refinement field on an image. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + locref_x: The x-coordinate of the location refinement field, of shape (h, w) + locref_y: The y-coordinate of the location refinement field, of shape (h, w) + step: The step with which to plot the location refinement field. + zoom_width: The zoom width with which to plot the scoremaps. + + Returns: + The figure and axis on which the image scoremap and locref field were plot. + """ + fig, ax = visualize_scoremaps(image, scmap) + X, Y = np.meshgrid(np.arange(locref_x.shape[1]), np.arange(locref_x.shape[0])) + M = np.zeros(locref_x.shape, dtype=bool) + M[scmap < 0.5] = True + U = np.ma.masked_array(locref_x, mask=M) + V = np.ma.masked_array(locref_y, mask=M) + ax.quiver( + X[::step, ::step], + Y[::step, ::step], + U[::step, ::step], + V[::step, ::step], + color="r", + units="x", + scale_units="xy", + scale=1, + angles="xy", + ) + if zoom_width > 0: + maxloc = np.unravel_index(np.argmax(scmap), scmap.shape) + ax.set_xlim(maxloc[1] - zoom_width, maxloc[1] + zoom_width) + ax.set_ylim(maxloc[0] + zoom_width, maxloc[0] - zoom_width) + return fig, ax + + +def visualize_paf( + image: np.ndarray, + paf: np.ndarray, + step: int = 5, + colors: list | None = None, +) -> tuple[plt.Figure, plt.Axes]: + """Plots the PAF on top of the image. + + Args: + image: Shape (height, width, channels). The image on which the model was run. + paf: Shape (height, width, 2 * len(paf_graph)). The PAF output by the model. + step: The step with which to plot the scoremaps. + colors: The colormap to use. + + Returns: + The figure and axis on which the image PAF was plot. + """ + ny, nx = np.shape(image)[:2] + fig, ax = form_figure(nx, ny) + ax.imshow(image) + n_fields = paf.shape[2] + if colors is None: + colors = ["r"] * n_fields + for n in range(n_fields): + U = paf[:, :, n, 0] + V = paf[:, :, n, 1] + X, Y = np.meshgrid(np.arange(U.shape[1]), np.arange(U.shape[0])) + M = np.zeros(U.shape, dtype=bool) + M[U**2 + V**2 < 0.5 * 0.5**2] = True + U = np.ma.masked_array(U, mask=M) + V = np.ma.masked_array(V, mask=M) + ax.quiver( + X[::step, ::step], + Y[::step, ::step], + U[::step, ::step], + V[::step, ::step], + scale=50, + headaxislength=4, + alpha=1, + width=0.002, + color=colors[n], + angles="xy", + ) + return fig, ax + + +def generate_model_output_plots( + output_folder: Path, + image_name: str, + bodypart_names: list[str], + bodyparts_to_plot: list[str], + image: np.ndarray, + scmap: np.ndarray, + locref: np.ndarray | None = None, + paf: np.ndarray | None = None, + paf_graph: list[tuple[int, int]] | None = None, + paf_all_in_one: bool = True, + paf_colormap: str = "rainbow", + output_suffix: str = "", +) -> None: + """Generates model output plots (maps) for an image and saves them to disk. + + Args: + output_folder: The folder in which the plots should be saved. + image_name: The name of the image for which the plots were generated. + bodypart_names: The names of bodyparts the model outputs. + bodyparts_to_plot: The names of bodyparts that should be plot. + image: Shape (height, width, channels). The image on which the model was run. + scmap: Shape (height, width, num_bodyparts). The scoremaps output by the model. + locref: Shape (height, width, num_bodyparts, 2). Optionally, the location + refinement fields output by the model. + paf: Shape (height, width, 2 * len(paf_graph)). Optionally, the part-affinity + fields output by the model. + paf_graph: Must be set if paf is not None. The PAF graph used to assemble. + paf_all_in_one: Whether to plot all PAFs in a single image. + paf_colormap: The colormap to use for the PAF maps. + output_suffix: The filename suffix for the maps to output. + """ + + def _filename(map_name) -> str: + return f"{image_name}_{map_name}_{output_suffix}.png" + + to_plot = [i for i, bpt in enumerate(bodypart_names) if bpt in bodyparts_to_plot] + if len(to_plot) > 1: + map_ = scmap[:, :, to_plot].sum(axis=2) + elif len(to_plot) == 1 and len(bodypart_names) > 1: + map_ = scmap[:, :, to_plot[0]] + else: + map_ = scmap[..., 0] + + fig1, _ = visualize_scoremaps(image, map_) + fig1.savefig(output_folder / _filename("scmap")) + + if locref is not None: + if len(to_plot) > 1: + map_ = scmap[:, :, to_plot] + locref_x_ = locref[:, :, to_plot, 0] + locref_y_ = locref[:, :, to_plot, 1] + # only get the locref fields around their respective detections + locref_x_[map_ < 0.5] = 0 + locref_y_[map_ < 0.5] = 0 + # combine locrefs + map_ = map_.sum(axis=2) + locref_x_ = locref_x_.sum(axis=2) + locref_y_ = locref_y_.sum(axis=2) + elif len(to_plot) == 1 and len(bodypart_names) > 1: + locref_x_ = locref[:, :, to_plot[0], 0] + locref_y_ = locref[:, :, to_plot[0], 1] + else: + locref_x_ = locref[..., 0] + locref_y_ = locref[..., 1] + + fig2, _ = visualize_locrefs(image, map_, locref_x_, locref_y_) + fig2.savefig(output_folder / _filename("locref")) + + if paf is not None: + if paf_graph is None: + raise ValueError("When plotting the PAF, you must pass the ``paf_graph``") + + edge_list = [] + for n, edge in enumerate(paf_graph): + if any(ind in to_plot for ind in edge): + e0, e1 = edge + edge_list.append([(2 * n, 2 * n + 1), (bodypart_names[e0], bodypart_names[e1])]) + + if paf_all_in_one: + inds = [elem[0] for elem in edge_list] + n_inds = len(inds) + cmap = plt.cm.get_cmap(paf_colormap, n_inds) + colors = cmap(range(n_inds)) + fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors) + fig3.savefig(output_folder / _filename("paf")) + else: + for inds, names in edge_list: + fig3, _ = visualize_paf(image, paf[:, :, [inds]]) + fig3.savefig(output_folder / _filename(f"paf_{'_'.join(names)}")) + + plt.close("all") diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py new file mode 100644 index 0000000000..ae755223cf --- /dev/null +++ b/deeplabcut/core/weight_init.py @@ -0,0 +1,207 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Classes to configure how to initialize model weights.""" + +from __future__ import annotations + +import warnings +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + + +@dataclass +class WeightInitialization: + """Configures weights initialization when transfer learning or fine-tuning models. + + Args: + snapshot_path: The path to the snapshot used to initialize pose model weights + when training a model. + detector_snapshot_path: The path to the snapshot used to initialize detector + weights when training a model. + dataset: Optionally, the dataset on which the snapshots were trained. Required + when fine-tuning SuperAnimal models. + with_decoder: Whether to load the decoder weights as well. + memory_replay: Only when ``with_decoder=True``. Whether to train the model with + memory replay, so that it predicts all SuperAnimal (or previous project) + bodyparts. + conversion_array: The mapping from SuperAnimal (or other project, on which the + weights were trained) to project bodyparts. Required when + `with_decoder=True`. + An array [7, 0, 1] means the project has 3 bodyparts, where the 1st bodypart + corresponds to the 8th bodypart in the pretrained model, the 2nd to the 1st + and the 3rd to the 2nd (as arrays are 0-indexed). + bodyparts: Optionally, the name of each bodypart entry in the conversion array. + """ + + snapshot_path: Path + detector_snapshot_path: Path | None = None + dataset: str | None = None + with_decoder: bool = False + memory_replay: bool = False + conversion_array: np.ndarray | None = None + bodyparts: list[str] | None = None + + def __post_init__(self): + if self.memory_replay and not self.with_decoder: + raise ValueError( + "You cannot train a model with memory replay if you do not keep the " + "decoder layers (``with_decoder=True``), but you passed " + "`memory_replay=True` and `with_decoder=False`. Please change your " + "WeightInitialization parameters." + ) + + if self.with_decoder and self.conversion_array is None: + raise ValueError( + "You must specify a conversion_array to initialize decoder weights (``with_decoder=True``)." + ) + + if self.bodyparts is not None and self.conversion_array is None: + raise ValueError( + "Specifying bodyparts should only be done when `with_decoder=True` and" + " the conversion array is specified." + ) + + if self.conversion_array is not None and self.bodyparts is not None: + if not len(self.conversion_array) == len(self.bodyparts): + raise ValueError( + "There must be the same number of elements in the bodyparts list " + "and conv. array; found {self.bodyparts}, {self.conversion_array}" + ) + + def to_dict(self) -> dict: + """Returns: the weight initialization as a dict""" + data = dict() + if self.dataset is not None: + data["dataset"] = self.dataset + + data["snapshot_path"] = str(self.snapshot_path) + if self.detector_snapshot_path is not None: + data["detector_snapshot_path"] = str(self.detector_snapshot_path) + + data["with_decoder"] = self.with_decoder + data["memory_replay"] = self.memory_replay + + if self.conversion_array is not None: + data["conversion_array"] = self.conversion_array.tolist() + + if self.bodyparts is not None: + data["bodyparts"] = self.bodyparts + + return data + + @staticmethod + def from_dict(data: dict) -> WeightInitialization: + if "snapshot_path" not in data: + return WeightInitialization.from_dict_legacy(data) + + detector_snapshot_path = data.get("detector_snapshot_path") + if detector_snapshot_path is not None: + detector_snapshot_path = Path(detector_snapshot_path) + + conversion_array = data.get("conversion_array") + if conversion_array is not None: + conversion_array = np.array(conversion_array, dtype=int) + + return WeightInitialization( + snapshot_path=Path(data["snapshot_path"]), + detector_snapshot_path=detector_snapshot_path, + dataset=data.get("dataset"), + with_decoder=data["with_decoder"], + memory_replay=data["memory_replay"], + conversion_array=conversion_array, + bodyparts=data.get("bodyparts"), + ) + + @staticmethod + def from_dict_legacy(data: dict) -> WeightInitialization: + """Deals with weight initialization that were created before 3.0.0rc5.""" + import deeplabcut.pose_estimation_pytorch.modelzoo.utils as utils + + conversion_array = data.get("conversion_array") + if conversion_array is not None: + conversion_array = np.array(conversion_array, dtype=int) + + return WeightInitialization( + snapshot_path=utils.get_super_animal_snapshot_path( + dataset=data["dataset"], + model_name="hrnet_w32", + ), + detector_snapshot_path=utils.get_super_animal_snapshot_path( + dataset=data["dataset"], + model_name="fasterrcnn_resnet50_fpn_v2", + ), + with_decoder=data["with_decoder"], + memory_replay=data["memory_replay"], + conversion_array=conversion_array, + bodyparts=data.get("bodyparts"), + ) + + @staticmethod + def build( + cfg: dict, + super_animal: str, + model_name: str = "hrnet_w32", + detector_name: str = "fasterrcnn_resnet50_fpn_v2", + with_decoder: bool = False, + memory_replay: bool = False, + customized_pose_checkpoint: str | None = None, + customized_detector_checkpoint: str | None = None, + ) -> WeightInitialization: + """Builds a WeightInitialization for a project. + + `WeightInitialization.build` is deprecated and will be removed in a future + version of DeepLabCut. Please use `build_weight_init` from `deeplabcut.modelzoo` + instead. + + Args: + cfg: The project's configuration. + super_animal: The SuperAnimal model with which to initialize weights. + model_name: The name of the model architecture for which to load the weights + (defaults to "hrnet_w32" for backwards compatibility). + detector_name: The name of the detector architecture for which to load the + weights (defaults to "fasterrcnn_resnet50_fpn_v2" for backwards + compatibility). + with_decoder: Whether to load the decoder weights as well. If this is true, + a conversion table must be specified for the given SuperAnimal in the + project configuration file. See + ``deeplabcut.modelzoo.utils.create_conversion_table`` to create a + conversion table. + memory_replay: Only when ``with_decoder=True``. Whether to train the model + with memory replay, so that it predicts all SuperAnimal bodyparts. + customized_pose_checkpoint: A customized SuperAnimal pose checkpoint, as an + alternative to the Hugging Face one + customized_detector_checkpoint: A customized SuperAnimal detector + checkpoint, as an alternative to the Hugging Face one + + Returns: + The built WeightInitialization. + """ + from deeplabcut.modelzoo import build_weight_init + + deprecation_warning = ( + "The `WeightInitialization.build` is deprecated and will be removed in a " + "future version of DeepLabCut. Please use `build_weight_init` from " + "`deeplabcut.modelzoo` instead." + ) + warnings.warn(deprecation_warning, DeprecationWarning, stacklevel=2) + + return build_weight_init( + cfg, + super_animal, + model_name, + detector_name, + with_decoder, + memory_replay, + customized_pose_checkpoint, + customized_detector_checkpoint, + ) diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index 9b83b12444..550c5a91e7 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -10,11 +10,8 @@ # -def add_new_videos( - config, videos, copy_videos=False, coords=None, extract_frames=False -): - """ - Add new videos to the config file at any stage of the project. +def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frames=False): + """Add new videos to the config file at any stage of the project. Parameters ---------- @@ -38,22 +35,29 @@ def add_new_videos( Examples -------- Video will be added, with cropping dimensions according to the frame dimensions of mouse5.avi - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi']) + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi'] + ) Video will be added, with cropping dimensions [0,100,0,200] - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi'],copy_videos=False,coords=[[0,100,0,200]]) + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml', + ['/data/videos/mouse5.avi'],copy_videos=False,coords=[[0,100,0,200]] + ) Two videos will be added, with cropping dimensions [0,100,0,200] and [0,100,0,250], respectively. - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi','/data/videos/mouse6.avi'],copy_videos=False,coords=[[0,100,0,200],[0,100,0,250]]) - + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml', + ['/data/videos/mouse5.avi','/data/videos/mouse6.avi'], + copy_videos=False,coords=[[0,100,0,200],[0,100,0,250]]) """ import os import shutil from pathlib import Path - from deeplabcut.utils import auxiliaryfunctions - from deeplabcut.utils.auxfun_videos import VideoReader - from deeplabcut.generate_training_dataset import frame_extraction + from ..generate_training_dataset import frame_extraction + from ..utils import auxiliaryfunctions + from ..utils.auxfun_videos import VideoReader # Read the config file cfg = auxiliaryfunctions.read_config(config) @@ -69,14 +73,12 @@ def add_new_videos( dirs = [data_path / Path(i.stem) for i in videos] for p in dirs: - """ - Creates directory under data & perhaps copies videos (to /video) - """ + """Creates directory under data & perhaps copies videos (to /video)""" p.mkdir(parents=True, exist_ok=True) destinations = [video_path.joinpath(vp.name) for vp in videos] if copy_videos: - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists(): pass else: @@ -86,7 +88,7 @@ def add_new_videos( else: # creates the symlinks of the video and puts it in the videos directory. print("Attempting to create a symbolic link of the video ...") - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists(): print(f"Video {dst} already exists. Skipping...") continue @@ -94,19 +96,16 @@ def add_new_videos( src = str(src) dst = str(dst) os.symlink(src, dst) - print("Created the symlink of {} to {}".format(src, dst)) + print(f"Created the symlink of {src} to {dst}") except OSError: try: import subprocess - subprocess.check_call("mklink %s %s" % (dst, src), shell=True) + subprocess.check_call(f"mklink {dst} {src}", shell=True) except (OSError, subprocess.CalledProcessError): - print( - "Symlink creation impossible (exFat architecture?): " - "copying the video instead." - ) + print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) - print("{} copied to {}".format(src, dst)) + print(f"{src} copied to {dst}") videos = destinations if copy_videos: @@ -117,7 +116,7 @@ def add_new_videos( # For windows os.path.realpath does not work and does not link to the real video. video_path = str(Path.resolve(Path(video))) # video_path = os.path.realpath(video) - except: + except Exception: video_path = os.readlink(video) vid = VideoReader(video_path) @@ -133,13 +132,7 @@ def add_new_videos( videos_str = [str(video) for video in videos] auxiliaryfunctions.write_config(config, cfg) if extract_frames: - frame_extraction.extract_frames( - config, userfeedback=False, videos_list=videos_str - ) - print( - "New videos were added to the project and frames have been extracted for labeling!" - ) + frame_extraction.extract_frames(config, userfeedback=False, videos_list=videos_str) + print("New videos were added to the project and frames have been extracted for labeling!") else: - print( - "New videos were added to the project! Use the function 'extract_frames' to select frames for labeling." - ) + print("New videos were added to the project! Use the function 'extract_frames' to select frames for labeling.") diff --git a/deeplabcut/create_project/demo_data.py b/deeplabcut/create_project/demo_data.py index c495c89ba6..30b58390f0 100644 --- a/deeplabcut/create_project/demo_data.py +++ b/deeplabcut/create_project/demo_data.py @@ -13,13 +13,17 @@ from pathlib import Path import deeplabcut +from deeplabcut.core.engine import Engine from deeplabcut.utils import auxiliaryfunctions -def load_demo_data(config, createtrainingset=True): - """ - Loads the demo data -- subset from trail-tracking data in Mathis et al. 2018. - When loading, it sets paths correctly to run this project on your system +def load_demo_data( + config: str, + createtrainingset: bool = True, + engine: Engine = Engine.PYTORCH, +): + """Loads the demo data -- subset from trail-tracking data in Mathis et al. 2018. + When loading, it sets paths correctly to run this project on your system. Parameter ---------- @@ -29,6 +33,9 @@ def load_demo_data(config, createtrainingset=True): createtrainingset : bool Boolean variable indicating if a training set shall be created. + engine: Engine + The Engine to create the training set for if a training set shall be created. + Example -------- >>> deeplabcut.load_demo_data('config.yaml') @@ -40,12 +47,12 @@ def load_demo_data(config, createtrainingset=True): transform_data(config) if createtrainingset: print("Loaded, now creating training data...") - deeplabcut.create_training_dataset(config, num_shuffles=1) + deeplabcut.create_training_dataset(config, num_shuffles=1, engine=engine) def transform_data(config): - """ - This function adds the full path to labeling dataset. + """This function adds the full path to labeling dataset. + It also adds the correct path to the video file in the config file. """ @@ -61,8 +68,6 @@ def transform_data(config): print("This is not an official demo dataset.") if "WILL BE AUTOMATICALLY UPDATED BY DEMO CODE" in cfg["video_sets"].keys(): - cfg["video_sets"][str(video_file)] = cfg["video_sets"].pop( - "WILL BE AUTOMATICALLY UPDATED BY DEMO CODE" - ) + cfg["video_sets"][str(video_file)] = cfg["video_sets"].pop("WILL BE AUTOMATICALLY UPDATED BY DEMO CODE") auxiliaryfunctions.write_config(config, cfg) diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index 76679f1867..b4d615803f 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -10,17 +10,38 @@ # import os +from collections.abc import Sequence from pathlib import Path import yaml - -import deeplabcut -from deeplabcut.utils import auxiliaryfunctions +from dlclibrary import get_available_detectors from dlclibrary.dlcmodelzoo.modelzoo_download import ( - download_huggingface_model, MODELOPTIONS, + download_huggingface_model, + get_available_datasets, + get_available_models, ) +import deeplabcut +from deeplabcut.core.config import read_config_as_dict, write_config +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset.metadata import ( + DataSplit, + ShuffleMetadata, + TrainingDatasetMetadata, +) +from deeplabcut.generate_training_dataset.trainingsetmanipulation import ( + MakeInference_yaml, +) +from deeplabcut.modelzoo.utils import get_super_animal_project_cfg +from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + add_metadata, + make_pytorch_test_config, +) +from deeplabcut.pose_estimation_pytorch.modelzoo.utils import load_super_animal_config +from deeplabcut.utils import auxiliaryfunctions +from deeplabcut.utils.deprecation import renamed_parameter + Modeloptions = MODELOPTIONS # backwards compatibility for COLAB NOTEBOOK @@ -58,18 +79,18 @@ def MakeTest_pose_yaml(dictionary, keys2save, saveasfile): # yaml.dump(dict_test, f) +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_pretrained_human_project( project, experimenter, videos, working_directory=None, copy_videos=False, - videotype="", + video_extensions: str | Sequence[str] | None = None, createlabeledvideo=True, analyzevideo=True, ): - """ - LEGACY FUNCTION will be deprecated. + """LEGACY FUNCTION will be deprecated. Use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) @@ -80,7 +101,9 @@ def create_pretrained_human_project( Please make sure to cite it too if you use this code! """ print( - "LEGACY FUNCTION will be deprecated.... use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) in the future!" + "LEGACY FUNCTION will be deprecated.... " + "use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) " + "in the future!" ) create_pretrained_project( project, @@ -89,26 +112,169 @@ def create_pretrained_human_project( model="full_human", working_directory=working_directory, copy_videos=copy_videos, - videotype=videotype, + video_extensions=video_extensions, createlabeledvideo=createlabeledvideo, analyzevideo=analyzevideo, + engine=Engine.TF, ) +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_pretrained_project( - project, - experimenter, - videos, - model="full_human", - working_directory=None, - copy_videos=False, - videotype="", - analyzevideo=True, - filtered=True, - createlabeledvideo=True, - trainFraction=None, + project: str, + experimenter: str, + videos: list[str], + model: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + analyzevideo: bool = True, + filtered: bool = True, + createlabeledvideo: bool = True, + trainFraction: float | None = None, + engine: Engine = Engine.PYTORCH, + multi_animal: bool = False, + individuals: list[str] | None = None, + net_name: str | None = None, + detector_name: str | None = None, ): + r"""Creates a new project directory, sub-directories and a basic configuration file. + Change its parameters to your projects need. + + The project will also be initialized with a pre-trained model from the DeepLabCut model zoo! + + http://modelzoo.deeplabcut.org + + Parameters + ---------- + project : string + String containing the name of the project. + + experimenter : string + String containing the name of the experimenter. + + model: string | None, default = None, + The model / dataset to use as basis for the project. + If None, the default model / dataset for the selected engine will be used. + + videos : list[string] + A list of string containing the full paths of the videos to include in the project. + + working_directory : string, optional, default = None + The directory where the project will be created. If None - the current working directory will be used. + + copy_videos : bool, optional, default = False, + If this is set to True, the videos are copied to the ``videos`` directory. + If it is False, symlink of the videos are copied to the project/videos directory. + Note: on Windows: True is often necessary! + + analyzevideo: bool, optional + If true, then the video is analyzed and a labeled video is created. + If false, then only the project will be created and the weights downloaded. + + filtered: bool, default True + Indicates if filtered pose data output should be plotted rather than frame-by-frame predictions. + Filtered version can be calculated with deeplabcut.filterpredictions() + + createlabeledvideo: bool, default True, + Specifies if a labeled video needs to be created. + + trainFraction: float|None, default = None. + Fraction that will be used in dlc-model/trainingset folder name. + If None - default value (0.95) from new projects will be used. + + engine: Engine, default Engine.PYTORCH, + engine on which the pretrained weights are based + + multi_animal: bool = False, + Specifies if the project is single or multi-animal. + Implemented only for Pytorch-based models. + + individuals: list[str] | None = None, + Only if multianimal is True. + Defines the names of the individuals. + + net_name: str | None, default = None, + Valid only if using Pytorch engine. + Name of the pose model on which the superanimal dataset has been trained on. + If None - "hrnet_w32" will be used as default. + + detector_name: str | None, default = None, + Valid only if using Pytorch engine. + Name of the detector model on which the superanimal dataset has been trained on. + If None - "fasterrcnn_resnet50_fpn_v2" will be used as default. + + Example + -------- + Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", + ... ["/data/videos/homosapiens1.avi"], copy_videos=False) + + Loading full_cat model and analyzing video "felixfeliscatus3.avi" + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", + ... ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) + + Windows: + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Bill", + ... [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) + Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ + if engine == Engine.TF: + return create_pretrained_project_tensorflow( + project=project, + experimenter=experimenter, + videos=videos, + model=model, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + analyzevideo=analyzevideo, + filtered=filtered, + createlabeledvideo=createlabeledvideo, + trainFraction=trainFraction, + ) + elif engine == Engine.PYTORCH: + return create_pretrained_project_pytorch( + project=project, + experimenter=experimenter, + videos=videos, + dataset=model, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + analyze_video=analyzevideo, + filtered=filtered, + create_labeled_video=createlabeledvideo, + train_fraction=trainFraction, + multi_animal=multi_animal, + individuals=individuals, + net_name=net_name, + detector_name=detector_name, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def create_pretrained_project_pytorch( + project: str, + experimenter: str, + videos: list[str], + dataset: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | None = None, + analyze_video: bool = True, + filtered: bool = True, + create_labeled_video: bool = True, + train_fraction: float | None = None, + multi_animal: bool = False, + individuals: list[str] | None = None, + net_name: str | None = None, + detector_name: str | None = None, +): + r"""Method used specifically for Pytorch-based ModelZoo models. + Creates a new project directory, sub-directories and a basic configuration file. Change its parameters to your projects need. @@ -124,48 +290,281 @@ def create_pretrained_project( experimenter : string String containing the name of the experimenter. - model: string, options see http://www.mousemotorlab.org/dlc-modelzoo - Current option and default: 'full_human' Creates a demo human project and analyzes a video with ResNet 101 weights pretrained on MPII Human Pose. This is from the DeeperCut paper - by Insafutdinov et al. https://arxiv.org/abs/1605.03170 Please make sure to cite it too if you use this code! + dataset: string|None, default = None, + The superanimal dataset to use as basis for the project. + If not specified - superanimal_quadruped will be used by default. - videos : list + videos : list[string] A list of string containing the full paths of the videos to include in the project. - working_directory : string, optional - The directory where the project will be created. The default is the ``current working directory``; if provided, it must be a string. + working_directory : string, optional, default = None + The directory where the project will be created. If None - the current working directory will be used. + + copy_videos : bool, optional, default = False, + If this is set to True, the videos are copied to the ``videos`` directory. + If it is False, symlink of the videos are copied to the project/videos directory. + Note: on Windows: True is often necessary! - copy_videos : bool, optional ON WINDOWS: TRUE is often necessary! - If this is set to True, the videos are copied to the ``videos`` directory. If it is False,symlink of the videos are copied to the project/videos directory. The default is ``False``; if provided it must be either - ``True`` or ``False``. + analyze_video: bool, optional + If true, then the video is analyzed and a labeled video is created. + If false, then only the project will be created and the weights downloaded. - analyzevideo " bool, optional - If true, then the video is analyzed and a labeled video is created. If false, then only the project will be created and the weights downloaded. You can then access them + filtered: bool, default True + Indicates if filtered pose data output should be plotted rather than frame-by-frame predictions. + Filtered version can be calculated with deeplabcut.filterpredictions() - filtered: bool, default false - Boolean variable indicating if filtered pose data output should be plotted rather than frame-by-frame predictions. - Filtered version can be calculated with deeplabcut.filterpredictions + create_labeled_video: bool, default True + Specifies if a labeled video needs to be created. - trainFraction: By default value from *new* projects. (0.95) + train_fraction: float|None, default = None. Fraction that will be used in dlc-model/trainingset folder name. + If None - default value (0.95) from new projects will be used. + + multi_animal: bool = False, + Specifies if the project is single or multi-animal + + individuals: list[str]|None = None, + Only if multianimal is True. + Defines the names of the individuals. + + net_name: str | None, default = None, + Valid only if using Pytorch engine. + Name of the pose model on which the superanimal dataset has been trained on. + If None - "hrnet_w32" will be used as default. + + detector_name: str | None, default = None, + Valid only if using Pytorch engine. + Name of the detector model on which the superanimal dataset has been trained on. + If None - "fasterrcnn_resnet50_fpn_v2" will be used as default. Example -------- Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi - >>> deeplabcut.create_pretrained_project('humanstrokestudy','Linus',['/data/videos/homosapiens1.avi'], copy_videos=False) + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", + ... ["/data/videos/homosapiens1.avi"], copy_videos=False) Loading full_cat model and analyzing video "felixfeliscatus3.avi" - >>> deeplabcut.create_pretrained_project('humanstrokestudy','Linus',['/data/videos/felixfeliscatus3.avi'], model='full_cat') + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", + ... ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) Windows: - >>> deeplabcut.create_pretrained_project('humanstrokestudy','Bill',[r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'],r'C:\yourusername\analysis\project' copy_videos=True) + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", + ... "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) + """ + # Check arguments + if not dataset: + dataset = "superanimal_quadruped" + + if not net_name: + net_name = "hrnet_w32" + + # Currently, all Pytorch Superanimal models are Top-Down. + if not detector_name: + detector_name = "fasterrcnn_resnet50_fpn_v2" + + if dataset not in get_available_datasets(): + raise ValueError(f"Invalid dataset '{dataset}'. Available datasets are: {get_available_datasets()}") + + if net_name not in get_available_models(dataset): + raise ValueError( + f"Invalid net_name '{net_name}' for dataset {dataset}. " + f"The following net types are available: {get_available_models(dataset)}" + ) + + if detector_name not in get_available_detectors(dataset): + raise ValueError( + f"Invalid detector_name '{detector_name}' for dataset {dataset}. " + f"The following detectors are available: {get_available_detectors(dataset)}" + ) + + # Create project + cfg_path = deeplabcut.create_new_project( + project=project, + experimenter=experimenter, + videos=videos, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + multianimal=multi_animal, + individuals=individuals, + ) + + # Edits to do to the project config + cfg_edits = {} + if train_fraction is not None: + cfg_edits["TrainingFraction"] = [train_fraction] + super_animal_project_cfg = get_super_animal_project_cfg(dataset) + super_animal_bodyparts = super_animal_project_cfg.get("bodyparts") + super_animal_skeleton = super_animal_project_cfg.get("skeleton") + cfg_edits["skeleton"] = super_animal_skeleton + if multi_animal: + cfg_edits["multianimalbodyparts"] = super_animal_bodyparts + else: + cfg_edits["bodyparts"] = super_animal_bodyparts + auxiliaryfunctions.edit_config(cfg_path, edits=cfg_edits) + + # Create the shuffle train and test directories + config = read_config_as_dict(cfg_path) + shuffle_dir = Path(cfg_path).parent / auxiliaryfunctions.get_model_folder( + trainFraction=config["TrainingFraction"][0], + shuffle=1, + cfg=config, + engine=Engine.PYTORCH, + ) + train_dir = shuffle_dir / "train" + test_dir = shuffle_dir / "test" + train_dir.mkdir(parents=True, exist_ok=True) + test_dir.mkdir(parents=True, exist_ok=True) + + # Download the weights and put them into appropriate directory + print("Downloading weights...") + super_animal_detector_name = f"{dataset}_{detector_name}" + new_detector_name = "snapshot-detector-000.pt" + download_huggingface_model( + model_name=super_animal_detector_name, + target_dir=str(train_dir), + rename_mapping={f"{super_animal_detector_name}.pt": new_detector_name}, + ) + super_animal_model_name = f"{dataset}_{net_name}" + new_snapshot_name = "snapshot-000.pt" + download_huggingface_model( + model_name=super_animal_model_name, + target_dir=str(train_dir), + rename_mapping={f"{super_animal_model_name}.pt": new_snapshot_name}, + ) + + # Create pytorch_config.yaml + train_cfg_path = train_dir / "pytorch_config.yaml" + pytorch_config = load_super_animal_config( + super_animal=dataset, + model_name=net_name, + detector_name=detector_name, + ) + pytorch_config = add_metadata(config, pytorch_config, train_cfg_path) + pytorch_config["resume_training_from"] = str(train_dir / new_snapshot_name) + pytorch_config["detector"]["resume_training_from"] = str(train_dir / new_detector_name) + write_config(train_cfg_path, pytorch_config) + + # Create test pose_cfg.yaml + test_cfg_path = test_dir / "pose_cfg.yaml" + make_pytorch_test_config(model_config=pytorch_config, test_config_path=test_cfg_path, save=True) + + # Create inference_cfg.yaml if needed + if multi_animal: + inference_cfg_path = test_dir / "inference_cfg.yaml" + _create_inference_config(inference_cfg_path, config) + + # Create metadata.yaml with shuffle info in training-data directory + _create_training_datasets_metadata(config, shuffle_dir.name, Engine.PYTORCH) + + # Process the videos + _process_videos( + cfg_path=cfg_path, + video_extensions=video_extensions, + analyze_video=analyze_video, + filtered=filtered, + create_labeled_video=create_labeled_video, + ) + return cfg_path, str(train_cfg_path) + + +def _create_inference_config(inference_cfg_path: str | Path, project_cfg: dict): + inf_updates = dict( + minimalnumberofconnections=int(len(project_cfg["multianimalbodyparts"]) / 2), + topktoretain=len(project_cfg["individuals"]), + withid=project_cfg.get("identity", False), + ) + default_inf_path = Path(auxiliaryfunctions.get_deeplabcut_path()) / "inference_cfg.yaml" + MakeInference_yaml(inf_updates, inference_cfg_path, default_inf_path) + + +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def create_pretrained_project_tensorflow( + project: str, + experimenter: str, + videos: list[str], + model: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + analyzevideo: bool = True, + filtered: bool = True, + createlabeledvideo: bool = True, + trainFraction: float | None = None, +): + r"""Method used specifically for Tensorflow-based ModelZoo models. + + Creates a new project directory, sub-directories and a basic configuration file. + Change its parameters to your projects need. + + The project will also be initialized with a pre-trained model from the DeepLabCut model zoo! + + http://modelzoo.deeplabcut.org + + Parameters + ---------- + project : string + String containing the name of the project. + + experimenter : string + String containing the name of the experimenter. + + model: string|None, default = None, + The model / dataset to use as basis for the project. + If not specified - full_human will be used by default. + + videos : list[string] + A list of string containing the full paths of the videos to include in the project. + + working_directory : string, optional, default = None + The directory where the project will be created. If None - the current working directory will be used. + + copy_videos : bool, optional, default = False, + If this is set to True, the videos are copied to the ``videos`` directory. + If it is False, symlink of the videos are copied to the project/videos directory. + Note: on Windows: True is often necessary! + analyzevideo: bool, optional + If true, then the video is analyzed and a labeled video is created. + If false, then only the project will be created and the weights downloaded. + + filtered: bool, default True + Indicates if filtered pose data output should be plotted rather than frame-by-frame predictions. + Filtered version can be calculated with deeplabcut.filterpredictions() + + createlabeledvideo: bool, default True + Specifies if a labeled video needs to be created. + + trainFraction: float|None, default = None. + Fraction that will be used in dlc-model/trainingset folder name. + If None - default value (0.95) from new projects will be used. + + Example + -------- + Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Linus", ["/data/videos/homosapiens1.avi"], copy_videos=False) + + Loading full_cat model and analyzing video "felixfeliscatus3.avi" + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) + + Windows: + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) + Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ + if not model: + model = "full_human" + if model in MODELOPTIONS: cwd = os.getcwd() cfg = deeplabcut.create_new_project( - project, experimenter, videos, working_directory, copy_videos, videotype + project, experimenter, videos, working_directory, copy_videos, video_extensions=video_extensions ) if trainFraction is not None: auxiliaryfunctions.edit_config(cfg, {"TrainingFraction": [trainFraction]}) @@ -245,16 +644,8 @@ def create_pretrained_project( modelfoldername = auxiliaryfunctions.get_model_folder( trainFraction=config["TrainingFraction"][0], shuffle=1, cfg=config ) - path_train_config = str( - os.path.join( - config["project_path"], Path(modelfoldername), "train", "pose_cfg.yaml" - ) - ) - path_test_config = str( - os.path.join( - config["project_path"], Path(modelfoldername), "test", "pose_cfg.yaml" - ) - ) + path_train_config = str(os.path.join(config["project_path"], Path(modelfoldername), "train", "pose_cfg.yaml")) + path_test_config = str(os.path.join(config["project_path"], Path(modelfoldername), "test", "pose_cfg.yaml")) # Download the weights and put then in appropriate directory print("Downloading weights...") @@ -272,13 +663,12 @@ def create_pretrained_project( } auxiliaryfunctions.edit_config(cfg, dict_) - # downloading base encoder / not required unless on re-trains (but when a training set is created this happens anyway) + # downloading base encoder / not required unless on re-trains + # (but when a training set is created this happens anyway) # model_path = auxfun_models.check_for_weights(pose_cfg['net_type'], parent_path) # Updating training and test pose_cfg: - snapshotname = [fn for fn in os.listdir(train_dir) if ".meta" in fn][0].split( - ".meta" - )[0] + snapshotname = [fn for fn in os.listdir(train_dir) if ".meta" in fn][0].split(".meta")[0] dict2change = { "init_weights": str(os.path.join(train_dir, snapshotname)), "project_path": str(config["project_path"]), @@ -300,23 +690,63 @@ def create_pretrained_project( MakeTest_pose_yaml(pose_cfg, keys2save, path_test_config) - video_dir = os.path.join(config["project_path"], "videos") - if analyzevideo == True: - print("Analyzing video...") - deeplabcut.analyze_videos(cfg, [video_dir], videotype, save_as_csv=True) - - if createlabeledvideo == True: - if filtered: - deeplabcut.filterpredictions(cfg, [video_dir], videotype) + _create_training_datasets_metadata(config, modelfoldername.name, Engine.TF) - print("Plotting results...") - deeplabcut.create_labeled_video( - cfg, [video_dir], videotype, draw_skeleton=True, filtered=filtered - ) - deeplabcut.plot_trajectories(cfg, [video_dir], videotype, filtered=filtered) + _process_videos( + cfg_path=cfg, + video_extensions=video_extensions, + analyze_video=analyzevideo, + filtered=filtered, + create_labeled_video=createlabeledvideo, + ) os.chdir(cwd) return cfg, path_train_config else: return "N/A", "N/A" + + +def _create_training_datasets_metadata(config: dict, shuffle_dir_name: str, engine: Engine): + # First create the metadata object + metadata = TrainingDatasetMetadata.create(config) + + # Create a new shuffle with TensorFlow engine + new_shuffle = ShuffleMetadata( + name=shuffle_dir_name, + train_fraction=config["TrainingFraction"][0], + index=1, + engine=engine, + split=DataSplit(train_indices=(), test_indices=()), + ) + + # Add the shuffle to metadata + metadata = metadata.add(new_shuffle) + + # Save the metadata + metadata.save() + + return metadata + + +def _process_videos( + cfg_path: str | Path, + video_extensions: str | Sequence[str] | None = None, + analyze_video: bool = True, + filtered: bool = True, + create_labeled_video: bool = True, +): + cfg_path = str(cfg_path) + video_dir = Path(cfg_path).parent / "videos" + + if analyze_video: + print("Analyzing video...") + deeplabcut.analyze_videos(cfg_path, [video_dir], video_extensions=video_extensions, save_as_csv=True) + + if create_labeled_video: + if filtered: + deeplabcut.filterpredictions(cfg_path, [video_dir], video_extensions) + + print("Plotting results...") + deeplabcut.create_labeled_video(cfg_path, [video_dir], video_extensions, draw_skeleton=True, filtered=filtered) + deeplabcut.plot_trajectories(cfg_path, [video_dir], video_extensions, filtered=filtered) diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index c6194e9882..11e1f26ba4 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -13,19 +13,25 @@ import os import shutil import warnings +from collections.abc import Sequence from pathlib import Path + from deeplabcut import DEBUG -from deeplabcut.utils.auxfun_videos import VideoReader +from deeplabcut.core.engine import Engine +from deeplabcut.utils.auxfun_videos import VideoReader, collect_video_paths +from deeplabcut.utils.deprecation import renamed_parameter +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_new_project( - project, - experimenter, - videos, - working_directory=None, - copy_videos=False, - videotype="", - multianimal=False, + project: str, + experimenter: str, + videos: list[str], + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + multianimal: bool = False, + individuals: list[str] | None = None, ): r"""Create the necessary folders and files for a new project. @@ -42,9 +48,17 @@ def create_new_project( The name of the experimenter. videos : list[str] - A list of strings representing the full paths of the videos to include in the - project. If the strings represent a directory instead of a file, all videos of - ``videotype`` will be imported. + A list of strings representing the full paths of the videos or video-directories + to include in the project. + + video_extensions (str | Sequence[str] | None, default=None): + Controls how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). working_directory : string, optional The directory where the project will be created. The default is the @@ -58,11 +72,21 @@ def create_new_project( multianimal: bool, optional. Default: False. For creating a multi-animal project (introduced in DLC 2.2) + individuals: list[str]|None = None, + Relevant only if multianimal is True. + list of individuals to be used in the project configuration. + If None - defaults to ['individual1', 'individual2', 'individual3'] + Returns ------- str Path to the new project configuration file. + Raises + ------ + FileNotFoundError + If a non-existent path is passed to ``videos``. + Examples -------- @@ -82,7 +106,7 @@ def create_new_project( project='reaching-task', experimenter='Linus', videos=['/data/videos'], - videotype='.mp4', + video_extensions='.mp4', ) Windows: @@ -94,9 +118,11 @@ def create_new_project( copy_videos=True, ) - Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) + Users must format paths with either: + r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions months_3letter = { @@ -122,12 +148,12 @@ def create_new_project( if working_directory is None: working_directory = "." wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}".format(pn=project, exp=experimenter, date=date) + project_name = f"{project}-{experimenter}-{date}" project_path = wd / project_name # Create project and sub-directories if not DEBUG and project_path.exists(): - print('Project "{}" already exists!'.format(project_path)) + print(f'Project "{project_path}" already exists!') return os.path.join(str(project_path), "config.yaml") video_path = project_path / "videos" data_path = project_path / "labeled-data" @@ -135,74 +161,54 @@ def create_new_project( results_path = project_path / "dlc-models" for p in [video_path, data_path, shuffles_path, results_path]: p.mkdir(parents=True, exist_ok=DEBUG) - print('Created "{}"'.format(p)) - - # Add all videos in the folder. Multiple folders can be passed in a list, similar to the video files. Folders and video files can also be passed! - vids = [] - for i in videos: - # Check if it is a folder - if os.path.isdir(i): - vids_in_dir = [ - os.path.join(i, vp) for vp in os.listdir(i) if vp.endswith(videotype) - ] - vids = vids + vids_in_dir - if len(vids_in_dir) == 0: - print("No videos found in", i) - print( - "Perhaps change the videotype, which is currently set to:", - videotype, - ) - else: - videos = vids - print( - len(vids_in_dir), - " videos from the directory", - i, - "were added to the project.", - ) - else: - if os.path.isfile(i): - vids = vids + [i] - videos = vids - - videos = [Path(vp) for vp in videos] - dirs = [data_path / Path(i.stem) for i in videos] + print(f'Created "{p}"') + + # Add all videos in the folder. Multiple folders can be passed in a list, + # similar to the video files. Folders and video files can also be passed! + collected_videos: list[Path] = collect_video_paths(videos, extensions=video_extensions) + + # TODO @deruyter92 2026-05-20: Move this verbosity block to `collect_video_paths` instead + files_per_dir: dict[Path, int] = {} + for f in collected_videos: + files_per_dir[f.parent] = files_per_dir.get(f.parent, 0) + 1 + for dir, count in files_per_dir.items(): + print(f"Found {count} videos in {dir}") + for p in (Path(v) for v in videos if Path(v).is_dir()): + if p.resolve() not in {d.resolve() for d in files_per_dir}: + print(f"No videos found in {p}") + print(f"Perhaps change the video_extensions, which is currently set to: {video_extensions}") + + videos = collected_videos + dirs = [data_path / i.stem for i in videos] for p in dirs: - """ - Creates directory under data - """ + """Creates directory under data.""" p.mkdir(parents=True, exist_ok=True) destinations = [video_path.joinpath(vp.name) for vp in videos] if copy_videos: print("Copying the videos") - for src, dst in zip(videos, destinations): - shutil.copy( - os.fspath(src), os.fspath(dst) - ) # https://www.python.org/dev/peps/pep-0519/ + for src, dst in zip(videos, destinations, strict=False): + shutil.copy(os.fspath(src), os.fspath(dst)) # https://www.python.org/dev/peps/pep-0519/ else: # creates the symlinks of the video and puts it in the videos directory. print("Attempting to create a symbolic link of the video ...") - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists() and not DEBUG: - raise FileExistsError("Video {} exists already!".format(dst)) + raise FileExistsError(f"Video {dst} exists already!") try: src = str(src) dst = str(dst) os.symlink(src, dst) - print("Created the symlink of {} to {}".format(src, dst)) + print(f"Created the symlink of {src} to {dst}") except OSError: try: import subprocess - subprocess.check_call("mklink %s %s" % (dst, src), shell=True) + subprocess.check_call(f"mklink {dst} {src}", shell=True) except (OSError, subprocess.CalledProcessError): - print( - "Symlink creation impossible (exFat architecture?): " - "copying the video instead." - ) + print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) - print("{} copied to {}".format(src, dst)) + print(f"{src} copied to {dst}") videos = destinations if copy_videos: @@ -213,16 +219,17 @@ def create_new_project( for video in videos: print(video) try: - # For windows os.path.realpath does not work and does not link to the real video. [old: rel_video_path = os.path.realpath(video)] + # For windows os.path.realpath does not work and does not link to the real + # video. [old: rel_video_path = os.path.realpath(video)] rel_video_path = str(Path.resolve(Path(video))) - except: + except Exception: rel_video_path = os.readlink(str(video)) try: vid = VideoReader(rel_video_path) video_sets[rel_video_path] = {"crop": ", ".join(map(str, vid.get_bbox()))} - except IOError: - warnings.warn("Cannot open the video file! Skipping to the next one...") + except OSError: + warnings.warn("Cannot open the video file! Skipping to the next one...", stacklevel=2) os.remove(video) # Removing the video or link from the project if not len(video_sets): @@ -230,7 +237,8 @@ def create_new_project( shutil.rmtree(project_path, ignore_errors=True) warnings.warn( "No valid videos were found. The project was not created... " - "Verify the video files and re-create the project." + "Verify the video files and re-create the project.", + stacklevel=2, ) return "nothingcreated" @@ -239,7 +247,7 @@ def create_new_project( cfg_file, ruamelFile = auxiliaryfunctions.create_config_template(multianimal) cfg_file["multianimalproject"] = multianimal cfg_file["identity"] = False - cfg_file["individuals"] = ["individual1", "individual2", "individual3"] + cfg_file["individuals"] = individuals if individuals else ["individual1", "individual2", "individual3"] cfg_file["multianimalbodyparts"] = ["bodypart1", "bodypart2", "bodypart3"] cfg_file["uniquebodyparts"] = [] cfg_file["bodyparts"] = "MULTI!" @@ -248,8 +256,15 @@ def create_new_project( ["bodypart2", "bodypart3"], ["bodypart1", "bodypart3"], ] - cfg_file["default_augmenter"] = "multi-animal-imgaug" - cfg_file["default_net_type"] = "dlcrnet_ms5" + engine = cfg_file.get("engine") + if engine in Engine.PYTORCH.aliases: + cfg_file["default_augmenter"] = "albumentations" + cfg_file["default_net_type"] = "resnet_50" + elif engine in Engine.TF.aliases: + cfg_file["default_augmenter"] = "multi-animal-imgaug" + cfg_file["default_net_type"] = "dlcrnet_ms5" + else: + raise ValueError(f"Unknown or undefined engine {engine}") cfg_file["default_track_method"] = "ellipse" else: cfg_file, ruamelFile = auxiliaryfunctions.create_config_template() @@ -272,13 +287,15 @@ def create_new_project( cfg_file["TrainingFraction"] = [0.95] cfg_file["iteration"] = 0 cfg_file["snapshotindex"] = -1 + cfg_file["detector_snapshotindex"] = -1 cfg_file["x1"] = 0 cfg_file["x2"] = 640 cfg_file["y1"] = 277 cfg_file["y2"] = 624 - cfg_file[ - "batch_size" - ] = 8 # batch size during inference (video - analysis); see https://www.biorxiv.org/content/early/2018/10/30/457242 + cfg_file["batch_size"] = ( + 8 # batch size during inference (video - analysis); see https://www.biorxiv.org/content/early/2018/10/30/457242 + ) + cfg_file["detector_batch_size"] = 1 cfg_file["corner2move2"] = (50, 50) cfg_file["move2corner"] = True cfg_file["skeleton_color"] = "black" @@ -293,7 +310,11 @@ def create_new_project( print('Generated "{}"'.format(project_path / "config.yaml")) print( - "\nA new project with name %s is created at %s and a configurable file (config.yaml) is stored there. Change the parameters in this file to adapt to your project's needs.\n Once you have changed the configuration file, use the function 'extract_frames' to select frames for labeling.\n. [OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." - % (project_name, str(wd)) + f"\nA new project with name {project_name} is created at {str(wd)} " + "and a configurable file (config.yaml) is stored there. " + "Change the parameters in this file to adapt to your project's needs.\n " + "Once you have changed the configuration file, " + "use the function 'extract_frames' to select frames for labeling.\n. " + "[OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." ) return projconfigfile diff --git a/deeplabcut/create_project/new_3d.py b/deeplabcut/create_project/new_3d.py index f95344b3e2..24be797a2d 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -17,8 +17,9 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_directory=None): - """Creates a new project directory, sub-directories and a basic configuration file for 3d project. - The configuration file is loaded with the default values. Adjust the parameters to your project's needs. + r"""Creates a new project directory, sub-directories and a basic configuration file + for 3d project. The configuration file is loaded with the default values. Adjust the + parameters to your project's needs. Parameters ---------- @@ -32,7 +33,8 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director An integer value specifying the number of cameras. working_directory : string, optional - The directory where the project will be created. The default is the ``current working directory``; if provided, it must be a string. + The directory where the project will be created. The default is the ``current working directory``; if provided, + it must be a string. Example @@ -42,10 +44,10 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director Windows: >>> deeplabcut.create_new_project('reaching-task','Bill',2) - Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) - + Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \\ ) """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions date = dt.today() @@ -58,13 +60,11 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director working_directory = "." wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}-{triangulate}".format( - pn=project, exp=experimenter, date=date, triangulate="3d" - ) + project_name = "{pn}-{exp}-{date}-{triangulate}".format(pn=project, exp=experimenter, date=date, triangulate="3d") project_path = wd / project_name # Create project and sub-directories if not DEBUG and project_path.exists(): - print('Project "{}" already exists!'.format(project_path)) + print(f'Project "{project_path}" already exists!') return camera_matrix_path = project_path / "camera_matrix" @@ -81,7 +81,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director path_removed_images, ]: p.mkdir(parents=True, exist_ok=DEBUG) - print('Created "{}"'.format(p)) + print(f'Created "{p}"') # Create config file cfg_file_3d, ruamelFile_3d = auxiliaryfunctions.create_config_template_3d() @@ -89,7 +89,10 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director cfg_file_3d["scorer"] = experimenter cfg_file_3d["date"] = d cfg_file_3d["project_path"] = str(project_path) - # cfg_file_3d['config_files']= [str('Enter the path of the config file ')+str(i)+ ' to include' for i in range(1,3)] + # cfg_file_3d['config_files']= [ + # str('Enter the path of the config file ') + str(i) + ' to include' + # for i in range(1, 3) + # ] # cfg_file_3d['config_files']= ['Enter the path of the config file 1'] cfg_file_3d["colormap"] = "jet" cfg_file_3d["dotsize"] = 15 @@ -98,9 +101,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director cfg_file_3d["markerColor"] = "r" cfg_file_3d["pcutoff"] = 0.4 cfg_file_3d["num_cameras"] = num_cameras - cfg_file_3d["camera_names"] = [ - str("camera-" + str(i)) for i in range(1, num_cameras + 1) - ] + cfg_file_3d["camera_names"] = [str("camera-" + str(i)) for i in range(1, num_cameras + 1)] cfg_file_3d["scorername_3d"] = "DLC_3D" cfg_file_3d["skeleton"] = [ @@ -113,26 +114,21 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director for i in range(num_cameras): path = str( - "/home/mackenzie/DEEPLABCUT/DeepLabCut/2DprojectCam" - + str(i + 1) - + "-Mackenzie-2019-06-05/config.yaml" - ) - cfg_file_3d.insert( - len(cfg_file_3d), str("config_file_camera-" + str(i + 1)), path + "/home/mackenzie/DEEPLABCUT/DeepLabCut/2DprojectCam" + str(i + 1) + "-Mackenzie-2019-06-05/config.yaml" ) + cfg_file_3d.insert(len(cfg_file_3d), str("config_file_camera-" + str(i + 1)), path) for i in range(num_cameras): cfg_file_3d.insert(len(cfg_file_3d), str("shuffle_camera-" + str(i + 1)), 1) - cfg_file_3d.insert( - len(cfg_file_3d), str("trainingsetindex_camera-" + str(i + 1)), 0 - ) + cfg_file_3d.insert(len(cfg_file_3d), str("trainingsetindex_camera-" + str(i + 1)), 0) projconfigfile = os.path.join(str(project_path), "config.yaml") auxiliaryfunctions.write_config_3d(projconfigfile, cfg_file_3d) print('Generated "{}"'.format(project_path / "config.yaml")) print( - "\nA new project with name %s is created at %s and a configurable file (config.yaml) is stored there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating the camera otherwise use the function ``triangulate`` to triangulate the dataframe" - % (project_name, wd) + f"\nA new project with name {project_name} is created at {wd} and a configurable file (config.yaml) is stored" + f"there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating" + f"the camera otherwise use the function ``triangulate`` to triangulate the dataframe" ) return projconfigfile diff --git a/deeplabcut/generate_training_dataset/__init__.py b/deeplabcut/generate_training_dataset/__init__.py index 05b0092d49..7729536aba 100644 --- a/deeplabcut/generate_training_dataset/__init__.py +++ b/deeplabcut/generate_training_dataset/__init__.py @@ -11,5 +11,10 @@ from deeplabcut.generate_training_dataset.frame_extraction import * -from deeplabcut.generate_training_dataset.trainingsetmanipulation import * +from deeplabcut.generate_training_dataset.metadata import ( + DataSplit, + ShuffleMetadata, + TrainingDatasetMetadata, +) from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import * +from deeplabcut.generate_training_dataset.trainingsetmanipulation import * diff --git a/deeplabcut/generate_training_dataset/frame_extraction.py b/deeplabcut/generate_training_dataset/frame_extraction.py index 6264e01157..3605b9aca7 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -11,11 +11,10 @@ def select_cropping_area(config, videos=None): - """ - Interactively select the cropping area of all videos in the config. - A user interface pops up with a frame to select the cropping parameters. - Use the left click to draw a box and hit the button 'set cropping parameters' - to store the cropping parameters for a video in the config.yaml file. + """Interactively select the cropping area of all videos in the config. A user + interface pops up with a frame to select the cropping parameters. Use the left click + to draw a box and hit the button 'set cropping parameters' to store the cropping + parameters for a video in the config.yaml file. Parameters ---------- @@ -31,7 +30,7 @@ def select_cropping_area(config, videos=None): cfg : dict Updated project configuration """ - from deeplabcut.utils import auxiliaryfunctions, auxfun_videos + from deeplabcut.utils import auxfun_videos, auxiliaryfunctions cfg = auxiliaryfunctions.read_config(config) if videos is None: @@ -251,23 +250,24 @@ def extract_frames( extracted_cam=0, ) """ + import glob import os - import sys import re - import glob - import numpy as np + import sys from pathlib import Path + + import numpy as np from skimage import io from skimage.util import img_as_ubyte - from deeplabcut.utils import frameselectiontools - from deeplabcut.utils import auxiliaryfunctions + + from deeplabcut.utils import auxiliaryfunctions, frameselectiontools config_file = Path(config).resolve() cfg = auxiliaryfunctions.read_config(config_file) print("Config file read successfully.") if videos_list is None: - videos = cfg.get("video_sets_original") or cfg["video_sets"] + videos = list(cfg.get("video_sets_original") or cfg["video_sets"]) else: # filter video_list by the ones in the config file videos = [v for v in cfg["video_sets"] if v in videos_list] @@ -284,13 +284,9 @@ def extract_frames( # Check for variable correctness if start > 1 or stop > 1 or start < 0 or stop < 0 or start >= stop: - raise Exception( - "Erroneous start or stop values. Please correct it in the config file." - ) + raise Exception("Erroneous start or stop values. Please correct it in the config file.") if numframes2pick < 1 and not int(numframes2pick): - raise Exception( - "Perhaps consider extracting more, or a natural number of frames." - ) + raise Exception("Perhaps consider extracting more, or a natural number of frames.") if opencv: from deeplabcut.utils.auxfun_videos import VideoWriter @@ -340,12 +336,7 @@ def extract_frames( askuser = input( "The directory already contains some frames. Do you want to add to it?(yes/no): " ) - if not ( - askuser == "y" - or askuser == "yes" - or askuser == "Y" - or askuser == "Yes" - ): + if not (askuser == "y" or askuser == "yes" or askuser == "Y" or askuser == "Yes"): sys.exit("Delete the frames and try again later!") if crop == "GUI": @@ -368,16 +359,12 @@ def extract_frames( else: coords = None - print("Extracting frames based on %s ..." % algo) + print(f"Extracting frames based on {algo} ...") if algo == "uniform": if opencv: - frames2pick = frameselectiontools.UniformFramescv2( - cap, numframes2pick, start, stop - ) + frames2pick = frameselectiontools.UniformFramescv2(cap, numframes2pick, start, stop) else: - frames2pick = frameselectiontools.UniformFrames( - clip, numframes2pick, start, stop - ) + frames2pick = frameselectiontools.UniformFrames(clip, numframes2pick, start, stop) elif algo == "kmeans": if opencv: frames2pick = frameselectiontools.KmeansbasedFrameselectioncv2( @@ -401,17 +388,16 @@ def extract_frames( ) else: print( - "Please implement this method yourself and send us a pull request! Otherwise, choose 'uniform' or 'kmeans'." + "Please implement this method yourself and send us a pull " + "request! Otherwise, choose 'uniform' or 'kmeans'." ) frames2pick = [] if not len(frames2pick): print("Frame selection failed...") - return + return [] - output_path = ( - Path(config).parents[0] / "labeled-data" / Path(video).stem - ) + output_path = Path(config).parents[0] / "labeled-data" / Path(video).stem output_path.mkdir(parents=True, exist_ok=True) is_valid = [] if opencv: @@ -420,12 +406,7 @@ def extract_frames( frame = cap.read_frame(crop=True) if frame is not None: image = img_as_ubyte(frame) - img_name = ( - str(output_path) - + "/img" - + str(index).zfill(indexlength) - + ".png" - ) + img_name = str(output_path) + "/img" + str(index).zfill(indexlength) + ".png" io.imsave(img_name, image) is_valid.append(True) else: @@ -436,16 +417,12 @@ def extract_frames( for index in frames2pick: try: image = img_as_ubyte(clip.get_frame(index * 1.0 / clip.fps)) - img_name = ( - str(output_path) - + "/img" - + str(index).zfill(indexlength) - + ".png" - ) + img_name = str(output_path) + "/img" + str(index).zfill(indexlength) + ".png" io.imsave(img_name, image) if np.var(image) == 0: # constant image print( - "Seems like black/constant images are extracted from your video. Perhaps consider using opencv under the hood, by setting: opencv=True" + "Seems like black/constant images are extracted from your video." + "Perhaps consider using opencv under the hood, by setting: opencv=True" ) is_valid.append(True) except FileNotFoundError: @@ -464,17 +441,17 @@ def extract_frames( if all(has_failed): print("Frame extraction failed. Video files must be corrupted.") - return + return has_failed elif any(has_failed): print("Although most frames were extracted, some were invalid.") else: - print( - "Frames were successfully extracted, for the videos listed in the config.yaml file." - ) + print("Frames were successfully extracted, for the videos listed in the config.yaml file.") print( "\nYou can now label the frames using the function 'label_frames' " - "(Note, you should label frames extracted from diverse videos (and many videos; we do not recommend training on single videos!))." + "(Note, you should label frames extracted from diverse videos " + "(and many videos; we do not recommend training on single videos!))." ) + return has_failed elif mode == "match": import cv2 @@ -487,19 +464,17 @@ def extract_frames( videos = [v for v in videos if v in videos_list] project_path = Path(config).parents[0] labels_path = os.path.join(project_path, "labeled-data/") - video_dir = os.path.join(project_path, "videos/") + os.path.join(project_path, "videos/") try: cfg_3d = auxiliaryfunctions.read_config(config3d) - except: + except Exception as e: raise Exception( "You must create a 3D project and edit the 3D config file before extracting matched frames. \n" - ) + ) from e cams = cfg_3d["camera_names"] extCam_name = cams[extracted_cam] del cams[extracted_cam] - label_dirs = sorted( - glob.glob(os.path.join(labels_path, "*" + extCam_name + "*")) - ) + label_dirs = sorted(glob.glob(os.path.join(labels_path, "*" + extCam_name + "*"))) # select crop method crop_list = [] @@ -521,7 +496,7 @@ def extract_frames( coords = None crop_list.append(coords) - for coords, dirPath in zip(crop_list, label_dirs): + for coords, dirPath in zip(crop_list, label_dirs, strict=False): extracted_images = glob.glob(os.path.join(dirPath, "*png")) imgPattern = re.compile("[0-9]{1,10}") @@ -563,12 +538,11 @@ def extract_frames( ) else: io.imsave(img_name, image) - print( - "\n Done extracting matched frames. You can now begin labeling frames using the function label_frames\n" - ) + print("\n Done extracting matched frames. You can now begin labeling frames using the function label_frames\n") else: print( - "Invalid MODE. Choose either 'manual', 'automatic' or 'match'. Check ``help(deeplabcut.extract_frames)`` on python and ``deeplabcut.extract_frames?`` \ - for ipython/jupyter notebook for more details." + "Invalid MODE. Choose either 'manual', 'automatic' or 'match'. " + "Check ``help(deeplabcut.extract_frames)`` on python and ``deeplabcut.extract_frames?``" + " for ipython/jupyter notebook for more details." ) diff --git a/deeplabcut/generate_training_dataset/metadata.py b/deeplabcut/generate_training_dataset/metadata.py new file mode 100644 index 0000000000..f854a962a5 --- /dev/null +++ b/deeplabcut/generate_training_dataset/metadata.py @@ -0,0 +1,492 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""File containing methods to load and parse shuffle metadata.""" + +from __future__ import annotations + +import logging +import pickle +import re +from dataclasses import dataclass +from pathlib import Path + +import numpy as np +from ruamel.yaml import YAML + +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + + +@dataclass(frozen=True) +class DataSplit: + """Class representing the metadata for a shuffle.""" + + train_indices: tuple[int, ...] + test_indices: tuple[int, ...] + + def __post_init__(self) -> None: + """ + Raises: + ValueError if the indices are not sorted in increasing + """ + for indices in [self.train_indices, self.test_indices]: + idx = np.array(indices) + if not np.all(idx[:-1] < idx[1:]): + raise RuntimeError( + "The training and test indices in a data split must be sorted in strictly ascending order." + ) + + +@dataclass(frozen=True) +class ShuffleMetadata: + """Class representing the metadata for a shuffle.""" + + name: str + train_fraction: float + index: int + engine: Engine + split: DataSplit | None + + def load_split(self, cfg: dict, trainset_path: Path) -> ShuffleMetadata: + """Loads the data split for this shuffle. + + Args: + cfg: the config for the DeepLabCut project + trainset_path: the path to the training dataset folder + + Returns: + a new instance with the data split defined + """ + _, doc_path = auxiliaryfunctions.get_data_and_metadata_filenames( + trainset_path, self.train_fraction, self.index, cfg + ) + if not Path(doc_path).exists(): + raise ValueError( + f"Could not load the metadata file for {self} as {doc_path} does not " + f"exist. If you deleted the shuffle, you also need to delete the " + f"shuffle from metadata.yaml or recreate the metadata.yaml file." + ) + + with open(doc_path, "rb") as f: + _, train_idx, test_idx, _ = pickle.load(f) + return ShuffleMetadata( + name=self.name, + train_fraction=self.train_fraction, + index=self.index, + engine=self.engine, + split=DataSplit( + train_indices=tuple(sorted([int(idx) for idx in train_idx])), + test_indices=tuple(sorted([int(idx) for idx in test_idx])), + ), + ) + + +@dataclass(frozen=True) +class TrainingDatasetMetadata: + """An immutable class containing the metadata for a dataset. + + When creating a new "training-datasets" folder (e.g., when creating the first + training set for a project, or when creating the first training for a given + iteration of a project), TrainingDatasetMetadata.create(cfg) should be called when + the "training-datasets" folder is still empty. + + For existing projects (created with DeepLabCut < 3.0), calling + TrainingDatasetMetadata.create(cfg) will go over documentation data for all existing + shuffles in the training-datasets folder and add them to a new metadata instance. + All shuffles will be given Engine.TF as an engine. + + Examples: + # Creating the metadata file for an existing project + config = "/data/my-dlc-project/config.yaml" + trainset_metadata = TrainingDatasetMetadata.create(config) + trainset_metadata.save() + + # Adding a new shuffle to the metadata file + config = "/data/my-dlc-project-2008-06-17/config.yaml" + trainset_metadata = TrainingDatasetMetadata.load(config) + new_shuffle = ShuffleMetadata( + name="my-dlc-projectJun17-trainset60shuffle5", + train_fraction=0.6, + index=5, + engine=compat.Engine.PYTORCH, + split=DataSplit(train_indices=(1, 3, 4), test_indices=(0, 2)), + ) + trainset_metadata = trainset_metadata.add(new_shuffle) + trainset_metadata.save() # saves to disk + """ + + project_config: dict + shuffles: tuple[ShuffleMetadata, ...] + file_header: tuple[str] = ( + "# This file is automatically generated - DO NOT EDIT", + "# It contains the information about the shuffles created for the dataset", + "---", + ) + + def __post_init__(self) -> None: + """ + Raises: + ValueError if the indices are not sorted in increasing order + """ + indices = [[s.train_fraction, s.index] for s in self.shuffles] + for (frac1, idx1), (frac2, idx2) in zip(indices[:-1], indices[1:], strict=False): + if not (frac1 < frac2 or (frac1 == frac2 and idx1 < idx2)): + raise RuntimeError( + "The shuffles given must be sorted in order of ascending training " + f"fraction and index. Found {self.shuffles}" + ) + + def add( + self, + shuffle: ShuffleMetadata, + overwrite: bool = False, + ) -> TrainingDatasetMetadata: + """Adds a new shuffle to the metadata file. + + Args: + shuffle: the shuffle to add + overwrite: if a shuffle with the same index is already stored in the + metadata file, whether to overwrite it + + Returns: + A new instance of TrainingDatasetMetadata with updated shuffles + + Raises: + ValueError: if overwrite=False and there is already a shuffle with the given + index in the metadata file. + """ + existing_indices = [s.index for s in self.shuffles if s.train_fraction == shuffle.train_fraction] + if shuffle.index in existing_indices: + if not overwrite: + raise RuntimeError( + f"Cannot add {shuffle} to the meta: a shuffle with index " + f"{shuffle.index} and train_fraction {shuffle.train_fraction} " + f"already exists: {self.shuffles}." + ) + + existing_shuffles = [ + s for s in self.shuffles if (s.index != shuffle.index or s.train_fraction != shuffle.train_fraction) + ] + shuffles = existing_shuffles + [shuffle] + return TrainingDatasetMetadata( + project_config=self.project_config, + shuffles=tuple(sorted(shuffles, key=lambda s: (s.train_fraction, s.index))), + ) + + def get(self, trainset_index: int = 0, index: int = 0) -> ShuffleMetadata: + """ + Args: + trainset_index: the index of the trainset fraction as defined in config.yaml + index: the index of the shuffle + + Returns: + the shuffle with the given trainset index and shuffle index + + Raises: + ValueError if trainset_index is out of bounds or the shuffle is not present + """ + fractions = self.project_config["TrainingFraction"] + if trainset_index >= len(fractions): + raise ValueError( + f"trainset_index={trainset_index} is out of bounds for " + f"TrainingFraction={fractions} (length {len(fractions)})." + ) + train_fraction = fractions[trainset_index] + for shuffle in self.shuffles: + if shuffle.train_fraction == train_fraction and shuffle.index == index: + return shuffle + + known = [(s.train_fraction, s.index) for s in self.shuffles] or "none" + raise ValueError( + f"Could not find a shuffle with train_fraction={train_fraction} and " + f"index={index}. Known shuffles (fraction, index): {known}." + ) + + def save(self) -> None: + """Saves the training dataset metadata to disk.""" + metadata = {"shuffles": {}} + data_splits: dict[DataSplit, int] = {} + trainset_path = self.path(self.project_config).parent + for s in self.shuffles: + if s.split is None: + s = s.load_split(cfg=self.project_config, trainset_path=trainset_path) + + split_index = data_splits.get(s.split) + if split_index is None: + split_index = len(data_splits) + 1 + data_splits[s.split] = split_index + + metadata["shuffles"][s.name] = { + "train_fraction": s.train_fraction, + "index": s.index, + "split": split_index, + "engine": s.engine.aliases[0], + } + + with open(self.path(self.project_config), "w") as file: + file.write("\n".join(self.file_header) + "\n") + YAML().dump(metadata, file) + + @staticmethod + def load( + config: str | Path | dict, + load_splits: bool = False, + ) -> TrainingDatasetMetadata: + """Loads the metadata from disk. + + Args: + config: the config for the DeepLabCut project (or its path) + load_splits: whether to load the data split for each shuffle + """ + if isinstance(config, (str, Path)): + cfg = auxiliaryfunctions.read_config(config) + else: + cfg = config + + metadata_path = TrainingDatasetMetadata.path(cfg) + if not metadata_path.exists(): + raise FileNotFoundError(f"No metadata.yaml found at {metadata_path}.") + with open(metadata_path) as file: + metadata = YAML(typ="safe", pure=True).load(file) + + shuffles = [] + for shuffle_name, shuffle_metadata in metadata["shuffles"].items(): + shuffle = ShuffleMetadata( + name=shuffle_name, + train_fraction=shuffle_metadata["train_fraction"], + index=shuffle_metadata["index"], + engine=Engine(shuffle_metadata["engine"]), + split=None, + ) + if load_splits: + shuffle = shuffle.load_split(cfg, metadata_path.parent) + + shuffles.append(shuffle) + + shuffles.sort(key=lambda s: (s.train_fraction, s.index)) + return TrainingDatasetMetadata(project_config=cfg, shuffles=tuple(shuffles)) + + @staticmethod + def create(config: str | Path | dict) -> TrainingDatasetMetadata: + """Function to create the metadata file. + + Assumes that all existing shuffles use the TensorFlow engine, as this file + should have already been created for PyTorch shuffles. + + Args; + config: the config for the DeepLabCut project (or its path) + default_engine: the default engine to set for shuffles in the project + + Returns: + the metadata for the existing shuffles in the project + """ + if isinstance(config, (str, Path)): + cfg = auxiliaryfunctions.read_config(config) + else: + cfg = config + + trainset_path = TrainingDatasetMetadata.path(cfg).parent + if trainset_path.exists(): + shuffle_docs = [ + f for f in trainset_path.iterdir() if re.match(r"Documentation_data-.+shuffle[0-9]+\.pickle", f.name) + ] + else: + trainset_path.mkdir(parents=True) + shuffle_docs = [] + + prefix = cfg["Task"] + cfg["date"] + shuffles = [] + existing_splits: dict[tuple[tuple[int, ...], tuple[int, ...]], int] = {} + for doc_path in shuffle_docs: + index = int(doc_path.stem.split("shuffle")[-1]) + with open(doc_path, "rb") as f: + _, train_idx, test_idx, train_frac = pickle.load(f) + + engine = Engine.TF + train_idx = tuple(sorted([int(idx) for idx in train_idx])) + test_idx = tuple(sorted([int(idx) for idx in test_idx])) + split_idx = existing_splits.get((train_idx, test_idx)) + if split_idx is None: + split_idx = len(existing_splits) + 1 + existing_splits[(train_idx, test_idx)] = split_idx + + shuffles.append( + ShuffleMetadata( + name=f"{prefix}-trainset{int(100 * train_frac)}shuffle{index}", + train_fraction=train_frac, + index=index, + engine=engine, + split=DataSplit(train_indices=train_idx, test_indices=test_idx), + ) + ) + + shuffles = tuple(sorted(shuffles, key=lambda s: (s.train_fraction, s.index))) + return TrainingDatasetMetadata( + project_config=cfg, + shuffles=shuffles, + ) + + @staticmethod + def path(cfg: dict) -> Path: + """ + Args: + cfg: the config for the DeepLabCut project + + Returns: + the path to the training dataset metadata file + """ + meta_path = auxiliaryfunctions.get_training_set_folder(cfg) / "metadata.yaml" + return Path(cfg["project_path"]) / meta_path + + +def update_metadata( + cfg: dict, + train_fraction: float, + shuffle: int, + engine: Engine, + train_indices: list[int], + test_indices: list[int], + overwrite: bool = False, +) -> None: + """Updates the metadata for a training-dataset. + + Args: + cfg: the config for the DeepLabCut project + train_fraction: the train_fraction of the new shuffle + shuffle: the index of the shuffle to add + engine: the engine for the shuffle + train_indices: the indices of images in the training set + test_indices: the indices of images in the test set + overwrite: whether to overwrite a shuffle with the same index and train fraction + if one exists + + Raises: + ValueError: if overwrite=False and there is already a shuffle with the given + index in the metadata file. + """ + prefix = cfg["Task"] + cfg["date"] + metadata = TrainingDatasetMetadata.load(cfg, load_splits=True) + new_shuffle = ShuffleMetadata( + name=f"{prefix}-trainset{int(100 * train_fraction)}shuffle{shuffle}", + train_fraction=train_fraction, + index=shuffle, + engine=engine, + split=DataSplit( + train_indices=tuple(sorted([int(i) for i in train_indices])), + test_indices=tuple(sorted([int(i) for i in test_indices])), + ), + ) + metadata = metadata.add(shuffle=new_shuffle, overwrite=overwrite) + metadata.save() + + +def get_shuffle_engine( + cfg: dict, + trainingsetindex: int, + shuffle: int, + modelprefix: str = "", +) -> Engine: + """ + Args: + cfg: the config for the DeepLabCut project + trainingsetindex: the training set index used + shuffle: the shuffle for which to get the engine + modelprefix: the model prefix, if there is one + + Returns: + the engine that the shuffle was created with + + Raises: + ValueError if the engine for the shuffle cannot be determined or the shuffle + doesn't exist + """ + if not TrainingDatasetMetadata.path(cfg).exists(): + metadata = TrainingDatasetMetadata.create(cfg) + if metadata.shuffles: + # only persist when there is actual content to avoid writing empty files + metadata.save() + else: + metadata = TrainingDatasetMetadata.load(cfg) + + # Try to resolve the shuffle from metadata; fall through to model-folder detection + # on failure so that inference works even when metadata is incomplete. + shuffle_metadata = None + try: + shuffle_metadata = metadata.get(trainingsetindex, shuffle) + except ValueError as e: + logging.warning( + "Could not read shuffle metadata for trainingsetindex=%s, shuffle=%s: %s. " + "Falling back to detecting the engine from model folders.", + trainingsetindex, + shuffle, + e, + ) + + if shuffle_metadata is not None: + return shuffle_metadata.engine + + engines = find_engines_from_model_folders(cfg, trainingsetindex, shuffle, modelprefix) + if len(engines) == 0: + prefix_str = f" and modelprefix={modelprefix}" if modelprefix else "" + raise ValueError( + f"Couldn't find any shuffles with trainingsetindex={trainingsetindex}, " + f"shuffle={shuffle}{prefix_str}. The shuffle was not found " + "in metadata.yaml and no model folder exists for it. Please check that " + "such a shuffle is defined." + ) + + engine = list(engines)[0] # Get any engine from the set + if len(engines) > 1: + logging.warning( + f"Found multiple engines for trainingsetindex={trainingsetindex}, " + f"shuffle={shuffle} and modelprefix={modelprefix}. Using engine={engine}. " + f"To select another engine, please specify it in your API call." + ) + return engine + + +def find_engines_from_model_folders( + cfg: dict, + trainingsetindex: int, + shuffle: int, + modelprefix: str = "", +) -> set[Engine]: + """Determines which engines are used with a given shuffle. + + This method can be useful when using modelprefix, as the engine for a shuffle stored + under a "modelprefix" might not be the same as the base shuffle (for which the + engine is stored in the training-datasets folder). + + Args: + cfg: the config for the DeepLabCut project + trainingsetindex: the training set index used + shuffle: the shuffle for which to get the engine + modelprefix: the model prefix, if there is one + + Returns: + the engines for which a model folder exists for the given shuffle + """ + project_path = Path(cfg["project_path"]) + train_fraction = cfg["TrainingFraction"][trainingsetindex] + + existing_engines = set() + for engine in Engine: + expected_model_folder = project_path / auxiliaryfunctions.get_model_folder( + trainFraction=train_fraction, + shuffle=shuffle, + cfg=cfg, + engine=engine, + modelprefix=modelprefix, + ) + if expected_model_folder.exists(): + existing_engines.add(engine) + + return existing_engines diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 196eb216d9..19b0fedc30 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -8,6 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations import os import os.path @@ -19,19 +20,25 @@ import numpy as np from tqdm import tqdm -from deeplabcut.generate_training_dataset import ( - merge_annotateddatasets, - read_image_shape_fast, - SplitTrials, - MakeTrain_pose_yaml, - MakeTest_pose_yaml, - MakeInference_yaml, - pad_train_test_indices, -) +import deeplabcut.compat as compat +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.engine import Engine +from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.utils import ( - auxiliaryfunctions, auxfun_models, auxfun_multianimal, + auxiliaryfunctions, +) + +from .trainingsetmanipulation import ( + MakeInference_yaml, + MakeTest_pose_yaml, + MakeTrain_pose_yaml, + SplitTrials, + merge_annotateddatasets, + pad_train_test_indices, + read_image_shape_fast, + validate_shuffles, ) @@ -49,9 +56,7 @@ def format_multianimal_training_data( n_individuals = individuals.unique().size mask_single = individuals.str.contains("single") n_animals = n_individuals - 1 if np.any(mask_single) else n_individuals - array = np.full( - (nrows, n_individuals, n_bodyparts, 3), fill_value=np.nan, dtype=np.float32 - ) + array = np.full((nrows, n_individuals, n_bodyparts, 3), fill_value=np.nan, dtype=np.float32) array[..., 0] = np.arange(n_bodyparts) temp = df.to_numpy() temp_multi = temp[:, ~mask_single].reshape((nrows, n_animals, -1, 2)) @@ -101,6 +106,7 @@ def create_multianimaltraining_dataset( Shuffles=None, windows2linux=False, net_type=None, + detector_type=None, numdigits=2, crop_size=(400, 400), crop_sampling="hybrid", @@ -109,15 +115,19 @@ def create_multianimaltraining_dataset( testIndices=None, n_edges_threshold=105, paf_graph_degree=6, + userfeedback: bool = True, + weight_init: WeightInitialization | None = None, + engine: Engine | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): - """ - Creates a training dataset for multi-animal datasets. Labels from all the extracted frames are merged into a single .h5 file.\n - Only the videos included in the config file are used to create this dataset.\n - [OPTIONAL] Use the function 'add_new_videos' at any stage of the project to add more videos to the project. + """Creates a training dataset for multi-animal datasets. Labels from all the + extracted frames are merged into a single .h5 file.\n Only the videos included in + the config file are used to create this dataset.\n [OPTIONAL] Use the function + 'add_new_videos' at any stage of the project to add more videos to the project. Important differences to standard: - stores coordinates with numdigits as many digits - - creates + Parameter ---------- config : string @@ -130,17 +140,74 @@ def create_multianimaltraining_dataset( Alternatively the user can also give a list of shuffles (integers!). net_type: string - Type of networks. Currently resnet_50, resnet_101, and resnet_152, efficientnet-b0, efficientnet-b1, efficientnet-b2, efficientnet-b3, - efficientnet-b4, efficientnet-b5, and efficientnet-b6 as well as dlcrnet_ms5 are supported (not the MobileNets!). - See Lauer et al. 2021 https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1 + Type of networks. The options available depend on which engine is used. See + Lauer et al. 2021 https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1 + Currently supported options are: + TensorFlow + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` for + a complete list) + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``dekr_w18`` + * ``dekr_w32`` + * ``dekr_w48`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` + * ``top_down_hrnet_w18`` + * ``top_down_hrnet_w32`` + * ``top_down_hrnet_w48`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` + + detector_type: string, optional, default=None + Only for the PyTorch engine. + When passing creating shuffles for top-down models, you can specify which + detector you want. If the detector_type is None, the ```ssdlite``` will be used. + The list of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` numdigits: int, optional crop_size: tuple of int, optional + Only for the TensorFlow engine. Dimensions (width, height) of the crops for data augmentation. Default is 400x400. crop_sampling: str, optional + Only for the TensorFlow engine. Crop centers sampling method. Must be either: "uniform" (randomly over the image), "keypoints" (randomly over the annotated keypoints), @@ -149,6 +216,7 @@ def create_multianimaltraining_dataset( Default is "hybrid". paf_graph: list of lists, or "config" optional (default=None) + Only for the TensorFlow engine. If not None, overwrite the default complete graph. This is useful for advanced users who already know a good graph, or simply want to use a specific one. Note that, in that case, the data-driven selection procedure upon model evaluation will be skipped. @@ -163,16 +231,43 @@ def create_multianimaltraining_dataset( List of one or multiple lists containing test indexes. n_edges_threshold: int, optional (default=105) + Only for the TensorFlow engine. Number of edges above which the graph is automatically pruned. paf_graph_degree: int, optional (default=6) + Only for the TensorFlow engine. Degree of paf_graph when automatically pruning it (before training). + userfeedback: bool, optional, default=True + If ``False``, all requested train/test splits are created (no matter if they + already exist). If you want to assure that previous splits etc. are not + overwritten, set this to ``True`` and you will be asked for each split. + + weight_init: WeightInitialisation, optional, default=None + PyTorch engine only. Specify how model weights should be initialized. The + default mode uses transfer learning from ImageNet weights. + + engine: Engine, optional + Whether to create a pose config for a Tensorflow or PyTorch model. Defaults to + the value specified in the project configuration file. If no engine is specified + for the project, defaults to ``deeplabcut.compat.DEFAULT_ENGINE``. + + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, + If using a conditional-top-down (CTD) net_type, this argument needs to be specified. + It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 + predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which + respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + Example -------- >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml',num_shuffles=1) - >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml', Shuffles=[0,1,2], trainIndices=[trainInd1, trainInd2, trainInd3], testIndices=[testInd1, testInd2, testInd3]) + >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml', Shuffles=[0,1,2], + trainIndices=[trainInd1, trainInd2, trainInd3], testIndices=[testInd1, testInd2, testInd3]) Windows: >>> deeplabcut.create_multianimaltraining_dataset(r'C:\\Users\\Ulf\\looming-task\\config.yaml',Shuffles=[3,17,5]) @@ -182,6 +277,7 @@ def create_multianimaltraining_dataset( warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) if len(crop_size) != 2 or not all(isinstance(v, int) for v in crop_size): @@ -189,8 +285,7 @@ def create_multianimaltraining_dataset( if crop_sampling not in ("uniform", "keypoints", "density", "hybrid"): raise ValueError( - f"Invalid sampling {crop_sampling}. Must be " - f"either 'uniform', 'keypoints', 'density', or 'hybrid." + f"Invalid sampling {crop_sampling}. Must be either 'uniform', 'keypoints', 'density', or 'hybrid." ) # Loading metadata from config file: @@ -202,6 +297,11 @@ def create_multianimaltraining_dataset( full_training_path = Path(project_path, trainingsetfolder) auxiliaryfunctions.attempt_to_make_folder(full_training_path, recursive=True) + # Create the trainset metadata file, if it doesn't yet exist + if not metadata.TrainingDatasetMetadata.path(cfg).exists(): + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + trainset_metadata.save() + Data = merge_annotateddatasets(cfg, full_training_path) if Data is None: return @@ -209,17 +309,22 @@ def create_multianimaltraining_dataset( if net_type is None: # loading & linking pretrained models net_type = cfg.get("default_net_type", "dlcrnet_ms5") - elif not any(net in net_type for net in ("resnet", "eff", "dlc", "mob")): - raise ValueError(f"Unsupported network {net_type}.") + + # load the engine to use to create the shuffle + if engine is None: + engine = compat.get_project_engine(cfg) + + if not (any(net in net_type for net in ("resnet", "eff", "dlc", "mob")) or engine == Engine.PYTORCH): + raise ValueError(f"Unsupported network {net_type} for engine {engine}.") multi_stage = False ### dlcnet_ms5: backbone resnet50 + multi-fusion & multi-stage module ### dlcr101_ms5/dlcr152_ms5: backbone resnet101/152 + multi-fusion & multi-stage module - if all(net in net_type for net in ("dlcr", "_ms5")): + if all(net in net_type for net in ("dlcr", "_ms5")) and engine != Engine.PYTORCH: num_layers = re.findall("dlcr([0-9]*)", net_type)[0] if num_layers == "": num_layers = 50 - net_type = "resnet_{}".format(num_layers) + net_type = f"resnet_{num_layers}" multi_stage = True dataset_type = "multi-animal-imgaug" @@ -231,9 +336,7 @@ def create_multianimaltraining_dataset( if paf_graph is None: # Automatically form a complete PAF graph n_bpts = len(multianimalbodyparts) - partaffinityfield_graph = [ - list(edge) for edge in combinations(range(n_bpts), 2) - ] + partaffinityfield_graph = [list(edge) for edge in combinations(range(n_bpts), 2)] n_edges_orig = len(partaffinityfield_graph) # If the graph is unnecessarily large (with 15+ keypoints by default), # we randomly prune it to a size guaranteeing an average node degree of 6; @@ -248,21 +351,14 @@ def create_multianimaltraining_dataset( # Use the skeleton defined in the config file skeleton = cfg["skeleton"] paf_graph = [ - sorted( - (multianimalbodyparts.index(bpt1), multianimalbodyparts.index(bpt2)) - ) - for bpt1, bpt2 in skeleton + sorted((multianimalbodyparts.index(bpt1), multianimalbodyparts.index(bpt2))) for bpt1, bpt2 in skeleton ] - print( - "Using `skeleton` from the config file as a paf_graph. Data-driven skeleton will not be computed." - ) + print("Using `skeleton` from the config file as a paf_graph. Data-driven skeleton will not be computed.") # Ignore possible connections between 'multi' and 'unique' body parts; # one can never be too careful... to_ignore = auxfun_multianimal.filter_unwanted_paf_connections(cfg, paf_graph) - partaffinityfield_graph = [ - edge for i, edge in enumerate(paf_graph) if i not in to_ignore - ] + partaffinityfield_graph = [edge for i, edge in enumerate(paf_graph) if i not in to_ignore] auxfun_multianimal.validate_paf_graph(cfg, partaffinityfield_graph) print("Utilizing the following graph:", partaffinityfield_graph) @@ -272,14 +368,13 @@ def create_multianimaltraining_dataset( # Loading the encoder (if necessary downloading from TF) dlcparent_path = auxiliaryfunctions.get_deeplabcut_path() defaultconfigfile = os.path.join(dlcparent_path, "pose_cfg.yaml") - model_path = auxfun_models.check_for_weights( - net_type, Path(dlcparent_path) - ) - if Shuffles is None: - Shuffles = range(1, num_shuffles + 1, 1) + if engine == Engine.PYTORCH: + model_path = dlcparent_path else: - Shuffles = [i for i in Shuffles if isinstance(i, int)] + model_path = auxfun_models.check_for_weights(net_type, Path(dlcparent_path)) + + Shuffles = validate_shuffles(cfg, Shuffles, num_shuffles, userfeedback) # print(trainIndices,testIndices, Shuffles, augmenter_type,net_type) if trainIndices is None and testIndices is None: @@ -290,19 +385,11 @@ def create_multianimaltraining_dataset( splits.append((train_frac, shuffle, (train_inds, test_inds))) else: if len(trainIndices) != len(testIndices) != len(Shuffles): - raise ValueError( - "Number of Shuffles and train and test indexes should be equal." - ) + raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate( - zip(trainIndices, testIndices) - ): - trainFraction = round( - len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2 - ) - print( - f"You passed a split with the following fraction: {int(100 * trainFraction)}%" - ) + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices, strict=False)): + trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) + print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, # the values added to pad the indices are removed. train_inds = np.asarray(train_inds) @@ -311,6 +398,11 @@ def create_multianimaltraining_dataset( test_inds = test_inds[test_inds != -1] splits.append((trainFraction, Shuffles[shuffle], (train_inds, test_inds))) + top_down = False + if engine == Engine.PYTORCH and net_type.startswith("top_down_"): + top_down = True + net_type = net_type[len("top_down_") :] + for trainFraction, shuffle, (trainIndices, testIndices) in splits: #################################################### # Generating data structure with labeled information & frame metadata (for deep cut) @@ -334,9 +426,7 @@ def create_multianimaltraining_dataset( ( datafilename, metadatafilename, - ) = auxiliaryfunctions.get_data_and_metadata_filenames( - trainingsetfolder, trainFraction, shuffle, cfg - ) + ) = auxiliaryfunctions.get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg) ################################################################################ # Saving metadata and data file (Pickle file) ################################################################################ @@ -347,6 +437,15 @@ def create_multianimaltraining_dataset( testIndices, trainFraction, ) + metadata.update_metadata( + cfg=cfg, + train_fraction=trainFraction, + shuffle=shuffle, + engine=engine, + train_indices=trainIndices, + test_indices=testIndices, + overwrite=not userfeedback, + ) datafilename = datafilename.split(".mat")[0] + ".pickle" import pickle @@ -361,17 +460,14 @@ def create_multianimaltraining_dataset( ################################################################################# modelfoldername = auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg - ) - auxiliaryfunctions.attempt_to_make_folder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername / "train") - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername / "test") + trainFraction, + shuffle, + cfg, + engine=engine, ) + auxiliaryfunctions.attempt_to_make_folder(Path(config).parents[0] / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername / "train")) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername / "test")) path_train_config = str( os.path.join( @@ -398,88 +494,123 @@ def create_multianimaltraining_dataset( ) ) - jointnames = [str(bpt) for bpt in multianimalbodyparts] - jointnames.extend([str(bpt) for bpt in uniquebodyparts]) - items2change = { - "dataset": datafilename, - "metadataset": metadatafilename, - "num_joints": len(multianimalbodyparts) - + len(uniquebodyparts), # cfg["uniquebodyparts"]), - "all_joints": [ - [i] for i in range(len(multianimalbodyparts) + len(uniquebodyparts)) - ], # cfg["uniquebodyparts"]))], - "all_joints_names": jointnames, - "init_weights": model_path, - "project_path": str(cfg["project_path"]), - "net_type": net_type, - "multi_stage": multi_stage, - "pairwise_loss_weight": 0.1, - "pafwidth": 20, - "partaffinityfield_graph": partaffinityfield_graph, - "partaffinityfield_predict": partaffinityfield_predict, - "weigh_only_present_joints": False, - "num_limbs": len(partaffinityfield_graph), - "dataset_type": dataset_type, - "optimizer": "adam", - "batch_size": 8, - "multi_step": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 200000]], - "save_iters": 10000, - "display_iters": 500, - "num_idchannel": len(cfg["individuals"]) - if cfg.get("identity", False) - else 0, - "crop_size": list(crop_size), - "crop_sampling": crop_sampling, - } + if engine == Engine.TF: + jointnames = [str(bpt) for bpt in multianimalbodyparts] + jointnames.extend([str(bpt) for bpt in uniquebodyparts]) + items2change = { + "dataset": datafilename, + "engine": engine.aliases[0], + "metadataset": metadatafilename, + "num_joints": len(multianimalbodyparts) + len(uniquebodyparts), # cfg["uniquebodyparts"]), + "all_joints": [ + [i] for i in range(len(multianimalbodyparts) + len(uniquebodyparts)) + ], # cfg["uniquebodyparts"]))], + "all_joints_names": jointnames, + "init_weights": str(model_path), + "project_path": str(cfg["project_path"]), + "net_type": net_type, + "multi_stage": multi_stage, + "pairwise_loss_weight": 0.1, + "pafwidth": 20, + "partaffinityfield_graph": partaffinityfield_graph, + "partaffinityfield_predict": partaffinityfield_predict, + "weigh_only_present_joints": False, + "num_limbs": len(partaffinityfield_graph), + "dataset_type": dataset_type, + "optimizer": "adam", + "batch_size": 8, + "multi_step": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 200000]], + "save_iters": 10000, + "display_iters": 500, + "num_idchannel": (len(cfg["individuals"]) if cfg.get("identity", False) else 0), + "crop_size": list(crop_size), + "crop_sampling": crop_sampling, + } + + trainingdata = MakeTrain_pose_yaml( + items2change, + path_train_config, + defaultconfigfile, + save=(engine == Engine.TF), + ) + keys2save = [ + "dataset", + "num_joints", + "all_joints", + "all_joints_names", + "net_type", + "multi_stage", + "init_weights", + "global_scale", + "location_refinement", + "locref_stdev", + "dataset_type", + "partaffinityfield_predict", + "pairwise_predict", + "partaffinityfield_graph", + "num_limbs", + "dataset_type", + "num_idchannel", + ] + + MakeTest_pose_yaml( + trainingdata, + keys2save, + path_test_config, + nmsradius=5.0, + minconfidence=0.01, + sigma=1, + locref_smooth=False, + ) # setting important def. values for inference + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_pytorch_pose_config, + make_pytorch_test_config, + ) + from deeplabcut.pose_estimation_pytorch.modelzoo.config import ( + make_super_animal_finetune_config, + ) - trainingdata = MakeTrain_pose_yaml( - items2change, path_train_config, defaultconfigfile - ) - keys2save = [ - "dataset", - "num_joints", - "all_joints", - "all_joints_names", - "net_type", - "multi_stage", - "init_weights", - "global_scale", - "location_refinement", - "locref_stdev", - "dataset_type", - "partaffinityfield_predict", - "pairwise_predict", - "partaffinityfield_graph", - "num_limbs", - "dataset_type", - "num_idchannel", - ] + # backwards compatibility with version 2.X + if net_type == "dlcrnet_ms5": + net_type = "dlcrnet_stride16_ms5" + + config_path = Path(path_train_config).with_name(engine.pose_cfg_name) + if weight_init is not None and weight_init.with_decoder: + pytorch_cfg = make_super_animal_finetune_config( + project_config=cfg, + pose_config_path=config_path, + model_name=net_type, + detector_name=detector_type, + weight_init=weight_init, + save=True, + ) + else: + pytorch_cfg = make_pytorch_pose_config( + project_config=cfg, + pose_config_path=config_path, + net_type=net_type, + top_down=top_down, + detector_type=detector_type, + weight_init=weight_init, + save=True, + ctd_conditions=ctd_conditions, + ) - MakeTest_pose_yaml( - trainingdata, - keys2save, - path_test_config, - nmsradius=5.0, - minconfidence=0.01, - sigma=1, - locref_smooth=False, - ) # setting important def. values for inference + make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) # Setting inference cfg file: - defaultinference_configfile = os.path.join( - dlcparent_path, "inference_cfg.yaml" - ) - items2change = { - "minimalnumberofconnections": int(len(cfg["multianimalbodyparts"]) / 2), - "topktoretain": len(cfg["individuals"]), - "withid": cfg.get("identity", False), - } - MakeInference_yaml( - items2change, path_inference_config, defaultinference_configfile + default_inf_path = Path(dlcparent_path) / "inference_cfg.yaml" + inf_updates = dict( + minimalnumberofconnections=int(len(cfg["multianimalbodyparts"]) / 2), + topktoretain=len(cfg["individuals"]), + withid=cfg.get("identity", False), ) + MakeInference_yaml(inf_updates, path_inference_config, default_inf_path) print( - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!" + "The training dataset is successfully created. Use the function " + "'train_network' to start training. Happy training!" ) else: pass @@ -491,20 +622,20 @@ def convert_cropped_to_standard_dataset( delete_crops=True, back_up=True, ): - import pandas as pd import pickle import shutil - from deeplabcut.generate_training_dataset import trainingsetmanipulation + + import pandas as pd + from deeplabcut.utils import read_plainconfig, write_config + from . import trainingsetmanipulation + cfg = auxiliaryfunctions.read_config(config_path) videos_orig = cfg.pop("video_sets_original") is_cropped = cfg.pop("croppedtraining") if videos_orig is None or not is_cropped: - print( - "Labeled data do not appear to be cropped. " - "Project will remain unchanged..." - ) + print("Labeled data do not appear to be cropped. Project will remain unchanged...") return project_path = cfg["project_path"] @@ -542,9 +673,7 @@ def strip_cropped_image_name(path): file = file.split("c")[0] return os.path.join(head, file + "." + ext) - img_names_old = np.asarray( - [strip_cropped_image_name(img) for img in df_old.index.to_list()] - ) + img_names_old = np.asarray([strip_cropped_image_name(img) for img in df_old.index.to_list()]) df = merge_annotateddatasets(cfg, datasets_folder) img_names = df.index.to_numpy() train_idx = [] @@ -557,15 +686,9 @@ def strip_cropped_image_name(path): if filename.startswith("Docu"): with open(pickle_file, "rb") as f: _, train_inds, test_inds, train_frac = pickle.load(f) - train_inds_temp = np.flatnonzero( - np.isin(img_names, img_names_old[train_inds]) - ) - test_inds_temp = np.flatnonzero( - np.isin(img_names, img_names_old[test_inds]) - ) - train_inds, test_inds = pad_train_test_indices( - train_inds_temp, test_inds_temp, train_frac - ) + train_inds_temp = np.flatnonzero(np.isin(img_names, img_names_old[train_inds])) + test_inds_temp = np.flatnonzero(np.isin(img_names, img_names_old[test_inds])) + train_inds, test_inds = pad_train_test_indices(train_inds_temp, test_inds_temp, train_frac) train_idx.append(train_inds) test_idx.append(test_inds) diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 6d237c27b3..e6fa7c913f 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -8,51 +8,47 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations -import math import logging +import math import os import os.path import warnings - -from functools import lru_cache +from functools import cache from pathlib import Path -from PIL import Image import numpy as np import pandas as pd import yaml +from PIL import Image -from deeplabcut.pose_estimation_tensorflow import training +import deeplabcut.compat as compat +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.engine import Engine +from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.utils import ( - auxiliaryfunctions, - conversioncode, auxfun_models, auxfun_multianimal, + auxiliaryfunctions, + conversioncode, ) from deeplabcut.utils.auxfun_videos import VideoReader -from deeplabcut.pose_estimation_tensorflow.config import load_config -from deeplabcut.modelzoo.utils import parse_available_supermodels def comparevideolistsanddatafolders(config): - """ - Auxiliary function that compares the folders in labeled-data and the ones listed under video_sets (in the config file). + """Auxiliary function that compares the folders in labeled-data and the ones listed + under video_sets (in the config file). Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] - alldatafolders = [ - fn - for fn in os.listdir(Path(config).parent / "labeled-data") - if "_labeled" not in fn - ] + alldatafolders = [fn for fn in os.listdir(Path(config).parent / "labeled-data") if "_labeled" not in fn] print("Config file contains:", len(video_names)) print("Labeled-data contains:", len(alldatafolders)) @@ -67,14 +63,16 @@ def comparevideolistsanddatafolders(config): def adddatasetstovideolistandviceversa(config): - """ - First run comparevideolistsanddatafolders(config) to compare the folders in labeled-data and the ones listed under video_sets (in the config file). - If you detect differences this function can be used to maker sure each folder has a video entry & vice versa. + """First run comparevideolistsanddatafolders(config) to compare the folders in + labeled-data and the ones listed under video_sets (in the config file). If you + detect differences this function can be used to maker sure each folder has a video + entry & vice versa. It corrects this problem in the following way: If a video entry in the config file does not contain a folder in labeled-data, then the entry is removed. - If a folder in labeled-data does not contain a video entry in the config file then the prefix path will be added in front of the name of the labeled-data folder and combined + If a folder in labeled-data does not contain a video entry in the config file then + the prefix path will be added in front of the name of the labeled-data folder and combined with the suffix variable as an ending. Width and height will be added as cropping variables as passed on. Handle with care! @@ -89,9 +87,7 @@ def adddatasetstovideolistandviceversa(config): video_names = [Path(i).stem for i in videos] alldatafolders = [ - fn - for fn in os.listdir(Path(config).parent / "labeled-data") - if "_labeled" not in fn and not fn.startswith(".") + fn for fn in os.listdir(Path(config).parent / "labeled-data") if "_labeled" not in fn and not fn.startswith(".") ] print("Config file contains:", len(video_names)) @@ -122,23 +118,19 @@ def adddatasetstovideolistandviceversa(config): if found: video_path = os.path.join(cfg["project_path"], "videos", file) clip = VideoReader(video_path) - videos.update( - {video_path: {"crop": ", ".join(map(str, clip.get_bbox()))}} - ) + videos.update({video_path: {"crop": ", ".join(map(str, clip.get_bbox()))}}) auxiliaryfunctions.write_config(config, cfg) def dropduplicatesinannotatinfiles(config): - """ - - Drop duplicate entries (of images) in annotation files (this should no longer happen, but might be useful). + """Drop duplicate entries (of images) in annotation files (this should no longer + happen, but might be useful). Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -154,24 +146,21 @@ def dropduplicatesinannotatinfiles(config): if len(DC.index) < numimages: print("Dropped", numimages - len(DC.index)) DC.to_hdf(fn, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") def dropannotationfileentriesduetodeletedimages(config): - """ - Drop entries for all deleted images in annotation files, i.e. for folders of the type: /labeled-data/*folder*/CollectedData_*scorer*.h5 - Will be carried out iteratively for all *folders* in labeled-data. + """Drop entries for all deleted images in annotation files, i.e. for folders of the + type: /labeled-data/*folder*/CollectedData_*scorer*.h5 Will be carried out + iteratively for all *folders* in labeled-data. Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -193,11 +182,9 @@ def dropannotationfileentriesduetodeletedimages(config): print("Dropping...", imagename) DC = DC.drop(imagename) dropped = True - if dropped == True: + if dropped: DC.to_hdf(fn, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) def dropimagesduetolackofannotation(config): @@ -222,6 +209,7 @@ def dropimagesduetolackofannotation(config): except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") continue + conversioncode.guarantee_multiindex_rows(DC) annotatedimages = [fn[-1] for fn in DC.index] imagelist = [fns for fns in os.listdir(str(folder)) if ".png" in fns] print("Annotated images: ", len(annotatedimages), " In folder:", len(imagelist)) @@ -229,9 +217,7 @@ def dropimagesduetolackofannotation(config): if imagename in annotatedimages: pass else: - fullpath = os.path.join( - cfg["project_path"], "labeled-data", folder, imagename - ) + fullpath = os.path.join(cfg["project_path"], "labeled-data", folder, imagename) if os.path.isfile(fullpath): print("Deleting", fullpath) os.remove(fullpath) @@ -249,15 +235,14 @@ def dropimagesduetolackofannotation(config): def dropunlabeledframes(config): - """ - Drop entries such that all the bodyparts are not labeled from the annotation files, i.e. h5 and csv files - Will be carried out iteratively for all *folders* in labeled-data. + """Drop entries such that all the bodyparts are not labeled from the annotation + files, i.e. h5 and csv files Will be carried out iteratively for all *folders* in + labeled-data. Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -277,9 +262,7 @@ def dropunlabeledframes(config): dropped = before_len - after_len if dropped: DC.to_hdf(h5file, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) print("Dropped ", dropped, "entries in ", folder) @@ -288,7 +271,7 @@ def dropunlabeledframes(config): def check_labels( config, - Labels=["+", ".", "x"], + Labels=None, scale=1, dpi=100, draw_skeleton=True, @@ -338,20 +321,17 @@ def check_labels( from deeplabcut.utils import visualization + if Labels is None: + Labels = ["+", ".", "x"] cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() video_names = [_robust_path_split(video)[1] for video in videos] - folders = [ - os.path.join(cfg["project_path"], "labeled-data", str(Path(i))) - for i in video_names - ] - print("Creating images with labels by %s." % cfg["scorer"]) + folders = [os.path.join(cfg["project_path"], "labeled-data", str(Path(i))) for i in video_names] + print("Creating images with labels by {}.".format(cfg["scorer"])) for folder in folders: try: - DataCombined = pd.read_hdf( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") - ) + DataCombined = pd.read_hdf(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5")) conversioncode.guarantee_multiindex_rows(DataCombined) if cfg.get("multianimalproject", False): color_by = "individual" if visualizeindividuals else "bodypart" @@ -371,9 +351,7 @@ def check_labels( except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") - print( - "If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!" - ) + print("If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!") def boxitintoacell(joints): @@ -395,19 +373,26 @@ def ParseYaml(configfile): def MakeTrain_pose_yaml( - itemstochange, saveasconfigfile, defaultconfigfile, items2drop={} + itemstochange, + saveasconfigfile, + defaultconfigfile, + items2drop: dict | None = None, + save: bool = True, ): + if items2drop is None: + items2drop = {} + docs = ParseYaml(defaultconfigfile) for key in items2drop.keys(): - # print(key, "dropping?") if key in docs[0].keys(): docs[0].pop(key) for key in itemstochange.keys(): docs[0][key] = itemstochange[key] - with open(saveasconfigfile, "w") as f: - yaml.dump(docs[0], f) + if save: + with open(saveasconfigfile, "w") as f: + yaml.dump(docs[0], f) return docs[0] @@ -459,33 +444,104 @@ def _robust_path_split(path): elif len(splits) == 2: parent, file = splits else: - raise ("Unknown filepath split for path {}".format(path)) + raise (f"Unknown filepath split for path {path}") filename, ext = os.path.splitext(file) return parent, filename, ext -def merge_annotateddatasets(cfg, trainingsetfolder_full): +def parse_video_filenames(videos: list[str]) -> list[str]: + """Parses the names of all videos listed in a project's ``config.yaml`` file. + + Goes through the paths all videos listed for a project, and removes entries with a + duplicate video name (e.g. if a video is listed twice, once with the path + ``/data/video-1.mov`` and once with the path ``/my-dlc-project/videos/video-1.mov``, + then ``video-1`` will only be returned once). The order of videos listed is + preserved. + + This prevents the same labeled-data to be added multiple times when merging + annotated datasets. + + Prints a warning for each filename with duplicate video paths. + + Args: + videos: the videos listed in the project's config.yaml file + + Returns: + the filenames of videos listed in the project's config.yaml file, with duplicate + entries removed """ - Merges all the h5 files for all labeled-datasets (from individual videos). + filenames = [] + filename_to_videos = {} + for video in videos: + _, filename, _ = _robust_path_split(video) + videos_with_filename = filename_to_videos.get(filename, []) + if len(videos_with_filename) == 0: + filenames.append(filename) + + videos_with_filename.append(video) + filename_to_videos[filename] = videos_with_filename + + for filename, videos in filename_to_videos.items(): + if len(videos) > 1: + video_str = "\n * " + "\n * ".join(videos) + logging.warning( + f"Found multiple videos with the same filename (``{filename}``). To " + f"avoid issues, please edit your project's `config.yaml` file to have " + f"each video added only once.\nDuplicate entries: {video_str}" + ) + + return filenames + + +def drop_likelihood_columns(df: pd.DataFrame) -> pd.DataFrame: + """Drop any columns whose coord level is named 'likelihood'. + + This sanitizes annotation DataFrames coming from h5/csv files before they are + used for training dataset generation. + + # NOTE @C-Achard 2026-05-18: This is used in several places as a guard + Most call sites using this should instead go through a canonical, validated project loading function + AND THEN do any custom local processing they require. The current design is hard to maintain and error prone, + and lacks a clearly documented, centralized project I/O interface. + """ + if not isinstance(df.columns, pd.MultiIndex): + return df + + coord_level = "coords" if "coords" in df.columns.names else df.columns.names[-1] + coord_values = df.columns.get_level_values(coord_level) + + likelihood_mask = coord_values == "likelihood" + if likelihood_mask.any(): + logging.warning("Detected likelihood columns in annotation data; dropping them.", stacklevel=2) + df = df.drop(columns=df.columns[likelihood_mask]) + + return df + + +def merge_annotateddatasets(cfg, trainingsetfolder_full): + """Merges all the h5 files for all labeled-datasets (from individual videos). This is a bit of a mess because of cross platform compatibility. - Within platform comp. is straightforward. But if someone labels on windows and wants to train on a unix cluster or colab... + Within platform comp. is straightforward. + But if someone labels on windows and wants to train on a unix cluster or colab... """ AnnotationData = [] data_path = Path(os.path.join(cfg["project_path"], "labeled-data")) videos = cfg["video_sets"].keys() - for video in videos: - _, filename, _ = _robust_path_split(video) - file_path = os.path.join( - data_path / filename, f'CollectedData_{cfg["scorer"]}.h5' - ) + video_filenames = parse_video_filenames(videos) + for filename in video_filenames: + file_path = os.path.join(data_path / filename, f"CollectedData_{cfg['scorer']}.h5") try: data = pd.read_hdf(file_path) conversioncode.guarantee_multiindex_rows(data) if data.columns.levels[0][0] != cfg["scorer"]: print( - f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation. If you need to merge datasets across scorers, see https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" + f"{file_path} labeled by a different scorer. " + "This data will not be utilized in training dataset creation." + "If you need to merge datasets across scorers, see " + "https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in\ + -DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" ) continue AnnotationData.append(data) @@ -494,7 +550,8 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): if not len(AnnotationData): print( - "Annotation data was not found by splitting video paths (from config['video_sets']). An alternative route is taken..." + "Annotation data was not found by splitting video paths (from config['video_sets']). " + "An alternative route is taken..." ) AnnotationData = conversioncode.merge_windowsannotationdataONlinuxsystem(cfg) if not len(AnnotationData): @@ -514,10 +571,18 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): bodyparts = multianimalbodyparts + uniquebodyparts else: bodyparts = cfg["bodyparts"] - AnnotationData = AnnotationData.reindex( - bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts") - ) - filename = os.path.join(trainingsetfolder_full, f'CollectedData_{cfg["scorer"]}') + AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts")) + # Filter out any stray likelihood columns that may have been concatenated in + # see napari-deeplabcut #204 and DeepLabCut #3319 + AnnotationData = drop_likelihood_columns(AnnotationData) + + if AnnotationData.empty: + logging.warning( + "The annotated dataframe is empty after reindexing using config. " + "Hint: are bodyparts correctly listed in the configuration?" + ) + + filename = os.path.join(trainingsetfolder_full, f"CollectedData_{cfg['scorer']}") AnnotationData.to_hdf(filename + ".h5", key="df_with_missing", mode="w") AnnotationData.to_csv(filename + ".csv") # human readable. return AnnotationData @@ -528,10 +593,12 @@ def SplitTrials( trainFraction=0.8, enforce_train_fraction=False, ): - """Split a trial index into train and test sets. Also checks that the trainFraction is a two digit number between 0 an 1. The reason - is that the folders contain the trainfraction as int(100*trainFraction). - If enforce_train_fraction is True, train and test indices are padded with -1 - such that the ratio of their lengths is exactly the desired train fraction. + """Split a trial index into train and test sets. + + Also checks that the trainFraction is a two digit number between 0 an 1. The reason + is that the folders contain the trainfraction as int(100*trainFraction). If + enforce_train_fraction is True, train and test indices are padded with -1 such that + the ratio of their lengths is exactly the desired train fraction. """ if trainFraction > 1 or trainFraction < 0: print( @@ -586,13 +653,14 @@ def pad_train_test_indices(train_inds, test_inds, train_fraction): def mergeandsplit(config, trainindex=0, uniform=True): - """ - This function allows additional control over "create_training_dataset". + """This function allows additional control over "create_training_dataset". - Merge annotated data sets (from different folders) and split data in a specific way, returns the split variables (train/test indices). + Merge annotated data sets (from different folders) and split data in a specific way, + returns the split variables (train/test indices). Importantly, this allows one to freeze a split. - One can also either create a uniform split (uniform = True; thereby indexing TrainingFraction in config file) or leave-one-folder out split + One can also either create a uniform split (uniform = True; thereby indexing TrainingFraction in config file) + or leave-one-folder out split by passing the index of the corresponding video from the config.yaml file as variable trainindex. Parameter @@ -601,8 +669,10 @@ def mergeandsplit(config, trainindex=0, uniform=True): Full path of the config.yaml file as a string. trainindex: int, optional - Either (in case uniform = True) indexes which element of TrainingFraction in the config file should be used (note it is a list!). - Alternatively (uniform = False) indexes which folder is dropped, i.e. the first if trainindex=0, the second if trainindex =1, etc. + Either (in case uniform = True) indexes which element of TrainingFraction + in the config file should be used (note it is a list!). + Alternatively (uniform = False) indexes which folder is dropped, + i.e. the first if trainindex=0, the second if trainindex =1, etc. uniform: bool, optional Perform uniform split (disregarding folder structure in labeled data), or (if False) leave one folder out. @@ -611,49 +681,49 @@ def mergeandsplit(config, trainindex=0, uniform=True): -------- To create a leave-one-folder-out model: >>> trainIndices, testIndices=deeplabcut.mergeandsplit(config,trainindex=0,uniform=False) - returns the indices for the first video folder (as defined in config file) as testIndices and all others as trainIndices. + returns the indices for the first video folder (as defined in config file) + as testIndices and all others as trainIndices. You can then create the training set by calling (e.g. defining it as Shuffle 3): >>> deeplabcut.create_training_dataset(config,Shuffles=[3],trainIndices=trainIndices,testIndices=testIndices) To freeze a (uniform) split (i.e. iid sampled from all the data): >>> trainIndices, testIndices=deeplabcut.mergeandsplit(config,trainindex=0,uniform=True) - You can then create two model instances that have the identical trainingset. Thereby you can assess the role of various parameters on the performance of DLC. - >>> deeplabcut.create_training_dataset(config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices],testIndices=[testIndices, testIndices]) + You can then create two model instances that have the identical trainingset. + Thereby you can assess the role of various parameters on the performance of DLC. + >>> deeplabcut.create_training_dataset( + ... config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices], + ... testIndices=[testIndices, testIndices]) -------- - """ # Loading metadata from config file: cfg = auxiliaryfunctions.read_config(config) scorer = cfg["scorer"] project_path = cfg["project_path"] # Create path for training sets & store data there - trainingsetfolder = auxiliaryfunctions.get_training_set_folder( - cfg - ) # Path concatenation OS platform independent - auxiliaryfunctions.attempt_to_make_folder( - Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True - ) + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) # Path concatenation OS platform independent + auxiliaryfunctions.attempt_to_make_folder(Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True) fn = os.path.join(project_path, trainingsetfolder, "CollectedData_" + cfg["scorer"]) try: - Data = pd.read_hdf(fn + ".h5") + data = pd.read_hdf(fn + ".h5") + data = drop_likelihood_columns(data) except FileNotFoundError: - Data = merge_annotateddatasets( + data = merge_annotateddatasets( cfg, Path(os.path.join(project_path, trainingsetfolder)), ) - if Data is None: + if data is None: return [], [] - conversioncode.guarantee_multiindex_rows(Data) - Data = Data[scorer] # extract labeled data + conversioncode.guarantee_multiindex_rows(data) + data = data[scorer] # extract labeled data - if uniform == True: + if uniform: TrainingFraction = cfg["TrainingFraction"] trainFraction = TrainingFraction[trainindex] trainIndices, testIndices = SplitTrials( - range(len(Data.index)), + range(len(data.index)), trainFraction, True, ) @@ -662,7 +732,7 @@ def mergeandsplit(config, trainindex=0, uniform=True): test_video_name = [Path(i).stem for i in videos][trainindex] print("Excluding the following folder (from training):", test_video_name) trainIndices, testIndices = [], [] - for index, name in enumerate(Data.index): + for index, name in enumerate(data.index): if test_video_name == name[1]: # this is the video name # print(name,test_video_name) testIndices.append(index) @@ -672,7 +742,7 @@ def mergeandsplit(config, trainindex=0, uniform=True): return trainIndices, testIndices -@lru_cache(maxsize=None) +@cache def read_image_shape_fast(path): # Blazing fast and does not load the image into memory with Image.open(path) as img: @@ -689,23 +759,49 @@ def to_matlab_cell(array): outer[0, 0] = array.astype("int64") return outer + # Again, remove likelihood if present + df = drop_likelihood_columns(df) + + if isinstance(df.columns, pd.MultiIndex): + coord_level = "coords" if "coords" in df.columns.names else df.columns.names[-1] + coord_values = df.columns.get_level_values(coord_level) + + has_x = "x" in coord_values + has_y = "y" in coord_values + + if not (has_x and has_y): + raise ValueError( + f"Training data must contain x/y coordinates. Found coordinate labels: {list(pd.unique(coord_values))}" + ) + for i in train_inds: data = dict() filename = df.index[i] data["image"] = filename img_shape = read_image_shape_fast(os.path.join(project_path, *filename)) data["size"] = img_shape - temp = df.iloc[i].values.reshape(-1, 2) + + row = df.iloc[i].values + + if row.size % 2 != 0: + raise ValueError( + "Training data row does not contain an even number of coordinate values " + f"after dropping non-coordinate columns. Row size={row.size}, " + f"image={filename}" + ) + + temp = row.reshape(-1, 2) joints = np.c_[range(nbodyparts), temp] joints = joints[~np.isnan(joints).any(axis=1)].astype(int) - # Check that points lie within the image + inside = np.logical_and( np.logical_and(joints[:, 1] < img_shape[2], joints[:, 1] > 0), np.logical_and(joints[:, 2] < img_shape[1], joints[:, 2] > 0), ) if not all(inside): joints = joints[inside] - if joints.size: # Exclude images without labels + + if joints.size: data["joints"] = joints train_data.append(data) matlab_data.append( @@ -715,8 +811,10 @@ def to_matlab_cell(array): to_matlab_cell(data["joints"]), ) ) + matlab_data = np.asarray( - matlab_data, dtype=[("image", "O"), ("size", "O"), ("joints", "O")] + matlab_data, + dtype=[("image", "O"), ("size", "O"), ("joints", "O")], ) return train_data, matlab_data @@ -726,13 +824,17 @@ def create_training_dataset( num_shuffles=1, Shuffles=None, windows2linux=False, - userfeedback=False, + userfeedback=True, trainIndices=None, testIndices=None, net_type=None, + detector_type=None, augmenter_type=None, posecfg_template=None, superanimal_name="", + weight_init: WeightInitialization | None = None, + engine: Engine | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): """Creates a training dataset. @@ -751,7 +853,7 @@ def create_training_dataset( Shuffles: list[int], optional Alternatively the user can also give a list of shuffles. - userfeedback: bool, optional, default=False + userfeedback: bool, optional, default=True If ``False``, all requested train/test splits are created (no matter if they already exist). If you want to assure that previous splits etc. are not overwritten, set this to ``True`` and you will be asked for each split. @@ -764,41 +866,117 @@ def create_training_dataset( List of one or multiple lists containing test indexes. net_type: list, optional, default=None - Type of networks. Currently supported options are - - * ``resnet_50`` - * ``resnet_101`` - * ``resnet_152`` - * ``mobilenet_v2_1.0`` - * ``mobilenet_v2_0.75`` - * ``mobilenet_v2_0.5`` - * ``mobilenet_v2_0.35`` - * ``efficientnet-b0`` - * ``efficientnet-b1`` - * ``efficientnet-b2`` - * ``efficientnet-b3`` - * ``efficientnet-b4`` - * ``efficientnet-b5`` - * ``efficientnet-b6`` + Type of networks. The options available depend on which engine is used. + Currently supported options are: + TensorFlow + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``mobilenet_v2_1.0`` + * ``mobilenet_v2_0.75`` + * ``mobilenet_v2_0.5`` + * ``mobilenet_v2_0.35`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` for + a complete list) + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_cspnext_m`` + * ``ctd_prenet_cspnext_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``dekr_w18`` + * ``dekr_w32`` + * ``dekr_w48`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` + * ``top_down_hrnet_w18`` + * ``top_down_hrnet_w32`` + * ``top_down_hrnet_w48`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` + + detector_type: string, optional, default=None + Only for the PyTorch engine. + When passing creating shuffles for top-down models, you can specify which + detector you want. If the detector_type is None, the ```ssdlite``` will be used. + The list of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` augmenter_type: string, optional, default=None - Type of augmenter. Currently supported augmenters are - - * ``default`` - * ``scalecrop`` - * ``imgaug`` - * ``tensorpack`` - * ``deterministic`` + Type of augmenter. The options available depend on which engine is used. + Currently supported options are: + TensorFlow + * ``default`` + * ``scalecrop`` + * ``imgaug`` + * ``tensorpack`` + * ``deterministic`` + PyTorch + * ``albumentations`` posecfg_template: string, optional, default=None + Only for the TensorFlow engine. Path to a ``pose_cfg.yaml`` file to use as a template for generating the new one for the current iteration. Useful if you would like to start with the same parameters a previous training iteration. None uses the default ``pose_cfg.yaml``. superanimal_name: string, optional, default="" - Specify the superanimal name is transfer learning with superanimal is desired. This makes sure the pose config template uses superanimal configs as template - + Only for the TensorFlow engine. For the PyTorch engine, use the ``weight_init`` + parameter. + Specify the superanimal name is transfer learning with superanimal is desired. + This makes sure the pose config template uses superanimal configs as template. + + weight_init: WeightInitialisation, optional, default=None + PyTorch engine only. Specify how model weights should be initialized. The + default mode uses transfer learning from ImageNet weights. + + engine: Engine, optional + Whether to create a pose config for a Tensorflow or PyTorch model. Defaults to + the value specified in the project configuration file. If no engine is specified + for the project, defaults to ``deeplabcut.compat.DEFAULT_ENGINE``. + + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None, default = None, + If using a conditional-top-down (CTD) net_type, this argument should be + specified. It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a + bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must + correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot + (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively + correspond to a bottom-up (BU) network type and a particular snapshot + name or index. Returns ------- @@ -818,14 +996,16 @@ def create_training_dataset( Examples -------- - Linux/MacOS - + Linux/MacOS: >>> deeplabcut.create_training_dataset( '/analysis/project/reaching-task/config.yaml', num_shuffles=1, ) - Windows + >>> deeplabcut.create_training_dataset( + '/analysis/project/reaching-task/config.yaml', Shuffles=[2], engine=deeplabcut.Engine.TF, + ) + Windows: >>> deeplabcut.create_training_dataset( 'C:\\Users\\Ulf\\looming-task\\config.yaml', Shuffles=[3,17,5], ) @@ -837,19 +1017,17 @@ def create_training_dataset( warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) # Loading metadata from config file: cfg = auxiliaryfunctions.read_config(config) - dlc_root_path = auxiliaryfunctions.get_deeplabcut_path() + auxiliaryfunctions.get_deeplabcut_path() if superanimal_name != "": - supermodels = parse_available_supermodels() - posecfg_template = os.path.join( - dlc_root_path, - "pose_estimation_tensorflow", - "superanimal_configs", - supermodels[superanimal_name], + raise ValueError( + "Invalid argument superanimal_name. This functionality has been " + "removed. Please use modelzoo.build_weight_init() instead." ) if posecfg_template: @@ -858,9 +1036,7 @@ def create_training_dataset( and not posecfg_template.endswith("superquadruped.yaml") and not posecfg_template.endswith("supertopview.yaml") ): - raise ValueError( - "posecfg_template argument must contain path to a pose_cfg.yaml file" - ) + raise ValueError("posecfg_template argument must contain path to a pose_cfg.yaml file") else: print("Reloading pose_cfg parameters from " + posecfg_template + "\n") from deeplabcut.utils.auxiliaryfunctions import read_plainconfig @@ -876,12 +1052,20 @@ def create_training_dataset( num_shuffles, Shuffles, net_type=net_type, + detector_type=detector_type, trainIndices=trainIndices, testIndices=testIndices, + userfeedback=userfeedback, + engine=engine, + weight_init=weight_init, + ctd_conditions=ctd_conditions, ) else: scorer = cfg["scorer"] project_path = cfg["project_path"] + if engine is None: + engine = compat.get_project_engine(cfg) + # Create path for training sets & store data there trainingsetfolder = auxiliaryfunctions.get_training_set_folder( cfg @@ -890,6 +1074,11 @@ def create_training_dataset( Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True ) + # Create the trainset metadata file, if it doesn't yet exist + if not metadata.TrainingDatasetMetadata.path(cfg).exists(): + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + trainset_metadata.save() + Data = merge_annotateddatasets( cfg, Path(os.path.join(project_path, trainingsetfolder)), @@ -901,40 +1090,56 @@ def create_training_dataset( # loading & linking pretrained models if net_type is None: # loading & linking pretrained models net_type = cfg.get("default_net_type", "resnet_50") + elif engine == Engine.PYTORCH: + pass else: - if ( - "resnet" in net_type - or "mobilenet" in net_type - or "efficientnet" in net_type - or "dlcrnet" in net_type - ): + if "resnet" in net_type or "mobilenet" in net_type or "efficientnet" in net_type or "dlcrnet" in net_type: pass else: raise ValueError("Invalid network type:", net_type) + top_down = False + if engine == Engine.PYTORCH: + if net_type.startswith("top_down_"): + top_down = True + net_type = net_type[len("top_down_") :] + + augmenters = compat.get_available_aug_methods(engine) + default_augmenter = augmenters[0] if augmenter_type is None: - augmenter_type = cfg.get("default_augmenter", "imgaug") + augmenter_type = cfg.get("default_augmenter", default_augmenter) + if augmenter_type is None: # this could be in config.yaml for old projects! # updating variable if null/None! #backwardscompatability - auxiliaryfunctions.edit_config(config, {"default_augmenter": "imgaug"}) - augmenter_type = "imgaug" - elif augmenter_type not in [ - "default", - "scalecrop", - "imgaug", - "tensorpack", - "deterministic", - ]: - raise ValueError("Invalid augmenter type:", augmenter_type) + augmenter_type = default_augmenter + auxiliaryfunctions.edit_config(config, {"default_augmenter": augmenter_type}) + elif augmenter_type not in augmenters: + # as the default augmenter might not be available for the given engine + augmenter_type = default_augmenter + logging.info( + f"Default augmenter {augmenter_type} not available for engine " + f"{engine}: using {default_augmenter} instead" + ) + + if augmenter_type not in augmenters: + if engine != Engine.PYTORCH: + raise ValueError( + f"Invalid augmenter type: {augmenter_type} (available: for engine={engine}: {augmenters})" + ) + + logging.info(f"Switching augmentation to {default_augmenter} for PyTorch") + augmenter_type = default_augmenter if posecfg_template: if net_type != prior_cfg["net_type"]: print( - "WARNING: Specified net_type does not match net_type from posecfg_template path entered. Proceed with caution." + "WARNING: Specified net_type does not match net_type from " + "posecfg_template path entered. Proceed with caution." ) if augmenter_type != prior_cfg["dataset_type"]: print( - "WARNING: Specified augmenter_type does not match dataset_type from posecfg_template path entered. Proceed with caution." + "WARNING: Specified augmenter_type does not match dataset_type " + "from posecfg_template path entered. Proceed with caution." ) # Loading the encoder (if necessary downloading from TF) @@ -943,16 +1148,14 @@ def create_training_dataset( defaultconfigfile = os.path.join(dlcparent_path, "pose_cfg.yaml") elif posecfg_template: defaultconfigfile = posecfg_template - model_path = auxfun_models.check_for_weights( - net_type, Path(dlcparent_path) - ) - if Shuffles is None: - Shuffles = range(1, num_shuffles + 1) + if engine == Engine.PYTORCH: + model_path = dlcparent_path else: - Shuffles = [i for i in Shuffles if isinstance(i, int)] + model_path = auxfun_models.check_for_weights(net_type, Path(dlcparent_path)) + + Shuffles = validate_shuffles(cfg, Shuffles, num_shuffles, userfeedback) - # print(trainIndices,testIndices, Shuffles, augmenter_type,net_type) if trainIndices is None and testIndices is None: splits = [ ( @@ -965,51 +1168,40 @@ def create_training_dataset( ] else: if len(trainIndices) != len(testIndices) != len(Shuffles): - raise ValueError( - "Number of Shuffles and train and test indexes should be equal." - ) + raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate( - zip(trainIndices, testIndices) - ): - trainFraction = round( - len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2 - ) - print( - f"You passed a split with the following fraction: {int(100 * trainFraction)}%" - ) + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices, strict=False)): + trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) + print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, # the values added to pad the indices are removed. train_inds = np.asarray(train_inds) train_inds = train_inds[train_inds != -1] test_inds = np.asarray(test_inds) test_inds = test_inds[test_inds != -1] - splits.append( - (trainFraction, Shuffles[shuffle], (train_inds, test_inds)) - ) + splits.append((trainFraction, Shuffles[shuffle], (train_inds, test_inds))) - bodyparts = cfg["bodyparts"] + bodyparts = auxiliaryfunctions.get_bodyparts(cfg) nbodyparts = len(bodyparts) for trainFraction, shuffle, (trainIndices, testIndices) in splits: if len(trainIndices) > 0: if userfeedback: - trainposeconfigfile, _, _ = training.return_train_network_path( + trainposeconfigfile, _, _ = compat.return_train_network_path( config, shuffle=shuffle, trainingsetindex=cfg["TrainingFraction"].index(trainFraction), + engine=engine, ) if trainposeconfigfile.is_file(): askuser = input( - "The model folder is already present. If you continue, it will overwrite the existing model (split). Do you want to continue?(yes/no): " + "The model folder is already present. " + "If you continue, it will overwrite the existing model (split). " + "Do you want to continue?(yes/no): " ) - if ( - askuser == "no" - or askuser == "No" - or askuser == "N" - or askuser == "No" - ): + if askuser == "no" or askuser == "No" or askuser == "N" or askuser == "No": raise Exception( - "Use the Shuffles argument as a list to specify a different shuffle index. Check out the help for more details." + "Use the Shuffles argument as a list to specify a different shuffle index. " + "Check out the help for more details." ) #################################################### @@ -1019,19 +1211,13 @@ def create_training_dataset( ( datafilename, metadatafilename, - ) = auxiliaryfunctions.get_data_and_metadata_filenames( - trainingsetfolder, trainFraction, shuffle, cfg - ) + ) = auxiliaryfunctions.get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg) ################################################################################ # Saving data file (convert to training file for deeper cut (*.mat)) ################################################################################ - data, MatlabData = format_training_data( - Data, trainIndices, nbodyparts, project_path - ) - sio.savemat( - os.path.join(project_path, datafilename), {"dataset": MatlabData} - ) + data, MatlabData = format_training_data(Data, trainIndices, nbodyparts, project_path) + sio.savemat(os.path.join(project_path, datafilename), {"dataset": MatlabData}) ################################################################################ # Saving metadata (Pickle file) @@ -1043,30 +1229,36 @@ def create_training_dataset( testIndices, trainFraction, ) + metadata.update_metadata( + cfg=cfg, + train_fraction=trainFraction, + shuffle=shuffle, + engine=engine, + train_indices=trainIndices, + test_indices=testIndices, + overwrite=not userfeedback, + ) ################################################################################ # Creating file structure for training & # Test files as well as pose_yaml files (containing training and testing information) ################################################################################# modelfoldername = auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg - ) - auxiliaryfunctions.attempt_to_make_folder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername) + "/train" - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername) + "/test" + trainFraction, + shuffle, + cfg, + engine=engine, ) + auxiliaryfunctions.attempt_to_make_folder(Path(config).parents[0] / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername) + "/train") + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername) + "/test") path_train_config = str( os.path.join( cfg["project_path"], Path(modelfoldername), "train", - "pose_cfg.yaml", + engine.pose_cfg_name, ) ) path_test_config = str( @@ -1077,76 +1269,212 @@ def create_training_dataset( "pose_cfg.yaml", ) ) - # str(cfg['proj_path']+'/'+Path(modelfoldername) / 'test' / 'pose_cfg.yaml') - items2change = { - "dataset": datafilename, - "metadataset": metadatafilename, - "num_joints": len(bodyparts), - "all_joints": [[i] for i in range(len(bodyparts))], - "all_joints_names": [str(bpt) for bpt in bodyparts], - "init_weights": model_path, - "project_path": str(cfg["project_path"]), - "net_type": net_type, - "dataset_type": augmenter_type, - } - - items2drop = {} - if augmenter_type == "scalecrop": - # these values are dropped as scalecrop - # doesn't have rotation implemented - items2drop = {"rotation": 0, "rotratio": 0.0} - # Also drop maDLC smart cropping augmentation parameters - for key in ["pre_resize", "crop_size", "max_shift", "crop_sampling"]: - items2drop[key] = None - - trainingdata = MakeTrain_pose_yaml( - items2change, path_train_config, defaultconfigfile, items2drop - ) + if engine == Engine.TF: + if weight_init is not None: + raise ValueError( + "Weight initialization is not supported for TensorFlow engine. " + "Pretrained weights are automatically downloaded." + ) + items2change = { + "dataset": datafilename, + "engine": engine.aliases[0], + "metadataset": metadatafilename, + "num_joints": len(bodyparts), + "all_joints": [[i] for i in range(len(bodyparts))], + "all_joints_names": [str(bpt) for bpt in bodyparts], + "init_weights": model_path, + "project_path": str(cfg["project_path"]), + "net_type": net_type, + "dataset_type": augmenter_type, + } + + items2drop = {} + if augmenter_type == "scalecrop": + # these values are dropped as scalecrop + # doesn't have rotation implemented + items2drop = {"rotation": 0, "rotratio": 0.0} + # Also drop maDLC smart cropping augmentation parameters + for key in [ + "pre_resize", + "crop_size", + "max_shift", + "crop_sampling", + ]: + items2drop[key] = None + + trainingdata = MakeTrain_pose_yaml( + items2change, + path_train_config, + defaultconfigfile, + items2drop, + save=(engine == Engine.TF), + ) - keys2save = [ - "dataset", - "num_joints", - "all_joints", - "all_joints_names", - "net_type", - "init_weights", - "global_scale", - "location_refinement", - "locref_stdev", - ] - MakeTest_pose_yaml(trainingdata, keys2save, path_test_config) - print( - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!" - ) + keys2save = [ + "dataset", + "num_joints", + "all_joints", + "all_joints_names", + "net_type", + "init_weights", + "global_scale", + "location_refinement", + "locref_stdev", + ] + MakeTest_pose_yaml(trainingdata, keys2save, path_test_config) + print( + "The training dataset is successfully created. Use the function" + "'train_network' to start training. Happy training!" + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_pytorch_pose_config, + make_pytorch_test_config, + ) + from deeplabcut.pose_estimation_pytorch.modelzoo.config import ( + make_super_animal_finetune_config, + ) + + if weight_init is not None and weight_init.with_decoder: + pytorch_cfg = make_super_animal_finetune_config( + project_config=cfg, + pose_config_path=path_train_config, + model_name=net_type, + detector_name=detector_type, + weight_init=weight_init, + save=True, + ) + else: + pytorch_cfg = make_pytorch_pose_config( + project_config=cfg, + pose_config_path=path_train_config, + net_type=net_type, + top_down=top_down, + detector_type=detector_type, + weight_init=weight_init, + save=True, + ctd_conditions=ctd_conditions, + ) + + make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) return splits def get_largestshuffle_index(config): """Returns the largest shuffle for all dlc-models in the current iteration.""" - cfg = auxiliaryfunctions.read_config(config) - project_path = cfg["project_path"] - iterate = "iteration-" + str(cfg["iteration"]) - dlc_model_path = os.path.join(project_path, "dlc-models", iterate) - if os.path.isdir(dlc_model_path): - models = os.listdir(dlc_model_path) - # sort the model directories - models.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) - - # get the shuffle index and offset by 1. - max_shuffle_index = int(models[-1].split("shuffle")[-1]) + 1 + shuffle_indices = get_existing_shuffle_indices(config) + if len(shuffle_indices) > 0: + return shuffle_indices[-1] + + return None + + +def get_existing_shuffle_indices( + cfg: dict | str | Path, + train_fraction: float | None = None, + engine: Engine | None = None, +) -> list[int]: + """ + Args: + cfg: The content of a project configuration file, or the path to the project + configuration file. + train_fraction: If defined, only get the indices of shuffles with this train + fraction. + engine: If specified, returns only the shuffle indices that were created with + the given engine. Can only be used when train_fraction is also defined. + + Returns: + the indices of existing shuffles for this iteration of the project, sorted by + ascending index + """ + + def is_valid_data_stem(stem: str) -> bool: + if len(stem) == 0: + return False + suffix = stem.split("_")[-1] + if len(suffix) == 0: + return False + info = suffix.split("shuffle") + if len(info) != 2: + return False + train_frac, idx = info + return ( + train_frac.isdigit() + and idx.isdigit() + and (train_fraction is None or int(train_frac) == int(100 * train_fraction)) + ) + + if isinstance(cfg, (str, Path)): + cfg = auxiliaryfunctions.read_config(cfg) + + project = Path(cfg["project_path"]) + trainset_folder = project / auxiliaryfunctions.get_training_set_folder(cfg) + if not trainset_folder.exists(): + return [] + + shuffle_indices = [ + int(p.stem.split("shuffle")[-1]) + for p in trainset_folder.iterdir() + if (p.stem.startswith("Documentation_data") and p.suffix == ".pickle" and is_valid_data_stem(p.stem)) + ] + if engine is not None: + if train_fraction is None: + raise ValueError(f"Must select {train_fraction} to filter shuffles by engine") + + shuffle_indices = [ + idx + for idx in shuffle_indices + if ( + project + / auxiliaryfunctions.get_model_folder( + trainFraction=train_fraction, + shuffle=idx, + cfg=cfg, + engine=engine, + ) + ).exists() + ] + + return sorted(shuffle_indices) + + +def validate_shuffles( + cfg: dict, + shuffles: list[int] | None, + num_shuffles: int | None, + userfeedback: bool, +) -> list[int]: + existing_shuffles = get_existing_shuffle_indices(cfg) + if shuffles is None: + first_index = 1 + if len(existing_shuffles) > 0: + first_index = existing_shuffles[-1] + 1 + + shuffles = range(first_index, num_shuffles + first_index) else: - max_shuffle_index = 0 + shuffles = [i for i in shuffles if isinstance(i, int)] + for shuffle_idx in shuffles: + if userfeedback and shuffle_idx in existing_shuffles: + raise ValueError( + f"Cannot create shuffle {shuffle_idx} as it already exists - " + f"you must either create the dataset with `userfeedback=False` " + f"or delete the shuffle with index {shuffle_idx} manually (in " + f"`dlc-models`/`dlc-models-pytorch` and in the " + f"`training-datasets` folder) if you want to create a new " + f"shuffle with that index. You can otherwise create a shuffle " + f"with a new index. Existing indices are {existing_shuffles}." + ) - return max_shuffle_index + return shuffles def create_training_model_comparison( config, trainindex=0, num_shuffles=1, - net_types=["resnet_50"], - augmenter_types=["imgaug"], + net_types=None, + augmenter_types=None, userfeedback=False, windows2linux=False, ): @@ -1236,12 +1564,17 @@ def create_training_model_comparison( of how to use ``shuffle_list``. """ # read cfg file + if augmenter_types is None: + augmenter_types = ["imgaug"] + if net_types is None: + net_types = ["resnet_50"] cfg = auxiliaryfunctions.read_config(config) if windows2linux: warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) # create log file @@ -1257,13 +1590,15 @@ def create_training_model_comparison( else: pass - largestshuffleindex = get_largestshuffle_index(config) + existing_shuffles = get_existing_shuffle_indices(cfg) + if len(existing_shuffles) == 0: + largestshuffleindex = 0 + else: + largestshuffleindex = existing_shuffles[-1] + 1 shuffle_list = [] for shuffle in range(num_shuffles): - trainIndices, testIndices = mergeandsplit( - config, trainindex=trainindex, uniform=True - ) + trainIndices, testIndices = mergeandsplit(config, trainindex=trainindex, uniform=True) for idx_net, net in enumerate(net_types): for idx_aug, aug in enumerate(augmenter_types): get_max_shuffle_idx = ( @@ -1298,3 +1633,193 @@ def create_training_model_comparison( logger.info(log_info) return shuffle_list + + +def create_training_dataset_from_existing_split( + config: str, + from_shuffle: int, + from_trainsetindex: int = 0, + num_shuffles: int = 1, + shuffles: list[int] | None = None, + userfeedback: bool = True, + net_type: str | None = None, + detector_type: str | None = None, + augmenter_type: str | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, + posecfg_template: dict | None = None, + superanimal_name: str = "", + weight_init: WeightInitialization | None = None, + engine: Engine | None = None, +) -> None | list[int]: + """Labels from all the extracted frames are merged into a single .h5 file. Only the + videos included in the config file are used to create this dataset. + + Args: + config: Full path of the ``config.yaml`` file as a string. + + from_shuffle: The index of the shuffle from which to copy the train/test split. + + from_trainsetindex: The trainset index of the shuffle from which to use the data + split. Default is 0. + + num_shuffles: Number of shuffles of training dataset to create, used if + ``shuffles`` is None. + + shuffles: If defined, ``num_shuffles`` is ignored and a shuffle is created for + each index given in the list. + + userfeedback: If ``False``, all requested train/test splits are created (no + matter if they already exist). If you want to assure that previous splits + etc. are not overwritten, set this to ``True`` and you will be asked for + each existing split if you want to overwrite it. + + net_type: The type of network to create the shuffle for. Currently supported + options for engine=Engine.TF are: + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``mobilenet_v2_1.0`` + * ``mobilenet_v2_0.75`` + * ``mobilenet_v2_0.5`` + * ``mobilenet_v2_0.35`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + Currently supported options for engine=Engine.TF can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_models()``. + + detector_type: string, optional, default=None + Only for the PyTorch engine. + When passing creating shuffles for top-down models, you can specify which + detector you want. If the detector_type is None, the ```ssdlite``` will be + used. The list of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported + options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` + + augmenter_type: Type of augmenter. Currently supported augmenters for + engine=Engine.TF are + * ``default`` + * ``scalecrop`` + * ``imgaug`` + * ``tensorpack`` + * ``deterministic`` + The only supported augmenter for Engine.PYTORCH is ``albumentations``. + + posecfg_template: Only for Engine.TF. Path to a ``pose_cfg.yaml`` file to use as + a template for generating the new one for the current iteration. Useful if + you would like to start with the same parameters a previous training + iteration. None uses the default ``pose_cfg.yaml``. + + superanimal_name: Specify the superanimal name is transfer learning with + superanimal is desired. This makes sure the pose config template uses + superanimal configs as template. + + weight_init: Only for Engine.PYTORCH. Specify how model weights should be + initialized. The default mode uses transfer learning from ImageNet weights. + + engine: Whether to create a pose config for a Tensorflow or PyTorch model. + Defaults to the value specified in the project configuration file. If no + engine is specified for the project, defaults to + ``deeplabcut.compat.DEFAULT_ENGINE``. + + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None, default = None, + If using a conditional-top-down (CTD) net_type, this argument should be + specified. It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a + bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must + correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot + (ctd_conditions: tuple[int, str] | tuple[int, int]), which + respectively correspond to a bottom-up (BU) network type and a + particular snapshot name or index. + + Returns: + If training dataset was successfully created, a list of tuples is returned. + The first two elements in each tuple represent the training fraction and the + shuffle value. The last two elements in each tuple are arrays of integers + representing the training and test indices. + + Returns None if training dataset could not be created. + + Raises: + ValueError: If the shuffle from which to copy the data split doesn't exist. + """ + cfg = auxiliaryfunctions.read_config(config) + trainset_meta_path = metadata.TrainingDatasetMetadata.path(cfg) + if not trainset_meta_path.exists(): + meta = metadata.TrainingDatasetMetadata.create(cfg) + meta.save() + else: + meta = metadata.TrainingDatasetMetadata.load(cfg, load_splits=False) + + shuffle = meta.get(trainset_index=from_trainsetindex, index=from_shuffle) + shuffle = shuffle.load_split(cfg, trainset_path=trainset_meta_path.parent) + + num_copies = num_shuffles + if shuffles is not None: + num_copies = len(shuffles) + + # pad the train and test indices with -1s so the training fraction is exact + train_idx = list(shuffle.split.train_indices) + test_idx = list(shuffle.split.test_indices) + n_train, n_test = len(train_idx), len(test_idx) + + train_fraction = round(cfg["TrainingFraction"][from_trainsetindex], 2) + if round(n_train / (n_train + n_test), 2) != train_fraction: + train_padding, test_padding = _compute_padding(train_fraction, n_train, n_test) + train_idx = train_idx + (train_padding * [-1]) + test_idx = test_idx + (test_padding * [-1]) + + return create_training_dataset( + config=config, + num_shuffles=num_shuffles, + Shuffles=shuffles, + userfeedback=userfeedback, + trainIndices=[train_idx for _ in range(num_copies)], + testIndices=[test_idx for _ in range(num_copies)], + net_type=net_type, + detector_type=detector_type, + augmenter_type=augmenter_type, + posecfg_template=posecfg_template, + superanimal_name=superanimal_name, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + + +def _compute_padding( + train_fraction: float, + num_train: int, + num_test: int, +) -> tuple[int, int]: + """Computes the amount of padding to add to train/test indices such that + train_fraction = num_train / (num_train + num_test). + + Returns: + the number of padding indices to add to the train indices + the number of padding indices to add to the test indices + """ + if train_fraction <= 0 or train_fraction >= 1: + raise ValueError(f"The training fraction must satisfy 0 < TrainingFraction < 1, but {train_fraction} was found") + + base_images = 100 + train_step = int(round(round(train_fraction, 2) * base_images)) + test_step = base_images - train_step + + tgt_train = train_step + tgt_test = test_step + while tgt_train < num_train or tgt_test < num_test: + tgt_train += train_step + tgt_test += test_step + + return (tgt_train - num_train), (tgt_test - num_test) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index f164804876..cb3a747524 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -8,10 +8,16 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations + import os +from pathlib import Path from PySide6 import QtWidgets -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import QIcon + +from deeplabcut.core.config import read_config_as_dict from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import ConfigEditor @@ -54,7 +60,7 @@ def _create_grid_layout( alignment=None, spacing: int = 20, margins: tuple = None, -) -> QtWidgets.QGridLayout(): +) -> QtWidgets.QGridLayout: layout = QtWidgets.QGridLayout() layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) layout.setSpacing(spacing) @@ -64,6 +70,50 @@ def _create_grid_layout( return layout +def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int = 0): + """Safely replaces all items in a QComboBox and sets the current index, ensuring + that the `currentTextChanged` signal is emitted exactly once (and only if items are + present). + + This method suppresses intermediate signal emissions that can be triggered + by `clear()` and `addItems()` — both of which may emit multiple signals + depending on the underlying Qt model and signal connections. + + It also handles the edge case where the item at the target index is already + selected: by default, Qt will not emit a signal if the index doesn't change. + To ensure consistent behavior, this method temporarily sets the index to -1 + (i.e., no selection), which is done with signals blocked, then restores the + intended index — causing the signal to emit once and only once. + + Parameters: + combo_box (QComboBox): The combo box to update. + items (list of str): New items to populate the combo box. + index (int): The index to select after updating items. Defaults to 0. + + Note: + - If the items list is empty, no item will be selected and no signal will be emitted. + - This method is designed to be safe for use with PySide, where signals + cannot be manually emitted, and future-proof if multiple slots are connected. + """ + combo_box.blockSignals(True) + combo_box.clear() + combo_box.addItems(items) + combo_box.blockSignals(False) + + if not items: + combo_box.setCurrentIndex(-1) + return + + current = combo_box.currentIndex() + if current == index: + # Temporarily change index to suppress duplicate signal + combo_box.blockSignals(True) + combo_box.setCurrentIndex(-1) + combo_box.blockSignals(False) + + combo_box.setCurrentIndex(index) + + class BodypartListWidget(QtWidgets.QListWidget): def __init__( self, @@ -73,7 +123,7 @@ def __init__( # NOTE: Is there a case where a specific list should # have bodyparts other than the root? I don't think so. ): - super(BodypartListWidget, self).__init__() + super().__init__() self.root = root self.parent = parent @@ -89,47 +139,63 @@ def __init__( self.itemSelectionChanged.connect(self.update_selected_bodyparts) + def refresh(self): + self.clear() + self.addItems(self.root.all_bodyparts) + self.update_selected_bodyparts() + def update_selected_bodyparts(self): self.selected_bodyparts = [item.text() for item in self.selectedItems()] self.root.logger.info(f"Selected bodyparts:\n\t{self.selected_bodyparts}") class VideoSelectionWidget(QtWidgets.QWidget): - def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): - super(VideoSelectionWidget, self).__init__(parent) + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + *, + hide_videotype: bool = False, + sync_videotype_with_selection: bool = False, + strict_videotype_filter: bool = False, + ): + super().__init__(parent) self.root = root self.parent = parent - self._init_layout() + # Optional safeties; defaults preserve current behavior + self.sync_videotype_with_selection = sync_videotype_with_selection + self.strict_videotype_filter = strict_videotype_filter - def _init_layout(self): + self._init_layout(hide_videotype) + + def _init_layout(self, hide_videotype: bool): layout = _create_horizontal_layout() # Videotype selection self.videotype_widget = QtWidgets.QComboBox() self.videotype_widget.setMinimumWidth(100) self.videotype_widget.addItems(DLCParams.VIDEOTYPES) - self.videotype_widget.setCurrentText(self.root.video_type) + self.videotype_widget.setCurrentText(self._normalize_videotype(self.root.video_type)) self.root.video_type_.connect(self.videotype_widget.setCurrentText) self.videotype_widget.currentTextChanged.connect(self.update_videotype) # Select videos self.select_video_button = QtWidgets.QPushButton("Select videos") self.select_video_button.setMaximumWidth(200) - self.select_video_button.clicked.connect(self.select_videos) + self.select_video_button.clicked.connect(self.update_videos) self.root.video_files_.connect(self._update_video_selection) # Number of selected videos text - self.selected_videos_text = QtWidgets.QLabel( - "" - ) # updated when videos are selected + self.selected_videos_text = QtWidgets.QLabel("") # Clear video selection self.clear_videos = QtWidgets.QPushButton("Clear selection") self.clear_videos.clicked.connect(self.clear_selected_videos) - layout.addWidget(self.videotype_widget) + if not hide_videotype: + layout.addWidget(self.videotype_widget) layout.addWidget(self.select_video_button) layout.addWidget(self.selected_videos_text) layout.addWidget(self.clear_videos, alignment=Qt.AlignRight) @@ -140,40 +206,333 @@ def _init_layout(self): def files(self): return self.root.video_files - def update_videotype(self, vtype): + def _normalize_videotype(self, vtype: str) -> str: + return (vtype or "").lower().lstrip(".") + + @property + def selected_suffixes(self) -> set[str]: + """Return normalized suffixes (without leading dot) of currently selected files.""" + suffixes = set() + for f in self.files: + suffix = Path(f).suffix.lower().lstrip(".") + if suffix: + suffixes.add(suffix) + return suffixes + + def get_effective_videotype( + self, + prefer_selected_files: bool = False, + with_dot: bool = True, + ) -> str: + """ + Return the videotype to use. + + By default, preserves current behavior and uses the dropdown. + If prefer_selected_files=True and the selected files all share one suffix, + that suffix is used instead. + """ + videotype = self._normalize_videotype(self.videotype_widget.currentText()) + + if prefer_selected_files: + suffixes = self.selected_suffixes + if len(suffixes) == 1: + videotype = next(iter(suffixes)) + + if with_dot and videotype: + return f".{videotype}" + return videotype + + def get_files_grouped_by_suffix(self, keep_dot: bool = False) -> dict[str, list[str]]: + """Return a dict grouping selected files by their suffixes.""" + groups: dict[str, list[str]] = {} + for f in self.files: + suffix = Path(f).suffix.lower() + if not keep_dot: + suffix = suffix.lstrip(".") + groups.setdefault(suffix, []).append(f) + return groups + + def _all_supported_video_patterns(self) -> list[str]: + """Return all supported video patterns in both lower and upper case.""" + return [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [ + f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:] + ] + + def _build_video_filter(self) -> str: + """ + Build the file dialog filter. + + By default, preserve current behavior: show all supported video types. + If strict_videotype_filter is enabled, restrict to the currently selected + videotype when it is non-empty. If the current dropdown value is empty + (the "all types" option), fall back to the full supported-extension filter. + """ + all_video_types = self._all_supported_video_patterns() + + if self.strict_videotype_filter: + current = self.get_effective_videotype( + prefer_selected_files=False, + with_dot=False, + ) + + if current: + video_types = [f"*.{current.lower()}", f"*.{current.upper()}"] + else: + # "All types" entry selected: keep the dialog usable + video_types = all_video_types + else: + video_types = all_video_types + + return f"Videos ({' '.join(video_types)})" + + def _set_videotype_silently(self, vtype: str): + """ + Update the dropdown/root videotype without triggering update_videotype(), + because that method clears the current selection. + + Only updates state if the videotype is supported by the combo box. + Otherwise, leaves the current state unchanged. + """ + normalized = self._normalize_videotype(vtype) + current = self._normalize_videotype(self.videotype_widget.currentText()) + + if not normalized: + self.root.logger.warning("Attempted to set an empty videotype silently; keeping current selection.") + return + + # Validate against actual combo-box items + if self.videotype_widget.findText(normalized) == -1: + self.root.logger.warning( + f"Attempted to set unsupported videotype '{normalized}' silently; " + f"keeping current videotype '{current}'." + ) + return + + if normalized != current: + self.videotype_widget.blockSignals(True) + self.videotype_widget.setCurrentText(normalized) + self.videotype_widget.blockSignals(False) + + self.root.video_type = normalized + + def update_videotype(self, vtype: str): + normalized = self._normalize_videotype(vtype) self.clear_selected_videos() - self.root.video_type = vtype + self.root.video_type = normalized def _update_video_selection(self, videopaths): n_videos = len(self.root.video_files) if n_videos: - self.selected_videos_text.setText(f"{n_videos} videos selected") + suffixes = self.selected_suffixes + if len(suffixes) == 1: + suffix = next(iter(suffixes)) + self.selected_videos_text.setText(f"{n_videos} videos selected (.{suffix})") + elif len(suffixes) > 1: + counts = { + suffix: len(files) for suffix, files in self.get_files_grouped_by_suffix(keep_dot=False).items() + } + summary = ", ".join(f"{count} .{suffix}" for suffix, count in sorted(counts.items())) + self.selected_videos_text.setText( + f"{n_videos} videos selected ({summary}; will run in separate batches)" + ) + else: + self.selected_videos_text.setText(f"{n_videos} videos selected") + self.select_video_button.setText("Add more videos") else: self.selected_videos_text.setText("") self.select_video_button.setText("Select videos") - def select_videos(self): - cwd = self.root.project_folder + def update_videos(self): + directory_to_open = self.root.project_folder + video_filter = self._build_video_filter() + filenames = QtWidgets.QFileDialog.getOpenFileNames( - self, - "Select video(s) to analyze", - cwd, - f"Videos ({' *.'.join(DLCParams.VIDEOTYPES)[1:]})", + parent=self, + caption="Select video(s) to analyze", + dir=directory_to_open, + filter=video_filter, ) if filenames[0]: - # Qt returns a tuple (list of files, filetype) - self.root.video_files = [os.path.abspath(vid) for vid in filenames[0]] + abs_files = [os.path.abspath(vid) for vid in filenames[0]] + self.root.add_video_files(abs_files) + + # Optional safety: sync dropdown to selected file suffix + if self.sync_videotype_with_selection: + suffixes = {Path(v).suffix.lower().lstrip(".") for v in abs_files if Path(v).suffix} + + if len(suffixes) == 1: + inferred = next(iter(suffixes)) + self._set_videotype_silently(inferred) + self.root.logger.info(f"Inferred videotype '{inferred}' from selected file(s)") + elif len(suffixes) > 1: + self.root.logger.warning( + f"Selected videos have mixed suffixes {sorted(suffixes)}; " + "keeping current videotype dropdown unchanged." + ) def clear_selected_videos(self): - self.root.video_files = set() - self.root.logger.info(f"Cleared selected videos") + self.root.clear_video_files() + self.root.logger.info("Cleared selected videos") + + +class SnapshotSelectionWidget(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + margins: tuple, + select_button_text: str, + ): + super().__init__(parent) + self.root = root + self.parent = parent + self.selected_snapshot = None + self._init_layout(margins, select_button_text) + + def _init_layout(self, margins, select_button_text): + layout = _create_horizontal_layout(margins=margins) + + # Select snapshot + self.select_snapshot_button = QtWidgets.QPushButton(select_button_text) + self.select_snapshot_button.setMaximumWidth(200) + self.select_snapshot_button.clicked.connect(self.select_snapshot) + + # Selected snapshot text + self.selected_snapshot_text = QtWidgets.QLabel("") # updated when snapshot is selected + + # Clear snapshot selection + self.clear_snapshot_button = QtWidgets.QPushButton("Clear selection") + self.clear_snapshot_button.clicked.connect(self.clear_selected_snapshot) + self.clear_snapshot_button.hide() + + layout.addWidget(self.select_snapshot_button) + layout.addWidget(self.selected_snapshot_text) + layout.addWidget(self.clear_snapshot_button, alignment=Qt.AlignRight) + + self.setLayout(layout) + + def _update_selected_snapshot_display(self): + if self.selected_snapshot is None: + self.selected_snapshot_text.setText("") + self.clear_snapshot_button.hide() + else: + self.selected_snapshot_text.setText(f"{os.path.basename(self.selected_snapshot)}") + self.clear_snapshot_button.show() + + def select_snapshot(self): + # Create a filter string with both lowercase and uppercase extensions + snapshot_types = ["*.pt", "*.PT"] + snapshot_filter = f"Snapshots ({' '.join(snapshot_types)})" + + directory_to_open = self.root.models_folder + + selected_snapshot, _ = QtWidgets.QFileDialog.getOpenFileName( + parent=self, + caption="Select snapshot to start training from", + dir=directory_to_open, + filter=snapshot_filter, + ) + # When Canceling a file selection, Qt returns an empty string as selected file + if selected_snapshot: + self.selected_snapshot = os.path.abspath(selected_snapshot) + + self._update_selected_snapshot_display() + + def clear_selected_snapshot(self): + self.selected_snapshot = None + self._update_selected_snapshot_display() + + +class ConditionsSelectionWidget(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + ): + super().__init__(parent=parent) + self.root = root + self.parent = parent + self.selected_conditions = None + self._init_layout() + + def _init_layout(self): + layout = _create_horizontal_layout() + + # Select conditions + self.select_conditions_button = QtWidgets.QPushButton("Select conditions") + self.select_conditions_button.setMaximumWidth(200) + self.select_conditions_button.clicked.connect(self.select_conditions) + + # Selected conditions text + self.selected_conditions_text = QtWidgets.QLabel("") # updated when conditions are selected + + layout.addWidget(self.select_conditions_button) + layout.addWidget(self.selected_conditions_text) + + self.setLayout(layout) + + def _update_selected_conditions_display(self): + def _shorten_path(path: str, max_length: int = 30) -> str: + if len(path) <= max_length: + return path + return "..." + path[-(max_length - 3) :] + + self.selected_conditions_text.setText( + "" if self.selected_conditions is None else f"{_shorten_path(self.selected_conditions)}" + ) + + def select_conditions(self): + def _is_model_bu(selected_conditions) -> bool: + model_config_path = Path(selected_conditions).parent / "pytorch_config.yaml" + model_config = read_config_as_dict(model_config_path) + return model_config.get("method").lower() == "bu" + + # Create a filter string with both lowercase and uppercase extensions + snapshots_label = "Snapshots" + h5_predictions_label = "H5 predictions" + json_prediction_label = "Json predictions" + snapshot_types = ["*.pt", "*.PT"] + h5_predictions_types = ["*.h5", "*.H5"] + json_prediction_types = ["*.json", "*.JSON"] + conditions_filter = ";;".join( + [ + f"{snapshots_label} ({' '.join(snapshot_types)})", + f"{h5_predictions_label} ({' '.join(h5_predictions_types)})", + f"{json_prediction_label} ({' '.join(json_prediction_types)})", + ] + ) + + directory_to_open = self.root.project_folder + + selected_conditions, selected_filter = QtWidgets.QFileDialog.getOpenFileName( + parent=self, + caption="Select conditions to use during inference (snapshot or predictions file)", + dir=directory_to_open, + filter=conditions_filter, + ) + if selected_filter.startswith(snapshots_label) and selected_conditions: + if not _is_model_bu(selected_conditions): + msg = _create_message_box( + "Invalid conditions", + ( + f"The selected snapshot ({selected_conditions}) cannot be " + "used as conditions because it is not a Bottom-Up model." + ), + ) + msg.exec_() + selected_conditions = None + + # When Canceling a file selection, Qt returns an empty string as selected file + self.selected_conditions = str(os.path.abspath(selected_conditions)) if selected_conditions else None + + self._update_selected_conditions_display() class TrainingSetSpinBox(QtWidgets.QSpinBox): def __init__(self, root, parent): - super(TrainingSetSpinBox, self).__init__(parent) + super().__init__(parent) self.root = root self.parent = parent @@ -185,14 +544,20 @@ def __init__(self, root, parent): class ShuffleSpinBox(QtWidgets.QSpinBox): def __init__(self, root, parent): - super(ShuffleSpinBox, self).__init__(parent) + super().__init__(parent) self.root = root self.parent = parent - self.setMaximum(100) + self.setMaximum(10_000) self.setValue(self.root.shuffle_value) self.valueChanged.connect(self.root.update_shuffle) + self.root.shuffle_change.connect(self.update_shuffle) + + @Slot(int) + def update_shuffle(self, new_shuffle: int): + if new_shuffle != self.value(): + self.setValue(new_shuffle) class DefaultTab(QtWidgets.QWidget): @@ -202,7 +567,7 @@ def __init__( parent: QtWidgets.QWidget = None, h1_description: str = "", ): - super(DefaultTab, self).__init__(parent) + super().__init__(parent) self.parent = parent self.root = root @@ -217,9 +582,7 @@ def __init__( def _init_default_layout(self): # Add tab header - self.main_layout.addWidget( - _create_label_widget(self.h1_description, "font:bold;", (10, 10, 0, 10)) - ) + self.main_layout.addWidget(_create_label_widget(self.h1_description, "font:bold;", (10, 10, 0, 10))) # Add separating line self.separator = QtWidgets.QFrame() @@ -235,10 +598,8 @@ def _init_default_layout(self): class EditYamlButton(QtWidgets.QPushButton): - def __init__( - self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None - ): - super(EditYamlButton, self).__init__(button_label) + def __init__(self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None): + super().__init__(parent) self.filepath = filepath self.parent = parent @@ -260,7 +621,7 @@ def __init__( file_text: str = None, parent=None, ): - super(BrowseFilesButton, self).__init__(button_label) + super().__init__(parent) self.filetype = filetype self.single_file_only = single_file self.cwd = cwd @@ -301,3 +662,48 @@ def browse_files(self): if filepaths: self.files.update(filepaths[0]) + + +def _create_message_box(text, info_text): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setText(text) + msg.setInformativeText(info_text) + + msg.setWindowTitle("Info") + msg.setMinimumWidth(900) + logo_dir = os.path.dirname(os.path.realpath("logo.png")) + os.path.sep + logo = logo_dir + "/assets/logo.png" + msg.setWindowIcon(QIcon(logo)) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + return msg + + +def _create_confirmation_box(title, description): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setText(title) + msg.setInformativeText(description) + + msg.setWindowTitle("Confirmation") + msg.setMinimumWidth(900) + logo_dir = os.path.dirname(os.path.realpath("logo.png")) + os.path.sep + logo = logo_dir + "/assets/logo.png" + msg.setWindowIcon(QIcon(logo)) + msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + return msg + + +def set_layout_contents_visible(layout: QtWidgets.QLayout, visible: bool): + for i in range(layout.count()): + item = layout.itemAt(i) + + # If it's a widget item + widget = item.widget() + if widget is not None: + widget.setVisible(visible) + + # If it's a nested layout + child_layout = item.layout() + if child_layout is not None: + set_layout_contents_visible(child_layout, visible) diff --git a/deeplabcut/gui/dialogs/__init__.py b/deeplabcut/gui/dialogs/__init__.py new file mode 100644 index 0000000000..abf3848d70 --- /dev/null +++ b/deeplabcut/gui/dialogs/__init__.py @@ -0,0 +1,28 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from collections.abc import Sequence + +from .debug_dialog import ( + DebugTextDialog, + create_generate_debug_log_action, + make_issue_report_provider, + make_log_text_provider, + show_debug_report_dialog, +) + +__all__: Sequence[str] = ( + "DebugTextDialog", + "create_generate_debug_log_action", + "make_issue_report_provider", + "make_log_text_provider", + "show_debug_report_dialog", +) diff --git a/deeplabcut/gui/dialogs/debug_dialog.py b/deeplabcut/gui/dialogs/debug_dialog.py new file mode 100644 index 0000000000..788284d631 --- /dev/null +++ b/deeplabcut/gui/dialogs/debug_dialog.py @@ -0,0 +1,321 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from __future__ import annotations + +from collections.abc import Callable, Iterable + +from PySide6 import QtGui +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QFontDatabase, QKeySequence, QTextCursor +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QHBoxLayout, + QLabel, + QPlainTextEdit, + QPushButton, + QVBoxLayout, + QWidget, +) + +from deeplabcut.core.debug import ( + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + build_debug_report, + get_debug_recorder, + install_debug_recorder, +) + + +def make_log_text_provider( + *, + recorder: InMemoryDebugRecorder | None, + limit: int = 300, +) -> Callable[[], str]: + """Return a callable that renders recent captured logs.""" + + def _provider() -> str: + if recorder is None: + return "" + return recorder.render_text(limit=limit) + + return _provider + + +def make_issue_report_provider( + *, + recorder: InMemoryDebugRecorder | None, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, +) -> Callable[[], str]: + """Return a callable that builds a full DLC debug report. + + ``libraries`` and ``executables`` are normalized to tuples so the returned + provider can be called repeatedly even if the caller passed a generator or + another one-shot iterable. + """ + libraries_snapshot = None if libraries is None else tuple(libraries) + executables_snapshot = None if executables is None else tuple(executables) + + def _provider() -> str: + return build_debug_report( + recorder=recorder, + libraries=libraries_snapshot, + executables=executables_snapshot, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + ) + + return _provider + + +class DebugTextDialog(QDialog): + """ + Minimal, application-agnostic debug text viewer. + + This widget only knows how to: + - fetch text from a callable + - display it read-only + - copy it to clipboard + - refresh it on demand + + It intentionally knows nothing about: + - recorder internals + - DLC main window internals + - environment/report formatting + """ + + def __init__( + self, + *, + title: str, + text_provider: Callable[[], str], + parent: QWidget | None = None, + initial_hint: str = "Read-only diagnostic output", + ) -> None: + super().__init__(parent=parent) + self.setWindowTitle(title) + self.setModal(False) + self.resize(950, 700) + + self._text_provider = text_provider + + self._build_ui(initial_hint=initial_hint) + + def update_content( + self, + *, + title: str | None = None, + text_provider: Callable[[], str] | None = None, + hint: str | None = None, + ) -> None: + """Update dialog metadata when reusing an existing instance.""" + if title is not None: + self.setWindowTitle(title) + if text_provider is not None: + self._text_provider = text_provider + if hint is not None: + self._hint_label.setText(hint) + + def _build_ui(self, *, initial_hint: str) -> None: + layout = QVBoxLayout(self) + + self._hint_label = QLabel(initial_hint, self) + self._hint_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + layout.addWidget(self._hint_label) + + self._text_edit = QPlainTextEdit(self) + self._text_edit.setReadOnly(True) + self._text_edit.setLineWrapMode(QPlainTextEdit.NoWrap) + + # Use a fixed-width system font for logs / reports + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + self._text_edit.setFont(font) + + layout.addWidget(self._text_edit, stretch=1) + + button_row = QHBoxLayout() + + self._status_label = QLabel("", self) + self._status_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + button_row.addWidget(self._status_label, stretch=1) + + self._refresh_btn = QPushButton("Refresh", self) + self._refresh_btn.clicked.connect(self.refresh_text) + button_row.addWidget(self._refresh_btn) + + self._copy_btn = QPushButton("Copy to clipboard", self) + self._copy_btn.clicked.connect(self.copy_to_clipboard) + button_row.addWidget(self._copy_btn) + + self._close_btn = QPushButton("Close", self) + self._close_btn.clicked.connect(self.close) + button_row.addWidget(self._close_btn) + + layout.addLayout(button_row) + + # Optional keyboard shortcut + copy_action = QAction(self) + copy_action.setShortcut(QKeySequence.StandardKey.Copy) + copy_action.triggered.connect(self.copy_to_clipboard) + self.addAction(copy_action) + + def refresh_text(self) -> None: + try: + QApplication.setOverrideCursor(Qt.WaitCursor) + text = self._text_provider() + except Exception as exc: + text = f"[debug-dialog] failed to build debug text\n\n{exc!r}" + finally: + QApplication.restoreOverrideCursor() + + self._text_edit.setPlainText(text or "") + self._text_edit.moveCursor(QTextCursor.MoveOperation.Start) + self._status_label.setText("") + + def copy_to_clipboard(self) -> None: + try: + text = self._text_edit.toPlainText() + QApplication.clipboard().setText(text) + self._status_label.setText("Copied to clipboard") + except Exception: + self._status_label.setText("Could not copy to clipboard") + + def showEvent(self, event: QtGui.QShowEvent) -> None: + """Refresh each time the dialog becomes visible.""" + super().showEvent(event) + self.refresh_text() + + +def _get_or_create_debug_dialog( + *, + parent: QWidget, + title: str, + text_provider: Callable[[], str], + text_hint: str, + attr_name: str = "_dlc_debug_dialog", +) -> DebugTextDialog: + """ + Reuse a single dialog instance attached to ``parent``. + + Storing the dialog on the main window avoids accidental garbage collection + and prevents opening a pile of duplicate windows. + """ + dlg = getattr(parent, attr_name, None) + if isinstance(dlg, DebugTextDialog): + dlg.update_content( + title=title, + text_provider=text_provider, + hint=text_hint, + ) + return dlg + + dlg = DebugTextDialog( + title=title, + text_provider=text_provider, + parent=parent, + initial_hint=text_hint, + ) + setattr(parent, attr_name, dlg) + return dlg + + +def show_debug_report_dialog( + *, + parent: QWidget, + recorder: InMemoryDebugRecorder | None = None, + logger_name: str = "deeplabcut", + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, + dialog_attr_name: str = "_dlc_debug_dialog", +) -> DebugTextDialog: + """ + Open (or reuse) the full diagnostic report dialog. + + If ``recorder`` is not provided, this function tries to reuse an existing + recorder for the given logger namespace and installs one if missing. + """ + if recorder is None: + recorder = get_debug_recorder(logger_name=logger_name) + if recorder is None: + recorder = install_debug_recorder(logger_name=logger_name) + + provider = make_issue_report_provider( + recorder=recorder, + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + ) + + dlg = _get_or_create_debug_dialog( + parent=parent, + title="DeepLabCut debug log", + text_provider=provider, + text_hint=("Diagnostic report for issue reporting. Use Refresh to update, then Copy to clipboard."), + attr_name=dialog_attr_name, + ) + # dlg.refresh_text() # redundant + dlg.show() + dlg.raise_() + dlg.activateWindow() + return dlg + + +def create_generate_debug_log_action( + *, + parent: QWidget, + recorder: InMemoryDebugRecorder | None = None, + logger_name: str = "deeplabcut", + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, + text: str = "&Generate debug log...", + status_tip: str = "Generate a diagnostic report for troubleshooting", + dialog_attr_name: str = "_dlc_debug_dialog", +) -> QAction: + """ + Create a QAction that opens the DLC debug report dialog. + + Typical usage in ``MainWindow.create_actions``:: + + self.generateDebugLogAction = create_generate_debug_log_action(parent=self) + """ + action = QAction(text, parent) + action.setStatusTip(status_tip) + + def _open_dialog() -> None: + show_debug_report_dialog( + parent=parent, + recorder=recorder, + logger_name=logger_name, + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + dialog_attr_name=dialog_attr_name, + ) + + action.triggered.connect(_open_dialog) + return action diff --git a/deeplabcut/gui/displays/__init__.py b/deeplabcut/gui/displays/__init__.py new file mode 100644 index 0000000000..117d127147 --- /dev/null +++ b/deeplabcut/gui/displays/__init__.py @@ -0,0 +1,10 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# diff --git a/deeplabcut/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py new file mode 100644 index 0000000000..d1ae3732b1 --- /dev/null +++ b/deeplabcut/gui/displays/selected_shuffle_display.py @@ -0,0 +1,125 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Module to display information about the selected shuffle in the GUI.""" + +from __future__ import annotations + +from pathlib import Path + +import PySide6.QtCore as QtCore +from PySide6 import QtWidgets + +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + + +class SelectedShuffleDisplay(QtWidgets.QWidget): + """A widget displaying information about the selected shuffle.""" + + pose_cfg_signal = QtCore.Signal(dict) + + def __init__(self, root, row_margin: int = 25): + super().__init__() + self.root = root + + self._row_margin = row_margin + + self._current_index: int | None = None + self._engine: Engine | None = None + self._is_top_down: bool = False + self._net_type: str | None = None + self._pose_cfg: dict | None = None + + self._label = QtWidgets.QLabel("Shuffle info:") + self._label.setStyleSheet(f"margin: 0px 0px {self._row_margin}px 0px") + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self._label) + self.setLayout(layout) + + # initialize the display + self._update_display(self.root.shuffle_value) + + # update the display when the shuffle or selected engine changes, or when a new + # shuffle has been created + self.root.shuffle_change.connect(self._update_display) + self.root.engine_change.connect(self._update_display) + self.root.shuffle_created.connect(self._update_display) + + @property + def pose_cfg(self) -> dict | None: + return self._pose_cfg + + @pose_cfg.setter + def pose_cfg(self, value: dict | None) -> None: + self._pose_cfg = value + self.pose_cfg_signal.emit(self._pose_cfg) + + @QtCore.Slot(int) + def _update_display(self, new_index: int) -> None: + self._current_index = new_index + + try: + pose_cfg_path = Path(self.root.pose_cfg_path) + except ValueError: + self._set_text_error(f"Failed to read shuffle {self._current_index} - check that it exists!") + return + except ModuleNotFoundError as err: + # Loading a TF shuffle but TF is not installed + self._set_text_error( + f"Failed to read shuffle {self._current_index} due to error `{err}`.\n" + "If the error is `ModuleNotFoundError: No module named 'tensorflow'`, " + f"this is because\nshuffle {self._current_index} uses the tensorflow " + " engine, but TensorFlow is not installed in your environment.\n" + "Ignore this error if you'll just train PyTorch models. To train " + "TensorFlow models, install it with \n" + " Windows/Linux: pip install 'deeplabcut[tf]'\n" + " Apple Silicon: pip install 'deeplabcut[apple_mchips]'" + ) + return + + if not pose_cfg_path.exists(): + self._set_text_error(f"The model configuration file {pose_cfg_path} was not created") + return + + self._read_pose_config(pose_cfg_path) + self._set_text() + + def _set_text(self) -> None: + engine_str = "None" + if self._engine is not None: + engine_str = self._engine.aliases[0] + + text = f"net type: {self._net_type} | engine: {engine_str}" + if self._engine == Engine.PYTORCH and self._is_top_down: + text += " | top-down" + + style = f"margin: 0px 0px {self._row_margin}px 0px;" + if self._engine != self.root.engine: + warning = "Change the selected Engine in the top-right to use this shuffle!" + text = warning + " | " + text + style += " color: orange;" + + self._label.setStyleSheet(style) + self._label.setText(text) + + def _set_text_error(self, error: str) -> None: + self._label.setText(error) + style = f"margin: 0px 0px {self._row_margin}px 0px; color: orange;" + self._label.setStyleSheet(style) + self.pose_cfg = None + + def _read_pose_config(self, pose_cfg_path: Path) -> None: + pose_cfg = auxiliaryfunctions.read_plainconfig(str(pose_cfg_path)) + + self._engine = Engine.PYTORCH if "pytorch" in pose_cfg_path.stem.lower() else Engine.TF + self._net_type = pose_cfg.get("net_type", "UNKNOWN") + self._is_top_down = self._engine == Engine.PYTORCH and pose_cfg.get("method").lower() == "td" + self.pose_cfg = pose_cfg diff --git a/deeplabcut/gui/displays/shuffle_metadata_viewer.py b/deeplabcut/gui/displays/shuffle_metadata_viewer.py new file mode 100644 index 0000000000..ec44d5a725 --- /dev/null +++ b/deeplabcut/gui/displays/shuffle_metadata_viewer.py @@ -0,0 +1,63 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Widget to display existing shuffles.""" + +from __future__ import annotations + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +import deeplabcut.generate_training_dataset.metadata as metadata + + +class ShuffleMetadataViewer(QtWidgets.QDialog): + """Viewer for shuffle metadata.""" + + def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): + super().__init__(parent) + self.root = root + self.parent = parent + self.file_content = _load_metadata(self.root.cfg) + + self.setWindowTitle("Existing Shuffles: Metadata") + self.setMinimumWidth(400) + self.setMinimumHeight(400) + + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + + inner_layout = QtWidgets.QVBoxLayout() + inner_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + inner_layout.setSpacing(0) + inner_layout.setContentsMargins(0, 0, 0, 0) + + for line in self.file_content: + inner_layout.addWidget(QtWidgets.QLabel(line)) + + inner = QtWidgets.QFrame(scroll) + inner.setLayout(inner_layout) + scroll.setWidget(inner) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(scroll) + self.setLayout(layout) + + +def _load_metadata(cfg: dict) -> list[str]: + metadata_path = metadata.TrainingDatasetMetadata.path(cfg) + if not metadata_path.exists(): + trainset_meta = metadata.TrainingDatasetMetadata.create(cfg) + trainset_meta.save() + + with open(metadata_path) as file: + raw_metadata = file.read() + + return raw_metadata.split("\n") diff --git a/deeplabcut/gui/dlc_params.py b/deeplabcut/gui/dlc_params.py index 2a267aac67..fce5d15a52 100644 --- a/deeplabcut/gui/dlc_params.py +++ b/deeplabcut/gui/dlc_params.py @@ -13,6 +13,7 @@ class DLCParams: "", "avi", "mp4", + "mkv", "mov", ] @@ -30,8 +31,6 @@ class DLCParams: "efficientnet-b6", ] - IMAGE_AUGMENTERS = ["default", "tensorpack", "imgaug"] - FRAME_EXTRACTION_ALGORITHMS = ["kmeans", "uniform"] OUTLIER_EXTRACTION_ALGORITHMS = ["jump", "fitting", "uncertain", "manual"] diff --git a/deeplabcut/gui/launch_script.py b/deeplabcut/gui/launch_script.py index 6ada864102..698070c0e0 100644 --- a/deeplabcut/gui/launch_script.py +++ b/deeplabcut/gui/launch_script.py @@ -18,16 +18,17 @@ Licensed under GNU Lesser General Public License v3.0 """ -import sys + import os -import logging +import sys import PySide6.QtWidgets as QtWidgets import qdarkstyle -from deeplabcut.gui import BASE_DIR from PySide6.QtCore import Qt from PySide6.QtGui import QIcon, QPixmap +from deeplabcut.gui import BASE_DIR + def launch_dlc(): app = QtWidgets.QApplication(sys.argv) @@ -40,7 +41,7 @@ def launch_dlc(): splash.show() stylefile = os.path.join(BASE_DIR, "style.qss") - with open(stylefile, "r") as f: + with open(stylefile) as f: app.setStyleSheet(f.read()) dark_stylesheet = qdarkstyle.load_stylesheet_pyside2() diff --git a/deeplabcut/gui/media/dlc-pt.png b/deeplabcut/gui/media/dlc-pt.png new file mode 100644 index 0000000000..d0ac99c187 Binary files /dev/null and b/deeplabcut/gui/media/dlc-pt.png differ diff --git a/deeplabcut/gui/media/dlc-tf.png b/deeplabcut/gui/media/dlc-tf.png new file mode 100644 index 0000000000..79d06f0528 Binary files /dev/null and b/deeplabcut/gui/media/dlc-tf.png differ diff --git a/deeplabcut/gui/style.qss b/deeplabcut/gui/style.qss index 19164d5d57..feaf66e3d4 100644 --- a/deeplabcut/gui/style.qss +++ b/deeplabcut/gui/style.qss @@ -1,8 +1,8 @@ - /* + /* Variables used -------------- - widgets height: 25px + widgets height: 25px */ @@ -28,4 +28,4 @@ QComboBox{ QLineEdit{ height: 25px; -} \ No newline at end of file +} diff --git a/deeplabcut/gui/tabs/analyze_videos.py b/deeplabcut/gui/tabs/analyze_videos.py index 40f651f044..1131ebfc3a 100644 --- a/deeplabcut/gui/tabs/analyze_videos.py +++ b/deeplabcut/gui/tabs/analyze_videos.py @@ -8,30 +8,51 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from dataclasses import dataclass from functools import partial +from pathlib import Path + from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.utils import move_to_separate_thread -from deeplabcut.gui.widgets import ConfigEditor +import deeplabcut from deeplabcut.gui.components import ( - DefaultTab, BodypartListWidget, + DefaultTab, ShuffleSpinBox, VideoSelectionWidget, _create_grid_layout, - _create_label_widget, _create_horizontal_layout, + _create_label_widget, _create_vertical_layout, ) - -import deeplabcut +from deeplabcut.gui.utils import move_to_separate_thread +from deeplabcut.gui.widgets import ConfigEditor from deeplabcut.utils.auxiliaryfunctions import edit_config +@dataclass(frozen=True) +class AnalyzeVideosOptions: + config: str + shuffle: int + save_as_csv: bool + filter_data: bool + plot_trajectories: bool + show_trajectory_plots: bool + displayed_bodyparts: tuple[str, ...] + create_video_all_detections: bool + auto_track: bool + calibrate_assembly: bool + assemble_with_ID_only: bool + num_animals_in_videos: int | None + cropping: tuple[int, int, int, int] | None + dynamic_cropping_params: tuple[bool, float, int] + track_method: str | None + + class AnalyzeVideos(DefaultTab): def __init__(self, root, parent, h1_description): - super(AnalyzeVideos, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() @@ -41,7 +62,9 @@ def files(self): def _set_page(self): self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) - self.video_selection_widget = VideoSelectionWidget(self.root, self) + self.video_selection_widget = VideoSelectionWidget( + self.root, self, hide_videotype=True, sync_videotype_with_selection=True + ) self.main_layout.addWidget(self.video_selection_widget) tmp_layout = _create_horizontal_layout() @@ -114,12 +137,6 @@ def _generate_layout_other_options(self, layout): tmp_layout.addWidget(self.save_as_csv) - self.save_as_nwb = QtWidgets.QCheckBox("Save result(s) as nwb") - self.save_as_nwb.setCheckState(Qt.Unchecked) - self.save_as_nwb.stateChanged.connect(self.update_nwb_choice) - - tmp_layout.addWidget(self.save_as_csv) - # Filter predictions self.filter_predictions = QtWidgets.QCheckBox("Filter predictions") self.filter_predictions.setCheckState(Qt.Unchecked) @@ -178,66 +195,52 @@ def _generate_layout_multianimal(self, layout): self.calibrate_assembly_checkbox = QtWidgets.QCheckBox("Calibrate assembly") self.calibrate_assembly_checkbox.setCheckState(Qt.Unchecked) - self.calibrate_assembly_checkbox.stateChanged.connect( - self.update_calibrate_assembly - ) + self.calibrate_assembly_checkbox.stateChanged.connect(self.update_calibrate_assembly) tmp_layout.addWidget(self.calibrate_assembly_checkbox, 0, 2) - self.assemble_with_ID_only_checkbox = QtWidgets.QCheckBox( - "Assemble with ID only" - ) + self.assemble_with_ID_only_checkbox = QtWidgets.QCheckBox("Assemble with ID only") self.assemble_with_ID_only_checkbox.setCheckState(Qt.Unchecked) - self.assemble_with_ID_only_checkbox.stateChanged.connect( - self.update_assemble_with_ID_only - ) + self.assemble_with_ID_only_checkbox.stateChanged.connect(self.update_assemble_with_ID_only) tmp_layout.addWidget(self.assemble_with_ID_only_checkbox, 0, 3) - self.create_detections_video_checkbox = QtWidgets.QCheckBox( - "Create video with all detections" - ) + self.create_detections_video_checkbox = QtWidgets.QCheckBox("Create video with all detections") self.create_detections_video_checkbox.setCheckState(Qt.Unchecked) - self.create_detections_video_checkbox.stateChanged.connect( - self.update_create_video_detections - ) + self.create_detections_video_checkbox.stateChanged.connect(self.update_create_video_detections) tmp_layout.addWidget(self.create_detections_video_checkbox, 0, 4) layout.addLayout(tmp_layout) def update_create_video_detections(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Create video with all detections {s}") def update_assemble_with_ID_only(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Assembly with ID only {s}") def update_calibrate_assembly(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Assembly calibration {s}") def update_tracker_type(self, method): self.root.logger.info(f"Using {method.upper()} tracker") def update_csv_choice(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Save results as CSV {s}") - def update_nwb_choice(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" - self.root.logger.info(f"Save results as NWB {s}") - def update_filter_choice(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Filtering predictions {s}") def update_showfigs_choice(self, state): - if state == Qt.Checked: + if Qt.CheckState(state) == Qt.Checked: self.root.logger.info("Plots will show as pop ups.") else: self.root.logger.info("Plots will not show up.") def update_crop_choice(self, state): - if state == Qt.Checked: + if Qt.CheckState(state) == Qt.Checked: self.root.logger.info("Dynamic bodypart cropping ENABLED.") self.dynamic_cropping = True else: @@ -245,7 +248,8 @@ def update_crop_choice(self, state): self.dynamic_cropping = False def update_plot_trajectory_choice(self, state): - if state == Qt.Checked: + if Qt.CheckState(state) == Qt.Checked: + self.bodyparts_list_widget.refresh() self.bodyparts_list_widget.show() self.bodyparts_list_widget.setEnabled(True) self.show_trajectory_plots.setEnabled(True) @@ -264,32 +268,31 @@ def edit_config_file(self): editor = ConfigEditor(self.root.config) editor.show() - def analyze_videos(self): + def _collect_options(self) -> AnalyzeVideosOptions: config = self.root.config shuffle = self.root.shuffle_value - - videos = list(self.files) - save_as_csv = self.save_as_csv.checkState() == Qt.Checked - videotype = self.video_selection_widget.videotype_widget.currentText() + save_as_csv = self.save_as_csv.isChecked() + filter_data = self.filter_predictions.isChecked() + plot_trajectories = self.plot_trajectories.isChecked() + show_trajectory_plots = self.show_trajectory_plots.isChecked() + displayed_bodyparts = tuple(self.bodyparts_list_widget.selected_bodyparts) if plot_trajectories else () if self.root.is_multianimal: - calibrate_assembly = ( - self.calibrate_assembly_checkbox.checkState() == Qt.Checked - ) - assemble_with_ID_only = ( - self.assemble_with_ID_only_checkbox.checkState() == Qt.Checked - ) + calibrate_assembly = self.calibrate_assembly_checkbox.isChecked() + assemble_with_ID_only = self.assemble_with_ID_only_checkbox.isChecked() track_method = self.tracker_type_widget.currentText() - edit_config(self.root.config, {"default_track_method": track_method}) num_animals_in_videos = self.num_animals_in_videos.value() + create_video_all_detections = self.create_detections_video_checkbox.isChecked() else: calibrate_assembly = False - num_animals_in_videos = None assemble_with_ID_only = False + track_method = None + num_animals_in_videos = None + create_video_all_detections = False cropping = None - - if self.root.cfg["cropping"] == "True": + crop_flag = self.root.cfg.get("cropping", False) + if str(crop_flag).lower() == "true": cropping = ( self.root.cfg["x1"], self.root.cfg["x2"], @@ -298,94 +301,143 @@ def analyze_videos(self): ) dynamic_cropping_params = (False, 0.5, 10) - try: - if self.dynamic_cropping: - dynamic_cropping_params = (True, 0.5, 10) - except AttributeError: - pass - - func = partial( - deeplabcut.analyze_videos, - config, - videos=videos, - videotype=videotype, + if getattr(self, "dynamic_cropping", False): + dynamic_cropping_params = (True, 0.5, 10) + + return AnalyzeVideosOptions( + config=config, shuffle=shuffle, save_as_csv=save_as_csv, - cropping=cropping, - dynamic=dynamic_cropping_params, + filter_data=filter_data, + plot_trajectories=plot_trajectories, + show_trajectory_plots=show_trajectory_plots, + displayed_bodyparts=displayed_bodyparts, + create_video_all_detections=create_video_all_detections, auto_track=self.root.is_multianimal, - n_tracks=num_animals_in_videos, - calibrate=calibrate_assembly, - identity_only=assemble_with_ID_only, + calibrate_assembly=calibrate_assembly, + assemble_with_ID_only=assemble_with_ID_only, + num_animals_in_videos=num_animals_in_videos, + cropping=cropping, + dynamic_cropping_params=dynamic_cropping_params, + track_method=track_method, ) - self.worker, self.thread = move_to_separate_thread(func) - self.worker.finished.connect(lambda: self.analyze_videos_btn.setEnabled(True)) - self.worker.finished.connect(lambda: self.root._progress_bar.hide()) - self.worker.finished.connect(lambda: self.run_enabled()) - self.thread.start() - self.analyze_videos_btn.setEnabled(False) - self.root._progress_bar.show() - - def run_enabled(self): - config = self.root.config - shuffle = self.root.shuffle_value - - videos = list(self.files) - save_as_csv = self.save_as_csv.checkState() == Qt.Checked - save_as_nwb = self.save_as_nwb.checkState() == Qt.Checked - filter_data = self.filter_predictions.checkState() == Qt.Checked - videotype = self.video_selection_widget.videotype_widget.currentText() - try: - create_video_all_detections = ( - self.create_detections_video_checkbox.checkState() == Qt.Checked - ) - except AttributeError: - create_video_all_detections = False - if create_video_all_detections: + def _get_video_batches(self): + """ + Returns a list of (videotype, videos) pairs. + videotype should include the leading dot, e.g. '.avi'. + """ + groups = self.video_selection_widget.get_files_grouped_by_suffix(keep_dot=True) + batches = [(suffix, videos) for suffix, videos in sorted(groups.items()) if suffix] + return batches + + def _get_unique_video_parent_folders(self, batches: list[tuple[str, list[str]]]) -> list[str]: + folders = [] + seen = set() + + for _, videos in batches: + for video in videos: + parent = str(Path(video).parent.resolve()) + if parent not in seen: + seen.add(parent) + folders.append(parent) + + return folders + + def _run_pipeline(self, options: AnalyzeVideosOptions, batches: list[tuple[str, list[str]]]): + for videotype, videos in batches: + try: + self.root.logger.info(f"Analyzing {len(videos)} video(s) with extension {videotype}") + + deeplabcut.analyze_videos( + options.config, + videos=videos, + video_extensions=videotype, + shuffle=options.shuffle, + save_as_csv=options.save_as_csv, + cropping=options.cropping, + dynamic=options.dynamic_cropping_params, + auto_track=options.auto_track, + n_tracks=options.num_animals_in_videos, + calibrate=options.calibrate_assembly, + identity_only=options.assemble_with_ID_only, + ) + + self._run_postprocessing_for_group(options, videotype, videos) + except Exception as e: + exc = f"Error analyzing videos {videos} with extension {videotype}: {e}" + self.root.logger.error(exc, exc_info=True) + raise RuntimeError(exc) from e + + # Run CSV conversion once per unique folder, after all batches + if options.auto_track and options.save_as_csv: + self._convert_outputs_to_csv_once_per_folder(batches) + + def _run_postprocessing_for_group( + self, + options: AnalyzeVideosOptions, + videotype: str, + videos: list[str], + ): + if options.create_video_all_detections: deeplabcut.create_video_with_all_detections( - config, + options.config, videos=videos, - videotype=videotype, - shuffle=shuffle, + video_extensions=videotype, + shuffle=options.shuffle, ) - if filter_data: + if options.filter_data: deeplabcut.filterpredictions( - config, + options.config, video=videos, - videotype=videotype, - shuffle=shuffle, + video_extensions=videotype, + shuffle=options.shuffle, filtertype="median", windowlength=5, - save_as_csv=save_as_csv, + save_as_csv=options.save_as_csv, + track_method=options.track_method, ) - if self.plot_trajectories.checkState() == Qt.Checked: - bdpts = self.bodyparts_list_widget.selected_bodyparts - self.root.logger.debug( - f"Selected body parts for plot_trajectories: {bdpts}" - ) - showfig = self.show_trajectory_plots.checkState() == Qt.Checked + if options.plot_trajectories: deeplabcut.plot_trajectories( - config, + options.config, videos=videos, - displayedbodyparts=bdpts, - videotype=videotype, - shuffle=shuffle, - filtered=filter_data, - showfigures=showfig, + displayedbodyparts=options.displayed_bodyparts, + video_extensions=videotype, + shuffle=options.shuffle, + filtered=options.filter_data, + showfigures=options.show_trajectory_plots, + track_method=options.track_method, ) - if self.root.is_multianimal and save_as_csv: + def _convert_outputs_to_csv_once_per_folder(self, batches: list[tuple[str, list[str]]]): + folders = self._get_unique_video_parent_folders(batches) + + for folder in folders: + self.root.logger.info(f"Converting H5 outputs to CSV in folder: {folder}") deeplabcut.analyze_videos_converth5_to_csv( - videos, - listofvideos=True, + folder, + listofvideos=False, ) - if save_as_nwb: - deeplabcut.analyze_videos_converth5_to_nwb( - config, - videos, - listofvideos=True, - ) + def analyze_videos(self): + options = self._collect_options() + batches = self._get_video_batches() + + if not batches: + self.root.logger.warning("No videos selected.") + return + + # Keep config in sync with GUI choice before launching worker + if self.root.is_multianimal and options.track_method is not None: + edit_config(self.root.config, {"default_track_method": options.track_method}) + + func = partial(self._run_pipeline, options, batches) + + self.worker, self.thread = move_to_separate_thread(func) + self.worker.finished.connect(lambda: self.analyze_videos_btn.setEnabled(True)) + self.worker.finished.connect(lambda: self.root._progress_bar.hide()) + self.thread.start() + self.analyze_videos_btn.setEnabled(False) + self.root._progress_bar.show() diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py index 6eaf66fa38..615806df27 100644 --- a/deeplabcut/gui/tabs/create_project.py +++ b/deeplabcut/gui/tabs/create_project.py @@ -4,26 +4,183 @@ # https://github.com/DeepLabCut/DeepLabCut # # Please see AUTHORS for contributors. -# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS # # Licensed under GNU Lesser General Public License v3.0 # import os from datetime import datetime -import deeplabcut -from deeplabcut.utils import auxiliaryfunctions +from PySide6 import QtCore, QtWidgets +from PySide6.QtGui import QBrush, QColor, QDesktopServices, QIcon, QPainter, QPen + +from deeplabcut.create_project import create_new_project, create_new_project_3d from deeplabcut.gui import BASE_DIR from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.tabs.docs import ( + URL_3D, + URL_MA_CONFIGURE, + URL_USE_GUIDE_SCENARIO, +) from deeplabcut.gui.widgets import ClickableLabel, ItemSelectionFrame +from deeplabcut.utils import auxiliaryfunctions -from PySide6 import QtCore, QtWidgets -from PySide6.QtGui import QIcon + +class DynamicTextList(QtWidgets.QWidget): + """Dynamically add text entries.""" + + def __init__(self, label_text="bodyparts", parent=None): + super().__init__(parent) + self.label_text = label_text + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + # Set maximum width for the widget + self.setMaximumWidth(300) + + # Add explanatory label + label = QtWidgets.QLabel(label_text) + self.layout.addWidget(label) + + # Create scroll area and its widget + self.scroll = QtWidgets.QScrollArea() + self.scroll.setWidgetResizable(True) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.scroll.setFrameShape(QtWidgets.QFrame.NoFrame) # Remove frame border + + # Create widget to hold the entries + self.entries_widget = QtWidgets.QWidget() + self.entries_layout = QtWidgets.QVBoxLayout(self.entries_widget) + self.entries_layout.setContentsMargins(0, 0, 0, 0) + self.entries_layout.setSpacing(5) # Consistent spacing between entries + self.entries_layout.setAlignment(QtCore.Qt.AlignTop) # Align entries to top + + # Add stretch at the bottom to keep entries at top + self.entries_layout.addStretch() + + self.scroll.setWidget(self.entries_widget) + + # Set fixed height for 6 items + self.entry_height = 30 # Fixed height for each entry + self.padding = 10 # Extra padding + self.scroll.setFixedHeight(5 * self.entry_height + self.padding) + + # Add scroll area to main layout + self.layout.addWidget(self.scroll) + + self.entries = [] + self.add_entry() + + def add_entry(self): + # Create horizontal layout for index and entry + entry_layout = QtWidgets.QHBoxLayout() + entry_layout.setContentsMargins(0, 0, 10, 0) + entry_layout.setSpacing(5) # Consistent spacing between index and entry + + # Create container widget for the entry row + entry_widget = QtWidgets.QWidget() + entry_widget.setFixedHeight(self.entry_height) + entry_widget.setLayout(entry_layout) + + # Add index label + index_label = QtWidgets.QLabel(str(len(self.entries) + 1) + ".") + index_label.setFixedWidth(20) # Set fixed width for alignment + entry_layout.addWidget(index_label) + + # Add text entry + entry = QtWidgets.QLineEdit() + entry.setFixedHeight(self.entry_height - 6) # Slightly smaller than container + entry.textChanged.connect(self._on_text_changed) + entry.textEdited.connect(lambda text: self._check_for_spaces(entry, text)) + self.entries.append((entry, index_label)) # Store both widgets + entry_layout.addWidget(entry) + + # Insert the new entry before the stretch + self.entries_layout.insertWidget(len(self.entries) - 1, entry_widget) + + def _check_for_spaces(self, entry, text): + if " " in text: + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Warning) + msg.setText(f"Spaces are not allowed in the {self.label_text} list. Use underscores instead.") + msg.setWindowTitle("Warning") + msg.exec_() + entry.setText(entry.text().replace(" ", "_")) + + def _on_text_changed(self): + # If the last entry has text, add a new empty entry + if self.entries[-1][0].text(): + self.add_entry() + + # Remove any empty entries except the last one + entries_to_remove = [] + for i, (entry, _) in enumerate(self.entries[:-1]): + if not entry.text(): + entries_to_remove.append(i) + + for i in reversed(entries_to_remove): + entry_widget = self.entries[i][0].parent() + self.entries_layout.removeWidget(entry_widget) + entry_widget.deleteLater() + self.entries.pop(i) + + self._update_indices() # Update the indices after removal + + def get_entries(self): + return [entry[0].text() for entry in self.entries if entry[0].text()] + + def _update_indices(self): + for i, (_entry, index_label) in enumerate(self.entries): + index_label.setText(str(i + 1) + ".") + + +class Switch(QtWidgets.QPushButton): + def __init__(self, on_text="Yes", off_text="No", width=80, parent=None): + super().__init__(parent) + self.on_text = on_text + self.off_text = off_text + self.setCheckable(True) + self.setFixedWidth(width) + self.setMinimumHeight(22) + + def paintEvent(self, event): + # Colors: https://qdarkstylesheet.readthedocs.io/en/latest/color_reference.html + label = self.on_text if self.isChecked() else self.off_text + bg_color = "#00ff00" if self.isChecked() else "#9DA9B5" + + radius = 10 + width = 32 + center = self.rect().center() + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.translate(center) + painter.setBrush(QColor(69, 83, 100)) # Lighter gray background + + pen = QPen("#455364") + pen.setWidth(2) + painter.setPen(pen) + + painter.drawRoundedRect(QtCore.QRect(-width, -radius, 2 * width, 2 * radius), radius, radius) + painter.setBrush(QBrush(bg_color)) + sw_rect = QtCore.QRect(-radius, -radius, width + radius, 2 * radius) + if not self.isChecked(): + sw_rect.moveLeft(-width) + + painter.drawRoundedRect(sw_rect, radius, radius) + + pen = QPen("#000000") + pen.setWidth(2) + painter.setPen(pen) + painter.drawText(sw_rect, QtCore.Qt.AlignCenter, label) class ProjectCreator(QtWidgets.QDialog): + """Project creation dialog.""" + def __init__(self, parent): - super(ProjectCreator, self).__init__(parent) + super().__init__(parent) self.parent = parent self.setWindowTitle("New Project") self.setModal(True) @@ -34,6 +191,19 @@ def __init__(self, parent): self.exp_default = "" self.loc_default = parent.project_folder + self.bodypart_list = None + self.individuals_list = None + self.unique_bodyparts_list = None + + self.toggle_3d = Switch() + self.toggle_3d.setChecked(False) + self.madlc_toggle = Switch() + self.madlc_toggle.setChecked(False) + self.unique_toggle = Switch() + self.unique_toggle.setChecked(False) + self.identity_toggle = Switch() + self.identity_toggle.setChecked(False) + main_layout = QtWidgets.QVBoxLayout(self) self.user_frame = self.lay_out_user_frame() self.video_frame = self.lay_out_video_frame() @@ -80,56 +250,195 @@ def lay_out_user_frame(self): grid.addWidget(self.loc_line, 2, 1) vbox.addLayout(grid) - self.madlc_box = QtWidgets.QCheckBox("Is it a multi-animal project?") - self.madlc_box.setChecked(False) - vbox.addWidget(self.madlc_box) + widget_3d = self.build_toggle_widget( + switch=self.toggle_3d, + question="Do you want to create a 3D pose estimation project?", + help_text="(What is needed for a 3D project?)", + docs_link=URL_3D, + ) + madlc_widget = self.build_toggle_widget( + switch=self.madlc_toggle, + question="Are there multiple individuals in your videos?", + help_text="(Why does this matter?)", + docs_link=URL_USE_GUIDE_SCENARIO, + ) + # Only visible when the maDLC widget is checked + unique_widget = self.build_toggle_widget( + switch=self.unique_toggle, + question="Do you have unique bodyparts in your video?", + help_text="(What are unique bodyparts?)", + docs_link=URL_MA_CONFIGURE, + ) + unique_widget.setVisible(False) + + # Labelling with identity + identity_widget = self.build_toggle_widget( + switch=self.identity_toggle, + question="Label with identity?", + help_text="(What is labeling with identity?)", + docs_link=URL_MA_CONFIGURE, + ) + identity_widget.setVisible(False) + + vbox.addWidget(widget_3d, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(madlc_widget, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(unique_widget, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(identity_widget, alignment=QtCore.Qt.AlignTop) + + # Create horizontal layout for the two lists + lists_layout = QtWidgets.QHBoxLayout() + lists_layout.setAlignment(QtCore.Qt.AlignTop) + + # Create both DynamicTextList widgets as class attributes + self.bodypart_list = DynamicTextList( + label_text="Bodyparts to track", + parent=self, + ) + + self.individuals_list = DynamicTextList( + label_text="Individual names", + parent=self, + ) + self.individuals_list.setVisible(False) + + self.unique_bodyparts_list = DynamicTextList( + label_text="Unique bodyparts to track", + parent=self, + ) + self.unique_bodyparts_list.setVisible(False) + + # Connect toggle state to individuals list visibility, unique, identity + self.madlc_toggle.toggled.connect(self.individuals_list.setVisible) + self.madlc_toggle.toggled.connect(unique_widget.setVisible) + self.madlc_toggle.toggled.connect(identity_widget.setVisible) + + # Connect the unique_toggle to the unique_bodyparts_list + self.unique_toggle.toggled.connect( + lambda yes: self.unique_bodyparts_list.setVisible(yes and self.madlc_toggle.isChecked()) + ) + + # Connect 3d toggle to all other option visibility + self.toggle_3d.toggled.connect(lambda yes: madlc_widget.setVisible(not yes)) + self.toggle_3d.toggled.connect( + lambda checked_3d: unique_widget.setVisible(not checked_3d and self.madlc_toggle.isChecked()) + ) + self.toggle_3d.toggled.connect( + lambda checked_3d: identity_widget.setVisible(not checked_3d and self.madlc_toggle.isChecked()) + ) + self.toggle_3d.toggled.connect(lambda checked_3d: self.bodypart_list.setVisible(not checked_3d)) + self.toggle_3d.toggled.connect( + lambda checked_3d: self.individuals_list.setVisible(not checked_3d and self.madlc_toggle.isChecked()) + ) + self.toggle_3d.toggled.connect( + lambda checked_3d: self.unique_bodyparts_list.setVisible( + not checked_3d and self.madlc_toggle.isChecked() and self.unique_toggle.isChecked() + ) + ) + + # Add both lists to the horizontal layout with top alignment + lists_layout.addWidget(self.bodypart_list, alignment=QtCore.Qt.AlignTop) + lists_layout.addWidget(self.individuals_list, alignment=QtCore.Qt.AlignTop) + lists_layout.addWidget(self.unique_bodyparts_list, alignment=QtCore.Qt.AlignTop) + + # Add the horizontal layout to the main vertical layout + vbox.addLayout(lists_layout) return user_frame + def build_toggle_widget( + self, + switch: Switch, + question: str, + help_text: str, + docs_link: str, + ) -> QtWidgets.QWidget: + toggle_layout = QtWidgets.QHBoxLayout() + toggle_layout.setContentsMargins(0, 0, 0, 0) + toggle_layout.setSpacing(10) + + toggle_label = QtWidgets.QLabel(question) + toggle_label.setAlignment(QtCore.Qt.AlignLeft) + help_label = ClickableLabel(help_text, parent=self) + help_label.setStyleSheet("text-decoration: underline; font-weight: bold;") + help_label.setCursor(QtCore.Qt.PointingHandCursor) + help_label.signal.connect(lambda: QDesktopServices.openUrl(QtCore.QUrl(docs_link))) + + toggle_layout.addWidget(switch, alignment=QtCore.Qt.AlignLeft) + toggle_layout.addWidget(toggle_label, alignment=QtCore.Qt.AlignLeft) + toggle_layout.addStretch() + toggle_layout.addWidget(help_label, alignment=QtCore.Qt.AlignRight) + toggle_widget = QtWidgets.QWidget() + toggle_widget.setLayout(toggle_layout) + return toggle_widget + def lay_out_video_frame(self): video_frame = ItemSelectionFrame([], self) - self.cam_combo = QtWidgets.QComboBox(video_frame) - self.cam_combo.addItems(map(str, (1, 2))) - self.cam_combo.currentTextChanged.connect(self.check_num_cameras) - ncam_label = QtWidgets.QLabel("Number of cameras:") - ncam_label.setBuddy(self.cam_combo) - self.copy_box = QtWidgets.QCheckBox("Copy videos to project folder") self.copy_box.setChecked(False) - browse_button = QtWidgets.QPushButton("Browse videos") + # Add checkbox for selecting individual files + self.select_files_box = QtWidgets.QCheckBox("Select individual files") + self.select_files_box.setChecked(False) + + browse_button = QtWidgets.QPushButton("Browse for videos") browse_button.clicked.connect(self.browse_videos) clear_button = QtWidgets.QPushButton("Clear") clear_button.clicked.connect(video_frame.fancy_list.clear) - layout1 = QtWidgets.QHBoxLayout() - layout1.addWidget(ncam_label) - layout1.addWidget(self.cam_combo) - layout2 = QtWidgets.QHBoxLayout() - layout2.addWidget(browse_button) - layout2.addWidget(clear_button) - video_frame.layout.insertLayout(0, layout1) - video_frame.layout.addLayout(layout2) + layout = QtWidgets.QHBoxLayout() + layout.addWidget(browse_button) + layout.addWidget(clear_button) + video_frame.layout.addLayout(layout) video_frame.layout.addWidget(self.copy_box) + video_frame.layout.addWidget(self.select_files_box) + self.toggle_3d.toggled.connect(lambda yes: self.copy_box.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: browse_button.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: clear_button.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: video_frame.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: self.select_files_box.setVisible(not yes)) return video_frame def browse_videos(self): - folder = QtWidgets.QFileDialog.getExistingDirectory( - self, - "Please select a folder", - self.loc_default, - ) - if not folder: - return + options = QtWidgets.QFileDialog.Options() + options |= QtWidgets.QFileDialog.DontUseNativeDialog + + if self.select_files_box.isChecked(): + # Select individual video files + video_types = [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [ + f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:] + ] + video_filter = f"Videos ({' '.join(video_types)})" + + files, _ = QtWidgets.QFileDialog.getOpenFileNames( + self, + "Select video files", + self.loc_default, + video_filter, + options=options, + ) + + if files: + for video in files: + self.video_frame.fancy_list.add_item(video) + else: + # Browse folders for videos + folder = QtWidgets.QFileDialog.getExistingDirectory( + self, + "Please select a folder", + self.loc_default, + options, + ) + if not folder: + return - for video in auxiliaryfunctions.grab_files_in_folder( - folder, - relative=False, - ): - if os.path.splitext(video)[1][1:] in DLCParams.VIDEOTYPES[1:]: - self.video_frame.fancy_list.add_item(video) + for video in auxiliaryfunctions.grab_files_in_folder( + folder, + relative=False, + ): + if os.path.splitext(video)[1][1:].lower() in DLCParams.VIDEOTYPES[1:]: + self.video_frame.fancy_list.add_item(video) def finalize_project(self): fields = [self.proj_line, self.exp_line] @@ -142,13 +451,13 @@ def finalize_project(self): if empty: return - n_cameras = int(self.cam_combo.currentText()) + create_3d = self.toggle_3d.isChecked() try: - if n_cameras > 1: - _ = deeplabcut.create_new_project_3d( + if create_3d: + _ = create_new_project_3d( self.proj_default, self.exp_default, - n_cameras, + 2, self.loc_default, ) else: @@ -158,12 +467,10 @@ def finalize_project(self): self.video_frame.fancy_list.setStyleSheet("border: 1px solid red") return else: - self.video_frame.fancy_list.setStyleSheet( - self.video_frame.fancy_list._default_style - ) + self.video_frame.fancy_list.setStyleSheet(self.video_frame.fancy_list._default_style) to_copy = self.copy_box.isChecked() - is_madlc = self.madlc_box.isChecked() - config = deeplabcut.create_new_project( + is_madlc = self.madlc_toggle.isChecked() + config = create_new_project( self.proj_default, self.exp_default, videos, @@ -171,39 +478,53 @@ def finalize_project(self): to_copy, multianimal=is_madlc, ) + + if self.bodypart_list is not None: + bodypart_key = "bodyparts" + updates = {} + if is_madlc: + bodypart_key = "multianimalbodyparts" + if self.individuals_list is not None: + individuals = self.individuals_list.get_entries() + if len(individuals) > 0: + updates["individuals"] = individuals + + if self.unique_toggle.isChecked() and self.unique_bodyparts_list is not None: + unique_bodyparts = self.unique_bodyparts_list.get_entries() + if len(unique_bodyparts) > 0: + updates["uniquebodyparts"] = unique_bodyparts + + if self.identity_toggle.isChecked(): + updates["identity"] = True + + bodyparts = self.bodypart_list.get_entries() + if len(bodyparts) > 0: + updates[bodypart_key] = bodyparts + + if len(updates) > 0: + cfg: dict = auxiliaryfunctions.read_config(config) + cfg.update(**updates) + auxiliaryfunctions.write_config(config, cfg) + self.parent.load_config(config) - self.parent._update_project_state( - config=config, - loaded=True, - ) + self.parent._update_project_state(config=config, loaded=True) except FileExistsError: - print('Project "{}" already exists!'.format(self.proj_default)) + print(f'Project "{self.proj_default}" already exists!') return - msg = QtWidgets.QMessageBox(text=f"New project created") + msg = QtWidgets.QMessageBox(text="New project created") msg.setIcon(QtWidgets.QMessageBox.Information) msg.exec_() self.close() def on_click(self): - dirname = QtWidgets.QFileDialog.getExistingDirectory( - self, "Please select a folder", self.loc_default - ) + dirname = QtWidgets.QFileDialog.getExistingDirectory(self, "Please select a folder", self.loc_default) if not dirname: return self.loc_default = dirname self.update_project_location() - def check_num_cameras(self, value): - val = int(value) - for child in self.video_frame.children(): - if child.isWidgetType() and not isinstance(child, QtWidgets.QComboBox): - if val > 1: - child.setDisabled(True) - else: - child.setDisabled(False) - def update_project_name(self, text): self.proj_default = text self.update_project_location() diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py index 7101b0e5b3..ff3a35e45c 100644 --- a/deeplabcut/gui/tabs/create_training_dataset.py +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -8,21 +8,42 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations + import os +import re +from importlib import import_module +from pathlib import Path +import dlclibrary from PySide6 import QtWidgets -from PySide6.QtCore import Qt -from PySide6.QtGui import QIcon +from PySide6.QtCore import Qt, Slot -from deeplabcut.gui.dlc_params import DLCParams +import deeplabcut +import deeplabcut.compat as compat +from deeplabcut.core.engine import Engine +from deeplabcut.core.weight_init import WeightInitialization +from deeplabcut.generate_training_dataset import get_existing_shuffle_indices +from deeplabcut.generate_training_dataset.metadata import get_shuffle_engine from deeplabcut.gui.components import ( + ConditionsSelectionWidget, DefaultTab, ShuffleSpinBox, + _create_confirmation_box, _create_grid_layout, _create_label_widget, + _create_message_box, + set_combo_items, +) +from deeplabcut.gui.displays.shuffle_metadata_viewer import ShuffleMetadataViewer +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.widgets import launch_napari +from deeplabcut.modelzoo import build_weight_init +from deeplabcut.pose_estimation_pytorch import ( + available_models, + is_model_cond_top_down, + is_model_top_down, ) - -import deeplabcut from deeplabcut.utils.auxiliaryfunctions import ( get_data_and_metadata_filenames, get_training_set_folder, @@ -31,7 +52,7 @@ class CreateTrainingDataset(DefaultTab): def __init__(self, root, parent, h1_description): - super(CreateTrainingDataset, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.model_comparison = False @@ -40,16 +61,32 @@ def __init__(self, root, parent, h1_description): self._generate_layout_attributes(self.layout_attributes) self.main_layout.addLayout(self.layout_attributes) + self.mapping_button = QtWidgets.QPushButton("Edit Conversion Table") + self.mapping_button.clicked.connect(self.edit_conversion_table) + self.mapping_button.setVisible(False) + self.root.engine_change.connect(self.set_edit_table_visibility) + self.ok_button = QtWidgets.QPushButton("Create Training Dataset") self.ok_button.setMinimumWidth(150) self.ok_button.clicked.connect(self.create_training_dataset) + self.main_layout.addWidget(self.mapping_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.ok_button, alignment=Qt.AlignRight) + self.view_shuffles_button = QtWidgets.QPushButton("View Existing Shuffles") + self.view_shuffles_button.clicked.connect(self.view_shuffles) + self.main_layout.addWidget(self.view_shuffles_button, alignment=Qt.AlignLeft) + self.help_button = QtWidgets.QPushButton("Help") self.help_button.clicked.connect(self.show_help_dialog) self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + def set_edit_table_visibility(self) -> None: + has_conversion_tables = bool(self.root.cfg.get("SuperAnimalConversionTables", {})) + is_pytorch_engine = self.root.engine == Engine.PYTORCH + is_finetuning = self.weight_init_selector.with_decoder + self.mapping_button.setVisible(has_conversion_tables & is_pytorch_engine & is_finetuning) + def show_help_dialog(self): dialog = QtWidgets.QDialog(self) layout = QtWidgets.QVBoxLayout() @@ -74,29 +111,83 @@ def _generate_layout_attributes(self, layout): shuffle_label = QtWidgets.QLabel("Shuffle") self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + # Dataset choices + self.weight_init_label = QtWidgets.QLabel("Weight Initialization") + self.weight_init_selector = WeightInitializationSelector(self.root) + self.update_weight_init_methods(self.root.engine) + self.root.engine_change.connect(self.update_weight_init_methods) + # Augmentation method augmentation_label = QtWidgets.QLabel("Augmentation method") self.aug_choice = QtWidgets.QComboBox() - self.aug_choice.addItems(DLCParams.IMAGE_AUGMENTERS) - self.aug_choice.setCurrentText("imgaug") + self.update_aug_methods(self.root.engine) + self.root.engine_change.connect(self.update_aug_methods) self.aug_choice.currentTextChanged.connect(self.log_augmentation_choice) # Neural Network nnet_label = QtWidgets.QLabel("Network architecture") self.net_choice = QtWidgets.QComboBox() - nets = DLCParams.NNETS.copy() - if not self.root.is_multianimal: - nets.remove("dlcrnet_ms5") - self.net_choice.addItems(nets) - self.net_choice.setCurrentText("resnet_50") + self.net_choice.setMinimumWidth(200) + self.update_nets(self.root.engine) + self.root.engine_change.connect(self.update_nets) self.net_choice.currentTextChanged.connect(self.log_net_choice) + # Update Net types when selected weight init changes + self.weight_init_selector.weight_init_choice.currentTextChanged.connect(lambda _: self.update_nets(None)) + self.weight_init_selector.weight_init_choice.currentTextChanged.connect( + lambda _: self.set_edit_table_visibility() + ) + + # Detector selection for top-down models + self.detector_label = QtWidgets.QLabel("Detector architecture") + self.detector_choice = QtWidgets.QComboBox() + self.detector_choice.setMinimumWidth(200) + self.update_detectors(engine=self.root.engine) + self.root.engine_change.connect(lambda engine: self.update_detectors(engine=engine)) + self.net_choice.currentTextChanged.connect( + lambda new_net_choice: self.update_detectors(net_choice=new_net_choice) + ) + + # Conditions selection for CTD models + self.conditions_label = QtWidgets.QLabel("Conditions") + self.conditions_selection_widget = ConditionsSelectionWidget(root=self.root, parent=self) + self.update_conditions(engine=self.root.engine) + self.root.engine_change.connect(lambda engine: self.update_conditions(engine=engine)) + self.net_choice.currentTextChanged.connect( + lambda new_net_choice: self.update_conditions(engine=self.root.engine, net_choice=new_net_choice) + ) + + # Overwrite selection + self.overwrite = QtWidgets.QCheckBox("Overwrite if exists") + self.overwrite.setChecked(False) + self.overwrite.setToolTip( + "When checked, creating a new shuffle with an index that already exists " + "will overwrite the existing index. Be careful with this option as you " + "might lose data." + ) + self.overwrite.stateChanged.connect(lambda s: self.root.logger.info(f"Overwrite: {s}")) + + # Use same data split as another shuffle + self.data_split_selection = DataSplitSelector(self.root, self) + layout.addWidget(shuffle_label, 0, 0) layout.addWidget(self.shuffle, 0, 1) - layout.addWidget(nnet_label, 0, 2) - layout.addWidget(self.net_choice, 0, 3) - layout.addWidget(augmentation_label, 0, 4) - layout.addWidget(self.aug_choice, 0, 5) + layout.addWidget(self.weight_init_label, 0, 2) + layout.addWidget(self.weight_init_selector, 0, 3) + + layout.addWidget(nnet_label, 1, 0) + layout.addWidget(self.net_choice, 1, 1) + layout.addWidget(augmentation_label, 1, 2) + layout.addWidget(self.aug_choice, 1, 3) + + layout.addWidget(self.detector_label, 2, 0) + layout.addWidget(self.detector_choice, 2, 1) + + layout.addWidget(self.conditions_label, 3, 0) + layout.addWidget(self.conditions_selection_widget, 3, 1) + + layout.addWidget(self.overwrite, 4, 0) + layout.addWidget(self.data_split_selection, 5, 0) def log_net_choice(self, net): self.root.logger.info(f"Network architecture set to {net.upper()}") @@ -104,34 +195,139 @@ def log_net_choice(self, net): def log_augmentation_choice(self, augmentation): self.root.logger.info(f"Image augmentation set to {augmentation.upper()}") + def edit_conversion_table(self): + # Test beforehand whether a conversion table exists + memory_replay_folder = Path(self.root.project_folder) / "memory_replay" + conversion_matrix_out_path = str(memory_replay_folder / "confusion_matrix.png") + files = [self.root.config] + if os.path.exists(conversion_matrix_out_path): + files.append(conversion_matrix_out_path) + _ = launch_napari(files) + def create_training_dataset(self): shuffle = self.shuffle.value() + cfg = self.root.cfg + existing_indices = get_existing_shuffle_indices( + cfg=cfg, train_fraction=cfg["TrainingFraction"][self.root.trainingset_index] + ) + + overwrite = self.overwrite.isChecked() + if shuffle in existing_indices: + if overwrite: + if not self._confirm_overwrite(shuffle, existing_indices): + return + else: + msg = _create_message_box( + "The training dataset could not be created.", + ( + f"Shuffle {shuffle} already exists - you can create a new " + "training dataset with an unused shuffle index (existing " + f"shuffles are {existing_indices}) or you can overwrite the " + f"shuffle by ticking the 'Overwrite' checkbox" + ), + ) + msg.exec_() + self.root.writer.write("Training dataset creation failed.") + return if self.model_comparison: raise NotImplementedError # TODO: finish model_comparison - deeplabcut.create_training_model_comparison( - config_file, - num_shuffles=shuffle, - net_types=self.net_type, - augmenter_types=self.aug_type, - ) + # deeplabcut.create_training_model_comparison( + # config_file, + # num_shuffles=shuffle, + # net_types=self.net_type, + # augmenter_types=self.aug_type, + # ) else: - if self.root.is_multianimal: - deeplabcut.create_multianimaltraining_dataset( - self.root.config, - shuffle, - Shuffles=[self.shuffle.value()], - net_type=self.net_choice.currentText(), + try: + engine = self.root.engine + net_type = self.net_choice.currentText() + detector_type = None + ctd_conditions = None + if engine == Engine.TF: + import_module("tensorflow") + + # try importing TF so they can't create shuffles for it if they + # don't have it installed + elif engine == Engine.PYTORCH: + if is_model_top_down(net_type): + detector_type = self.detector_choice.currentText() + elif is_model_cond_top_down(net_type): + ctd_conditions = self._build_ctd_conditions( + self.conditions_selection_widget.selected_conditions + ) + + try: + weight_init = self.weight_init_selector.get_super_animal_weight_init( + net_type, + detector_type, + ) + except ValueError as err: + print(f"The training dataset could not be created: {err}.") + return + + if self.data_split_selection.selected: + deeplabcut.create_training_dataset_from_existing_split( + self.root.config, + from_shuffle=self.data_split_selection.from_shuffle, + shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + + elif self.root.is_multianimal: + deeplabcut.create_multianimaltraining_dataset( + self.root.config, + shuffle, + Shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + else: + deeplabcut.create_training_dataset( + self.root.config, + shuffle, + Shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + augmenter_type=self.aug_choice.currentText(), + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + except ValueError as err: + msg = _create_message_box( + "The training dataset could not be created.", + str(err), ) - else: - deeplabcut.create_training_dataset( - self.root.config, - shuffle, - Shuffles=[self.shuffle.value()], - net_type=self.net_choice.currentText(), - augmenter_type=self.aug_choice.currentText(), + msg.exec_() + return + except ModuleNotFoundError as err: + info_text = ( + f"Error `{err}`. If the error is `ModuleNotFoundError: No module " + "named 'tensorflow'`, this is because you tried creating a " + "TensorFlow shuffle, but TensorFlow is not installed in your " + "environment. To create TensorFlow shuffles (and use TensorFlow " + "models), install it with\n" + " Windows/Linux:\n" + " pip install 'deeplabcut[tf]'\n" + " Apple Silicon:\n" + " pip install 'deeplabcut[apple_mchips]'" ) + msg = _create_message_box("The training dataset could not be created.", info_text) + msg.exec_() + return + # Check that training data files were indeed created. trainingsetfolder = get_training_set_folder(self.root.cfg) filenames = list( @@ -144,10 +340,8 @@ def create_training_dataset(self): ) if self.root.is_multianimal: filenames[0] = filenames[0].replace("mat", "pickle") - if all( - os.path.exists(os.path.join(self.root.project_folder, file)) - for file in filenames - ): + if all(os.path.exists(os.path.join(self.root.project_folder, file)) for file in filenames): + self.root.shuffle_created.emit(self.shuffle.value()) msg = _create_message_box( "The training dataset is successfully created.", "Use the function 'train_network' to start training. Happy training!", @@ -162,17 +356,449 @@ def create_training_dataset(self): msg.exec_() self.root.writer.write("Training dataset creation failed.") + def _confirm_overwrite(self, shuffle: int, existing_indices: list[int]) -> bool: + """Asks the user to confirm that they want to overwrite a shuffle. + + Args: + shuffle: the shuffle the user wants to overwrite + existing_indices: the indices of existing shuffles + + Returns: + whether the user confirmed overwriting the shuffle + """ + try: + engine = get_shuffle_engine(self.root.cfg, self.root.trainingset_index, shuffle) + engine_str = f" (with engine '{engine.aliases[0]}')" + except ValueError: + engine_str = "" + + conf = _create_confirmation_box( + title=f"Are you sure you want to overwrite shuffle {shuffle}?", + description=( + f"As shuffle {shuffle} already exists{engine_str}, the training-dataset files would be overwritten." + ), + ) + result = conf.exec() + if result != QtWidgets.QMessageBox.Yes: + msg = _create_message_box( + text="The training dataset was not be created.", + info_text=(f"You can create a shuffle with another index. Existing indices are {existing_indices}"), + ) + msg.exec_() + self.root.writer.write("Training dataset creation interrupted.") + return False + + return True + + def _build_ctd_conditions(self, conditions_path: str | Path) -> Path | tuple[int, str]: + """ + Builds CTD conditions in appropriate format from path to conditions. + Args: + conditions_path: str | Path: + path to conditions (path to snapshot or to predictions) + + Returns: + ctd_conditions: Path | tuple[int, str] + ctd conditions in the right format for deeplabcut.create_training_dataset() API method. + + Raises: + Value error if conditions are missing or invalid. + """ + if conditions_path is None: + raise ValueError("No conditions were selected for CTD model.") + else: + conditions_path = Path(conditions_path) + if conditions_path.suffix.lower() in [".h5", ".json"]: + return conditions_path + elif conditions_path.suffix.lower() == ".pt": + match = re.search(r"shuffle(\d+)", str(conditions_path)) + if match: + shuffle_number = int(match.group(1)) + else: + raise ValueError("Shuffle number could not be extracted from path.") + snapshot_filename = conditions_path.name + return shuffle_number, snapshot_filename + else: + raise ValueError("Unsupported conditions file type") + + @Slot(Engine) + def update_nets(self, engine: Engine | None) -> None: + if engine is None: + engine = self.root.engine + + default_net = None + if engine == Engine.TF: + nets = DLCParams.NNETS.copy() + if not self.root.is_multianimal: + nets.remove("dlcrnet_ms5") + else: + nets = available_models() + net_filter = self.get_net_filter() + default_net = self.get_default_net() + td_prefix = "top_down_" + if net_filter is not None: + nets = [ + n + for n in nets + if (n in net_filter or (n.startswith(td_prefix) and n[len(td_prefix) :] in net_filter)) + ] + + if default_net is None: + default_net = self.root.cfg.get("default_net_type", "resnet_50") + if ( + engine == Engine.TF + and default_net not in DLCParams.NNETS + or engine == Engine.PYTORCH + and default_net not in available_models() + ): + default_net = "resnet_50" + + set_combo_items( + combo_box=self.net_choice, + items=nets, + index=nets.index(default_net) if default_net in nets else 0, + ) + + @Slot(Engine) + def update_detectors( + self, + engine: Engine | None = None, + net_choice: str | None = None, + ) -> None: + if engine is None: + engine = self.root.engine + + if engine == Engine.TF: + detectors = [] + else: + # FIXME: Circular imports make it impossible to import this at the top + from deeplabcut.pose_estimation_pytorch import available_detectors + + detectors = available_detectors() + det_filter = self.get_detector_filter() + if det_filter is not None: + detectors = [d for d in detectors if d in det_filter] + + default_detector = self.get_default_detector() + try: + index = detectors.index(default_detector) + except ValueError: + try: + index = detectors.index("ssdlite") + except ValueError: + index = -1 + set_combo_items( + combo_box=self.detector_choice, + items=detectors, + index=index, + ) + + if net_choice is None: + net_choice = self.net_choice.currentText() + + if engine == Engine.PYTORCH and is_model_top_down(net_choice): + self.detector_label.show() + self.detector_choice.show() + else: + self.detector_label.hide() + self.detector_choice.hide() + + @Slot(Engine) + def update_conditions( + self, + engine: Engine | None = None, + net_choice: str | None = None, + ) -> None: + if engine is None: + engine = self.root.engine + + if net_choice is None: + net_choice = self.net_choice.currentText() + + if engine == Engine.PYTORCH and is_model_cond_top_down(net_choice): + self.conditions_label.show() + self.conditions_selection_widget.show() + else: + self.conditions_label.hide() + self.conditions_selection_widget.hide() + + @Slot(Engine) + def update_aug_methods(self, engine: Engine) -> None: + methods = compat.get_available_aug_methods(engine) + set_combo_items( + combo_box=self.aug_choice, + items=methods, + index=0, + ) + + @Slot(Engine) + def update_weight_init_methods(self, engine: Engine) -> None: + if engine != Engine.PYTORCH: + self.weight_init_label.hide() + self.weight_init_selector.hide() + return + + self.weight_init_label.show() + self.weight_init_selector.update_choices(list(_WEIGHT_INIT_OPTIONS.keys())) + self.weight_init_selector.show() + + def get_net_filter(self) -> list[str] | None: + """Returns: the net type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + if "super_animal" in weight_init_cfg: + return dlclibrary.get_available_models(weight_init_cfg["super_animal"]) + + return None + + def get_detector_filter(self) -> list[str] | None: + """Returns: the detectors that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + if "super_animal" in weight_init_cfg: + return dlclibrary.get_available_detectors(weight_init_cfg["super_animal"]) + + return None + + def get_default_net(self) -> str | None: + """Returns: the net type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + return weight_init_cfg.get("default_net") + + def get_default_detector(self) -> str | None: + """Returns: the detector type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + return weight_init_cfg.get("default_detector") + + def view_shuffles(self) -> None: + viewer = ShuffleMetadataViewer(root=self.root, parent=self) + viewer.show() + + +class WeightInitializationSelector(QtWidgets.QWidget): + """Widget to select weight initialization.""" + + def __init__(self, root): + super().__init__() + self.root = root + + self.weight_init_choice = QtWidgets.QComboBox() + + self.memory_replay_label = QtWidgets.QLabel("With memory replay") + self.memory_replay_box = QtWidgets.QCheckBox() + self.memory_replay_label.hide() + self.memory_replay_box.hide() + + memory_replay_layout = QtWidgets.QHBoxLayout() + memory_replay_layout.addWidget(self.memory_replay_label) + memory_replay_layout.addWidget(self.memory_replay_box) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.weight_init_choice) + layout.addLayout(memory_replay_layout) + self.setLayout(layout) + + self.weight_init_choice.currentTextChanged.connect(self._choice_changed) + + @property + def weight_init(self) -> str: + return self.weight_init_choice.currentText() + + @property + def with_decoder(self) -> bool: + weight_init_choice = self.weight_init_choice.currentText() + return "fine-tuning" in weight_init_choice.lower() + + @property + def memory_replay(self) -> bool: + return self.memory_replay_box.isChecked() + + def update_choices(self, choices: list[str]) -> None: + """Updates the WeightInitialization methods that can be selected.""" + set_combo_items( + combo_box=self.weight_init_choice, + items=choices, + ) + + def get_super_animal_weight_init( + self, + net_type: str, + detector_type: str, + ) -> WeightInitialization | None: + """ + Args: + net_type: The architecture of the pose model from which to fine-tune a + SuperAnimal model. + detector_type: The architecture of the detector from which to fine-tune a + SuperAnimal model. + + Raises: + ValueError if WeightInitialization should be defined but could not be + created (e.g. if there's no conversion table). + """ + if self.root.engine != Engine.PYTORCH: + return None + + weight_init_choice = self.weight_init_choice.currentText() + if "imagenet" in weight_init_choice.lower(): + return + + weight_init_data = _WEIGHT_INIT_OPTIONS[weight_init_choice] + super_animal = weight_init_data["super_animal"] + if net_type.startswith("top_down_"): + net_type = net_type[len("top_down_") :] + try: + weight_init = build_weight_init( + self.root.cfg, + super_animal=super_animal, + model_name=net_type, + detector_name=detector_type, + with_decoder=self.with_decoder, + memory_replay=self.memory_replay, + ) + except ValueError as err: + QtWidgets.QMessageBox.critical( + self, + "Error", + ( + f"No Conversion table specified for {super_animal} in the project " + "configuration file. Please create a conversion table using the GUI" + ", with ``deeplabcut.modelzoo.utils.create_conversion_table``, or " + "by adding it to your project's configuration file manually." + ), + ) + raise err + + return weight_init + + def _choice_changed(self, state: str) -> None: + if "fine-tuning" in str(state).lower(): + self.memory_replay_label.show() + self.memory_replay_box.show() + else: + self.memory_replay_label.hide() + self.memory_replay_box.hide() + + +class DataSplitSelector(QtWidgets.QWidget): + """Allows users to create training sets with the same train/test split as + another.""" + + def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): + super().__init__() + self.root = root + self.parent = parent + + self.setToolTip( + "This allows you to create a shuffle where the data split is the same as " + "one of your existing shuffles (the images on which the model is " + "trained/tested are the same)." + ) + + layout = QtWidgets.QVBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + box_layout = QtWidgets.QHBoxLayout() + box_layout.setSpacing(0) + box_layout.setContentsMargins(0, 0, 0, 0) + + selector_layout = QtWidgets.QHBoxLayout() + selector_layout.setSpacing(0) + selector_layout.setContentsMargins(0, 0, 0, 0) + + self.shuffle_label = QtWidgets.QLabel("From shuffle:") + self.shuffle_label.hide() + self.shuffle_selector = QtWidgets.QSpinBox() + self.shuffle_selector.setMaximum(10_000) + self.shuffle_selector.setValue(0) + self.shuffle_selector.hide() + + self.box = QtWidgets.QCheckBox(parent=self) + self.box.stateChanged.connect(self._checkbox_status_changed) + self.box_label = QtWidgets.QLabel("Use an existing data split") + + box_layout.addWidget(self.box) + box_layout.addWidget(self.box_label) + selector_layout.addWidget(self.shuffle_label) + selector_layout.addWidget(self.shuffle_selector) + layout.addLayout(box_layout) + layout.addLayout(selector_layout) + self.setLayout(layout) + + @property + def selected(self) -> bool: + return self.box.isChecked() + + @property + def from_shuffle(self) -> int: + """The shuffle from which to copy the data split.""" + return self.shuffle_selector.value() + + def _checkbox_status_changed(self, state: int) -> None: + if Qt.CheckState(state) == Qt.Checked: + self.shuffle_selector.show() + self.shuffle_label.show() + else: + self.shuffle_selector.hide() + self.shuffle_label.hide() -def _create_message_box(text, info_text): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Information) - msg.setText(text) - msg.setInformativeText(info_text) - msg.setWindowTitle("Info") - msg.setMinimumWidth(900) - logo_dir = os.path.dirname(os.path.realpath("logo.png")) + os.path.sep - logo = logo_dir + "/assets/logo.png" - msg.setWindowIcon(QIcon(logo)) - msg.setStandardButtons(QtWidgets.QMessageBox.Ok) - return msg +_WEIGHT_INIT_OPTIONS = { # FIXME - Generate dynamically + "Transfer Learning - ImageNet": { + "model_filter": None, + "detector_filter": None, + }, + "Transfer Learning - SuperAnimal Bird": { + "default_net": "top_down_resnet_50", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_bird", + }, + "Transfer Learning - SuperAnimal Quadruped": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_quadruped", + }, + "Transfer Learning - SuperAnimal TopViewMouse": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_topviewmouse", + }, + "Fine-tuning - SuperAnimal Bird": { + "default_net": "top_down_resnet_50", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_bird", + }, + "Fine-tuning - SuperAnimal Quadruped": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_quadruped", + }, + "Fine-tuning - SuperAnimal TopViewMouse": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_topviewmouse", + }, +} diff --git a/deeplabcut/gui/tabs/create_videos.py b/deeplabcut/gui/tabs/create_videos.py index e1f140762c..dcbec6a232 100644 --- a/deeplabcut/gui/tabs/create_videos.py +++ b/deeplabcut/gui/tabs/create_videos.py @@ -11,6 +11,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt +import deeplabcut from deeplabcut.gui.components import ( BodypartListWidget, DefaultTab, @@ -21,12 +22,10 @@ _create_vertical_layout, ) -import deeplabcut - class CreateVideos(DefaultTab): def __init__(self, root, parent, h1_description): - super(CreateVideos, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.bodyparts_to_use = self.root.all_bodyparts self._set_page() @@ -55,9 +54,7 @@ def _set_page(self): self.main_layout.addLayout(tmp_layout) - self.main_layout.addWidget( - _create_label_widget("Video Parameters", "font:bold") - ) + self.main_layout.addWidget(_create_label_widget("Video Parameters", "font:bold")) self.layout_video_parameters = _create_vertical_layout() self._generate_layout_video_parameters(self.layout_video_parameters) self.main_layout.addLayout(self.layout_video_parameters) @@ -134,18 +131,33 @@ def _generate_layout_video_parameters(self, layout): # Skeleton self.draw_skeleton_checkbox = QtWidgets.QCheckBox("Draw skeleton") - self.draw_skeleton_checkbox.setCheckState(Qt.Checked) + self.draw_skeleton_checkbox.setCheckState(Qt.Unchecked) self.draw_skeleton_checkbox.stateChanged.connect(self.update_draw_skeleton) tmp_layout.addWidget(self.draw_skeleton_checkbox) # Filtered data self.use_filtered_data_checkbox = QtWidgets.QCheckBox("Use filtered data") self.use_filtered_data_checkbox.setCheckState(Qt.Unchecked) - self.use_filtered_data_checkbox.stateChanged.connect( - self.update_use_filtered_data - ) + self.use_filtered_data_checkbox.stateChanged.connect(self.update_use_filtered_data) tmp_layout.addWidget(self.use_filtered_data_checkbox) + # Selector for p-cutoff + pcutoff_widget = QtWidgets.QWidget() + pcutoff_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) + pcutoff_label = QtWidgets.QLabel("Plotting confidence cutoff (pcutoff)") + self.pcutoff_selector = QtWidgets.QDoubleSpinBox() + self.pcutoff_selector.setMinimum(0.0) + self.pcutoff_selector.setMaximum(1.0) + self.pcutoff_selector.setValue(0.6) + self.pcutoff_selector.setSingleStep(0.05) + pcutoff_layout.addWidget(pcutoff_label) + pcutoff_layout.addWidget(self.pcutoff_selector) + pcutoff_widget.setLayout(pcutoff_layout) + pcutoff_widget.setToolTip( + "This value sets the confidence threshold, above which predictions are shown in the labeled videos." + ) + tmp_layout.addWidget(pcutoff_widget) + # Plot trajectories self.plot_trajectories = QtWidgets.QCheckBox("Plot trajectories") self.plot_trajectories.setCheckState(Qt.Unchecked) @@ -153,13 +165,9 @@ def _generate_layout_video_parameters(self, layout): tmp_layout.addWidget(self.plot_trajectories) # High quality video - self.create_high_quality_video = QtWidgets.QCheckBox( - "High quality video (slow)" - ) + self.create_high_quality_video = QtWidgets.QCheckBox("High quality video (slow)") self.create_high_quality_video.setCheckState(Qt.Unchecked) - self.create_high_quality_video.stateChanged.connect( - self.update_high_quality_video - ) + self.create_high_quality_video.stateChanged.connect(self.update_high_quality_video) tmp_layout.addWidget(self.create_high_quality_video) nested_tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) @@ -179,24 +187,20 @@ def _generate_layout_video_parameters(self, layout): layout.addLayout(tmp_layout, Qt.AlignLeft) def update_high_quality_video(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"High quality {s}.") def update_plot_trajectory_choice(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Plot trajectories {s}.") def update_selected_bodyparts(self): - selected_bodyparts = [ - item.text() for item in self.bodyparts_list_widget.selectedItems() - ] - self.root.logger.info( - f"Selected bodyparts for plotting:\n\t{selected_bodyparts}" - ) + selected_bodyparts = [item.text() for item in self.bodyparts_list_widget.selectedItems()] + self.root.logger.info(f"Selected bodyparts for plotting:\n\t{selected_bodyparts}") self.bodyparts_to_use = selected_bodyparts def update_use_all_bodyparts(self, s): - if s == Qt.Checked: + if Qt.CheckState(s) == Qt.Checked: self.bodyparts_list_widget.setEnabled(False) self.bodyparts_list_widget.hide() self.root.logger.info("Plot all bodyparts ENABLED.") @@ -207,15 +211,15 @@ def update_use_all_bodyparts(self, s): self.root.logger.info("Plot all bodyparts DISABLED.") def update_use_filtered_data(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Use filtered data {s}") def update_draw_skeleton(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Draw skeleton {s}") def update_overwrite_videos(self, state): - s = "ENABLED" if state == Qt.Checked else "DISABLED" + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" self.root.logger.info(f"Overwrite videos {s}") def update_color_by(self, text): @@ -243,13 +247,10 @@ def create_videos(self): # Single animal scenario. # Color is based on bodypart. color_by = "bodypart" - filtered = bool(self.use_filtered_data_checkbox.checkState()) + filtered = self.use_filtered_data_checkbox.isChecked() bodyparts = "all" - if ( - len(self.bodyparts_to_use) != 0 - and self.plot_all_bodyparts.checkState() != Qt.Checked - ): + if len(self.bodyparts_to_use) != 0 and not self.plot_all_bodyparts.isChecked(): self.update_selected_bodyparts() bodyparts = self.bodyparts_to_use @@ -258,28 +259,29 @@ def create_videos(self): videos=videos, shuffle=shuffle, filtered=filtered, - save_frames=bool(self.create_high_quality_video.checkState()), + save_frames=self.create_high_quality_video.isChecked(), + pcutoff=self.pcutoff_selector.value(), displayedbodyparts=bodyparts, - draw_skeleton=bool(self.draw_skeleton_checkbox.checkState()), + draw_skeleton=self.draw_skeleton_checkbox.isChecked(), trailpoints=trailpoints, color_by=color_by, + overwrite=self.overwrite_videos.isChecked(), ) if all(videos_created): self.root.writer.write("Labeled videos created.") else: - failed_videos = [ - video for success, video in zip(videos_created, videos) if not success - ] + failed_videos = [video for success, video in zip(videos_created, videos, strict=False) if not success] failed_videos_str = ", ".join(failed_videos) self.root.writer.write(f"Failed to create videos from {failed_videos_str}.") - if self.plot_trajectories.checkState(): + if self.plot_trajectories.isChecked(): deeplabcut.plot_trajectories( config=config, videos=videos, shuffle=shuffle, filtered=filtered, displayedbodyparts=bodyparts, + pcutoff=self.pcutoff_selector.value(), ) def build_skeleton(self, *args): diff --git a/deeplabcut/gui/tabs/docs.py b/deeplabcut/gui/tabs/docs.py new file mode 100644 index 0000000000..1f52118e19 --- /dev/null +++ b/deeplabcut/gui/tabs/docs.py @@ -0,0 +1,15 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +BASE_URL = "https://deeplabcut.github.io/DeepLabCut/docs/" +README = "https://deeplabcut.github.io/DeepLabCut/README.html" +URL_3D = BASE_URL + "Overviewof3D.html" +URL_MA_CONFIGURE = BASE_URL + "maDLC_UserGuide.html#configure-the-project" +URL_USE_GUIDE_SCENARIO = BASE_URL + "UseOverviewGuide.html#what-scenario-do-you-have" diff --git a/deeplabcut/gui/tabs/evaluate_network.py b/deeplabcut/gui/tabs/evaluate_network.py index b94590a33e..4eb31b63eb 100644 --- a/deeplabcut/gui/tabs/evaluate_network.py +++ b/deeplabcut/gui/tabs/evaluate_network.py @@ -8,17 +8,21 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations + import os +from pathlib import Path + import matplotlib.image as mpimg from matplotlib.backends.backend_qt5agg import ( FigureCanvasQTAgg as FigureCanvas, ) from matplotlib.figure import Figure from PySide6 import QtWidgets -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Slot import deeplabcut -from deeplabcut.utils.auxiliaryfunctions import get_evaluation_folder +from deeplabcut.core.engine import Engine from deeplabcut.gui.components import ( BodypartListWidget, DefaultTab, @@ -27,7 +31,9 @@ _create_label_widget, _create_vertical_layout, ) -from deeplabcut.gui.widgets import ConfigEditor +from deeplabcut.gui.displays.selected_shuffle_display import SelectedShuffleDisplay +from deeplabcut.gui.widgets import ConfigEditor, launch_napari +from deeplabcut.utils import auxiliaryfunctions class GridCanvas(QtWidgets.QDialog): @@ -41,7 +47,7 @@ def __init__(self, image_paths, parent=None): self.canvas = FigureCanvas(self.figure) layout.addWidget(self.canvas) - for image_path, gridspec in zip(image_paths[:9], self.grid): + for image_path, gridspec in zip(image_paths[:9], self.grid, strict=False): ax = self.figure.add_subplot(gridspec) ax.set_axis_off() img = mpimg.imread(image_path) @@ -50,7 +56,7 @@ def __init__(self, image_paths, parent=None): class EvaluateNetwork(DefaultTab): def __init__(self, root, parent, h1_description): - super(EvaluateNetwork, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.bodyparts_to_use = self.root.all_bodyparts @@ -80,9 +86,7 @@ def _set_page(self): self.edit_inferencecfg_btn.clicked.connect(self.open_inferencecfg_editor) if self.root.is_multianimal: - self.main_layout.addWidget( - self.edit_inferencecfg_btn, alignment=Qt.AlignRight - ) + self.main_layout.addWidget(self.edit_inferencecfg_btn, alignment=Qt.AlignRight) self.main_layout.addWidget(self.ev_nw_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.opt_button, alignment=Qt.AlignRight) @@ -91,6 +95,9 @@ def _set_page(self): self.help_button.clicked.connect(self.show_help_dialog) self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + self.root.engine_change.connect(self._on_engine_change) + self._on_engine_change(self.root.engine) + def show_help_dialog(self): dialog = QtWidgets.QDialog(self) layout = QtWidgets.QVBoxLayout() @@ -107,9 +114,11 @@ def show_help_dialog(self): def _generate_layout_attributes(self, layout): opt_text = QtWidgets.QLabel("Shuffle") self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + self.shuffle_display = SelectedShuffleDisplay(self.root, row_margin=0) layout.addWidget(opt_text) layout.addWidget(self.shuffle) + layout.addWidget(self.shuffle_display) def open_inferencecfg_editor(self): editor = ConfigEditor(self.root.inference_cfg_path) @@ -123,27 +132,17 @@ def plot_maps(self): # Display all images dest_folder = os.path.join( self.root.project_folder, - str( - get_evaluation_folder( - self.root.cfg["TrainingFraction"][0], shuffle, self.root.cfg - ) - ), + str(auxiliaryfunctions.get_evaluation_folder(self.root.cfg["TrainingFraction"][0], shuffle, self.root.cfg)), "maps", ) - image_paths = [ - os.path.join(dest_folder, file) - for file in os.listdir(dest_folder) - if file.endswith(".png") - ] + image_paths = [os.path.join(dest_folder, file) for file in os.listdir(dest_folder) if file.endswith(".png")] canvas = GridCanvas(image_paths, parent=self) canvas.show() def _generate_additional_attributes(self, layout): tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) - self.plot_predictions = QtWidgets.QCheckBox( - "Plot predictions (as in standard DLC projects)" - ) + self.plot_predictions = QtWidgets.QCheckBox("Plot predictions (as in standard DLC projects)") self.plot_predictions.stateChanged.connect(self.update_plot_predictions) tmp_layout.addWidget(self.plot_predictions) @@ -159,46 +158,68 @@ def _generate_additional_attributes(self, layout): layout.addWidget(self.bodyparts_list_widget, alignment=Qt.AlignLeft) def update_map_choice(self, state): - if state == Qt.Checked: + if Qt.CheckState(state) == Qt.Checked: self.root.logger.info("Plot scoremaps ENABLED") else: self.root.logger.info("Plot predictions DISABLED") def update_plot_predictions(self, s): - if s == Qt.Checked: + if Qt.CheckState(s) == Qt.Checked: self.root.logger.info("Plot predictions ENABLED") else: self.root.logger.info("Plot predictions DISABLED") def update_bodypart_choice(self, s): - if s == Qt.Checked: + if Qt.CheckState(s) == Qt.Checked: self.bodyparts_list_widget.setEnabled(False) self.bodyparts_list_widget.hide() self.root.logger.info("Use all bodyparts") else: self.bodyparts_list_widget.setEnabled(True) self.bodyparts_list_widget.show() - self.root.logger.info( - f"Use selected bodyparts only: {self.bodyparts_list_widget.selected_bodyparts}" - ) + self.root.logger.info(f"Use selected bodyparts only: {self.bodyparts_list_widget.selected_bodyparts}") def evaluate_network(self): config = self.root.config - - Shuffles = [self.root.shuffle_value] - plotting = self.plot_predictions.checkState() == Qt.Checked + shuffle = self.root.shuffle_value + plotting = self.plot_predictions.isChecked() bodyparts_to_use = "all" if ( - len(self.root.all_bodyparts) - != len(self.bodyparts_list_widget.selected_bodyparts) - ) and self.use_all_bodyparts.checkState() == False: + len(self.root.all_bodyparts) != len(self.bodyparts_list_widget.selected_bodyparts) + ) and not self.use_all_bodyparts.isChecked(): bodyparts_to_use = self.bodyparts_list_widget.selected_bodyparts deeplabcut.evaluate_network( config, - Shuffles=Shuffles, + Shuffles=[shuffle], plotting=plotting, show_errors=True, comparisonbodyparts=bodyparts_to_use, ) + + if plotting: + project_cfg = self.root.cfg + eval_folder = auxiliaryfunctions.get_evaluation_folder( + trainFraction=project_cfg["TrainingFraction"][0], + shuffle=shuffle, + cfg=project_cfg, + ) + scorer, _ = auxiliaryfunctions.get_scorer_name( + cfg=project_cfg, + shuffle=shuffle, + trainFraction=project_cfg["TrainingFraction"][0], + ) + + image_dir = Path(self.root.project_folder) / eval_folder / f"LabeledImages_{scorer}" + labeled_images = [str(p) for p in image_dir.rglob("*.png")] + if len(labeled_images) > 0: + _ = launch_napari(labeled_images) + + @Slot(Engine) + def _on_engine_change(self, engine: Engine) -> None: + if engine == Engine.PYTORCH: + self.opt_button.hide() + return + + self.opt_button.show() diff --git a/deeplabcut/gui/tabs/extract_frames.py b/deeplabcut/gui/tabs/extract_frames.py index 27c18c3083..9e8b92ba8f 100644 --- a/deeplabcut/gui/tabs/extract_frames.py +++ b/deeplabcut/gui/tabs/extract_frames.py @@ -9,28 +9,28 @@ # Licensed under GNU Lesser General Public License v3.0 # from functools import partial +from pathlib import Path from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.generate_training_dataset import extract_frames from deeplabcut.gui.components import ( DefaultTab, VideoSelectionWidget, _create_grid_layout, _create_label_widget, ) +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.utils import move_to_separate_thread from deeplabcut.gui.widgets import launch_napari -from deeplabcut.generate_training_dataset import extract_frames def select_cropping_area(config, videos=None): - """ - Interactively select the cropping area of all videos in the config. - A user interface pops up with a frame to select the cropping parameters. - Use the left click to draw a box and hit the button 'set cropping parameters' - to store the cropping parameters for a video in the config.yaml file. + """Interactively select the cropping area of all videos in the config. A user + interface pops up with a frame to select the cropping parameters. Use the left click + to draw a box and hit the button 'set cropping parameters' to store the cropping + parameters for a video in the config.yaml file. Parameters ---------- @@ -46,8 +46,8 @@ def select_cropping_area(config, videos=None): cfg : dict Updated project configuration """ - from deeplabcut.utils import auxiliaryfunctions from deeplabcut.gui.widgets import FrameCropper + from deeplabcut.utils import auxiliaryfunctions cfg = auxiliaryfunctions.read_config(config) if videos is None: @@ -81,8 +81,9 @@ def select_cropping_area(config, videos=None): class ExtractFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(ExtractFrames, self).__init__(root, parent, h1_description) - + super().__init__(root, parent, h1_description) + self.worker = None + self.thread = None self._set_page() def _set_page(self): @@ -93,7 +94,8 @@ def _set_page(self): self.main_layout.addWidget( _create_label_widget( - "Optional: frame extraction from a video subset", "font:bold" + "Frame extraction from a video subset (optional for automatic extraction)", + "font:bold", ) ) self.video_selection_widget = VideoSelectionWidget(self.root, self) @@ -127,25 +129,19 @@ def _generate_layout_attributes(self, layout): self.extraction_method_widget = QtWidgets.QComboBox() options = ["automatic", "manual"] self.extraction_method_widget.addItems(options) - self.extraction_method_widget.currentTextChanged.connect( - self.log_extraction_method - ) + self.extraction_method_widget.currentTextChanged.connect(self.log_extraction_method) # Frame extraction algorithm ext_algo_label = QtWidgets.QLabel("Extraction algorithm") self.extraction_algorithm_widget = QtWidgets.QComboBox() self.extraction_algorithm_widget.addItems(DLCParams.FRAME_EXTRACTION_ALGORITHMS) - self.extraction_algorithm_widget.currentTextChanged.connect( - self.log_extraction_algorithm - ) + self.extraction_algorithm_widget.currentTextChanged.connect(self.log_extraction_algorithm) # Frame cropping frame_crop_label = QtWidgets.QLabel("Frame cropping") self.frame_cropping_widget = QtWidgets.QComboBox() self.frame_cropping_widget.addItems(["disabled", "read from config", "GUI"]) - self.frame_cropping_widget.currentTextChanged.connect( - self.log_frame_cropping_choice - ) + self.frame_cropping_widget.currentTextChanged.connect(self.log_frame_cropping_choice) # Cluster step cluster_step_label = QtWidgets.QLabel("Cluster step") @@ -194,7 +190,19 @@ def extract_frames(self): config = self.root.config mode = self.extraction_method_widget.currentText() if mode == "manual": - _ = launch_napari(list(self.video_selection_widget.files)[0]) + videos = list(self.video_selection_widget.files) + if not videos: + QtWidgets.QMessageBox.critical( + self, + "Error", + "Please select exactly one video to extract frames from.", + ) + return + first_video = videos[0] + if len(videos) > 1: + self.root.writer.write(f"Only the first video ({first_video}) will be opened.") + video_path_in_folder = self._check_symlink(first_video) + _ = launch_napari(str(video_path_in_folder)) return algo = self.extraction_algorithm_widget.currentText() @@ -221,7 +229,8 @@ def extract_frames(self): userfeedback=False, videos_list=self.video_selection_widget.files or None, ) - self.worker, self.thread = move_to_separate_thread(func) + + self.worker, self.thread = move_to_separate_thread(func, capture_outputs=True) self.worker.finished.connect(lambda: self.ok_button.setEnabled(True)) self.worker.finished.connect(lambda: self.root._progress_bar.hide()) self.thread.finished.connect(self._show_success_message) @@ -230,10 +239,63 @@ def extract_frames(self): self.root._progress_bar.show() def _show_success_message(self): + message = "Failed to create worker: it is None" + root_message = "failed to extract frames: worker is None" + if self.worker is not None: + failed = self.worker.outputs + if failed is None: + # outputs are None during manual frame extraction + return + + if len(failed) == 0: + message = "Frame extraction failed. Please check your terminal output for more information." + elif all(failed): + message = "Frame extraction failed. Video files must be corrupted." + elif any(failed): + message = "Although most frames were extracted, some were invalid." + root_message = "failed to extract (some) frames" + else: + message = "Frames were successfully extracted, for the videos of interest." + root_message = "successfully extracted frames" + msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Information) - msg.setText("Frames were successfully extracted, for the videos of interest.") + msg.setText(message) msg.setWindowTitle("Info") msg.setStandardButtons(QtWidgets.QMessageBox.Ok) msg.exec_() - self.root.writer.write("Frames successfully extracted.") + self.root.writer.write(root_message) + + def _check_symlink(self, video_path: str | Path) -> Path: + """Checks that a video is in the DeepLabCut 'videos' folder. + + This is required before launching manual frame extraction. When users select + a symlink of a video using the VideoSelectionWidget, the path is resolved to the + true path of the video (which leads napari-deeplabcut to save the frames in the + incorrect folder). + + Args: + video_path: the path to a video in a DeepLabCut project or a video that was + added to the project + + Returns: + the path to the video (or symlink) in the project's 'videos' folder + + Raises: + FileNotFoundError if there is no symlink or video in the 'videos' folder for + the given video + """ + video_path = Path(video_path).resolve() + project_videos = (Path(self.root.config).parent / "videos").resolve() + if video_path.parent == project_videos: + return video_path + + symlink_path = project_videos / video_path.name + if not symlink_path.exists(): + raise FileNotFoundError( + f"Could not find the video {video_path.name} in your project videos. " + f"Did you add the video (you can do so in the 'Manage Project' tab)? " + f"There should be a file in {symlink_path}." + ) + + return symlink_path diff --git a/deeplabcut/gui/tabs/extract_outlier_frames.py b/deeplabcut/gui/tabs/extract_outlier_frames.py index ac62911ae5..cdb7ec3830 100644 --- a/deeplabcut/gui/tabs/extract_outlier_frames.py +++ b/deeplabcut/gui/tabs/extract_outlier_frames.py @@ -11,7 +11,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.dlc_params import DLCParams +import deeplabcut from deeplabcut.gui.components import ( DefaultTab, ShuffleSpinBox, @@ -19,14 +19,13 @@ _create_horizontal_layout, _create_label_widget, ) +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import launch_napari -import deeplabcut - class ExtractOutlierFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(ExtractOutlierFrames, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.filelist = [] self._set_page() @@ -47,9 +46,7 @@ def _set_page(self): self._generate_multianimal_options(self.layout_attributes) self.main_layout.addLayout(self.layout_attributes) - self.main_layout.addWidget( - _create_label_widget("Frame extraction options", "font:bold") - ) + self.main_layout.addWidget(_create_label_widget("Frame extraction options", "font:bold")) self.layout_extraction_options = _create_horizontal_layout() self._generate_layout_extraction_options(self.layout_extraction_options) self.main_layout.addLayout(self.layout_extraction_options) @@ -67,9 +64,7 @@ def _set_page(self): self.merge_data_button.clicked.connect(self.merge_dataset) self.merge_data_button.setMinimumWidth(150) - self.main_layout.addWidget( - self.extract_outlierframes_button, alignment=Qt.AlignRight - ) + self.main_layout.addWidget(self.extract_outlierframes_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.label_outliers_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.merge_data_button, alignment=Qt.AlignRight) @@ -115,9 +110,7 @@ def _generate_layout_extraction_options(self, layout): self.outlier_algorithm_widget = QtWidgets.QComboBox() self.outlier_algorithm_widget.addItems(DLCParams.OUTLIER_EXTRACTION_ALGORITHMS) self.outlier_algorithm_widget.setMinimumWidth(200) - self.outlier_algorithm_widget.currentTextChanged.connect( - self.update_outlier_algorithm - ) + self.outlier_algorithm_widget.currentTextChanged.connect(self.update_outlier_algorithm) layout.addWidget(opt_text) layout.addWidget(self.outlier_algorithm_widget) @@ -126,9 +119,7 @@ def update_tracker_type(self, method): self.root.logger.info(f"Using {method.upper()} tracker") def update_outlier_algorithm(self, algorithm): - self.root.logger.info( - f"Using {algorithm.upper()} algorithm for frame extraction" - ) + self.root.logger.info(f"Using {algorithm.upper()} algorithm for frame extraction") def extract_outlier_frames(self): config = self.root.config @@ -145,7 +136,7 @@ def extract_outlier_frames(self): config: {config}, shuffle: {shuffle}, videos: {videos}, - videotype: {videotype}, + video_extensions: {videotype}, outlier algorithm: {outlieralgorithm}, track method: {track_method} """ @@ -153,7 +144,7 @@ def extract_outlier_frames(self): deeplabcut.extract_outlier_frames( config=config, videos=videos, - videotype=videotype, + video_extensions=videotype, shuffle=shuffle, outlieralgorithm=outlieralgorithm, track_method=track_method, @@ -168,7 +159,8 @@ def merge_dataset(self): msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Warning) msg.setText( - "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" + "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you" + "need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" ) msg.setWindowTitle("Warning") msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) diff --git a/deeplabcut/gui/tabs/label_frames.py b/deeplabcut/gui/tabs/label_frames.py index 507f61a00b..509b04fcd3 100644 --- a/deeplabcut/gui/tabs/label_frames.py +++ b/deeplabcut/gui/tabs/label_frames.py @@ -8,16 +8,91 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations + import os +from pathlib import Path + from PySide6 import QtWidgets from PySide6.QtCore import Qt + from deeplabcut.generate_training_dataset import check_labels from deeplabcut.gui.components import DefaultTab from deeplabcut.gui.widgets import launch_napari +from deeplabcut.utils.skeleton import SkeletonBuilder + + +def label_frames(config_path: str | Path | None = None, image_folder: str | None = None): + """Launches the napari-deeplabcut labelling GUI. + + For more information on labelling data with napari-deeplabcut, see our docs: + https://github.com/DeepLabCut/napari-deeplabcut?tab=readme-ov-file#usage + + If no parameters are given, the napari-deeplabcut labelling GUI is simply open, + and the folder containing the images to label can be dropped into the GUI. + If the `config_path` and the `image_folder` are given as arguments, the given + `image_folder` for the project is opened in the napari-deeplabcut GUI to be labeled. + If only the `config_path` is given, the first image folder is opened. -def label_frames(config_path): - _ = launch_napari(config_path) + Parameters + ---------- + config_path: str, Path, None + Full path of the project config.yaml file. + + image_folder: str, None + Name of the image folder to open for labelling. + + Examples + -------- + Opening the napari-deeplabcut annotation GUI without opening a specific folder of + images to label. You then need to drag-and-drop your image folder into the GUI. + See the napari-deeplabcut docs linked above for more information about labelling in + napari-deeplabcut. + >>> import deeplabcut + >>> deeplabcut.label_frames() + + Opening the images extracted from the "2025-01-01-experiment7" video in + napari-deeplabcut on Windows. The project's folder structure should look as follows: + reaching-task/ # project root directory + ├── config.yaml # project configuration file + └── labeled-data/ # folder containing all extracted image folders + ├── ... + ├── 2025-01-01-experiment7 # folder containing the images to label + └── ... + + >>> deeplabcut.label_frames( + >>> "C:\\myproject\\reaching-task\\config.yaml", + >>> "2025-01-01-experiment7", + >>> ) + + Opening the images extracted from the first video listed in the project + configuration in napari-deeplabcut on a Unix system. + >>> deeplabcut.label_frames("/users/john/project/config.yaml") + """ + files = None + if config_path is None: + if image_folder is not None: + raise ValueError( + f"If the ``config_path`` is None, the ``image_folder`` must be None " + f"too. Found {image_folder}. To label the images in {image_folder}, " + f"give the project configuration file as `config_path`." + ) + else: + data_dir = Path(config_path).parent / "labeled-data" + if image_folder is None: + image_dirs = [path for path in data_dir.iterdir() if path.is_dir()] + if len(image_dirs) == 0: + raise ValueError( + f"Could not find any image folders in {data_dir}. Please check " + f"the config path given to `deeplabcut.label_frames(...)`" + ) + image_dir = list(sorted(image_dirs))[0] + else: + image_dir = data_dir / image_folder + + files = [str(image_dir), str(config_path)] + _ = launch_napari(files=files) refine_labels = label_frames @@ -25,7 +100,7 @@ def label_frames(config_path): class LabelFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(LabelFrames, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() @@ -34,8 +109,11 @@ def _set_page(self): self.label_frames_btn.clicked.connect(self.label_frames) self.check_labels_btn = QtWidgets.QPushButton("Check Labels") self.check_labels_btn.clicked.connect(self.check_labels) + self.build_skeleton_btn = QtWidgets.QPushButton("Build skeleton") + self.build_skeleton_btn.clicked.connect(self.build_skeleton) self.main_layout.addWidget(self.label_frames_btn, alignment=Qt.AlignLeft) self.main_layout.addWidget(self.check_labels_btn, alignment=Qt.AlignLeft) + self.main_layout.addWidget(self.build_skeleton_btn, alignment=Qt.AlignLeft) def log_color_by_option(self, choice): self.root.logger.info(f"Labeled images will by colored by {choice.upper()}") @@ -44,9 +122,7 @@ def label_frames(self): dialog = QtWidgets.QFileDialog(self) dialog.setFileMode(QtWidgets.QFileDialog.Directory) dialog.setViewMode(QtWidgets.QFileDialog.Detail) - dialog.setDirectory( - os.path.join(os.path.dirname(self.root.config), "labeled-data") - ) + dialog.setDirectory(os.path.join(os.path.dirname(self.root.config), "labeled-data")) if dialog.exec_(): folder = dialog.selectedFiles()[0] has_h5 = False @@ -60,3 +136,8 @@ def label_frames(self): def check_labels(self): check_labels(self.root.config, visualizeindividuals=self.root.is_multianimal) + labeled_images = (Path(self.root.config).parent / "labeled-data").rglob("*_labeled/*.png") + _ = launch_napari(labeled_images, plugin="napari", stack=True) + + def build_skeleton(self, *args): + SkeletonBuilder(self.root.config) diff --git a/deeplabcut/gui/tabs/manage_project.py b/deeplabcut/gui/tabs/manage_project.py index 932adcf827..b4d05e7d2a 100644 --- a/deeplabcut/gui/tabs/manage_project.py +++ b/deeplabcut/gui/tabs/manage_project.py @@ -9,16 +9,18 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QPushButton, QFileDialog, QLabel, QLineEdit, + QPushButton, ) + from deeplabcut.create_project import add_new_videos -from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.components import DefaultTab, _create_horizontal_layout +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import ConfigEditor diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py index 2607fab362..b40252354d 100644 --- a/deeplabcut/gui/tabs/modelzoo.py +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -9,21 +9,29 @@ # Licensed under GNU Lesser General Public License v3.0 # import os +import webbrowser from functools import partial +from pathlib import Path -import deeplabcut +import dlclibrary from PySide6 import QtWidgets -from PySide6.QtCore import Qt, Signal, QTimer, QRegularExpression -from PySide6.QtGui import QPixmap, QRegularExpressionValidator +from PySide6.QtCore import QRegularExpression, QSize, Qt, QTimer, Signal, Slot +from PySide6.QtGui import QIcon, QPixmap, QRegularExpressionValidator + +import deeplabcut +from deeplabcut.core.engine import Engine +from deeplabcut.gui import BASE_DIR from deeplabcut.gui.components import ( DefaultTab, VideoSelectionWidget, - _create_label_widget, _create_grid_layout, + _create_label_widget, + set_combo_items, + set_layout_contents_visible, ) -from deeplabcut.gui import BASE_DIR from deeplabcut.gui.utils import move_to_separate_thread -from deeplabcut.modelzoo.utils import parse_available_supermodels +from deeplabcut.gui.widgets import ClickableLabel +from deeplabcut.pose_estimation_pytorch.apis.utils import TORCHVISION_DETECTORS class RegExpValidator(QRegularExpressionValidator): @@ -40,47 +48,246 @@ def __init__(self, root, parent, h1_description): super().__init__(root, parent, h1_description) self._val_pattern = QRegularExpression(r"(\d{3,5},\s*)+\d{3,5}") self._set_page() + self.root.engine_change.connect(self._on_engine_change) + self.root.engine_change.connect(self._update_available_models) + self._update_pose_models(self.model_combo.currentText()) + self._update_detectors(self.model_combo.currentText()) + self._destfolder = None + self.worker = None + self.thread = None @property def files(self): return self.video_selection_widget.files def _set_page(self): + # Create Run button first so it exists for any method that references it + self.run_button = QtWidgets.QPushButton("Run") + self.run_button.setStyleSheet( + """ + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + } + QPushButton:disabled { + background-color: #9E9E9E; + color: white; + font-weight: bold; + } + """ + ) + self.run_button.setFixedWidth(120) + self.run_button.clicked.connect(self.run_video_inference_superanimal) + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.run_button) + button_layout.addStretch() + self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) - self.video_selection_widget = VideoSelectionWidget(self.root, self) + self.video_selection_widget = VideoSelectionWidget(self.root, self, hide_videotype=True) self.main_layout.addWidget(self.video_selection_widget) - model_settings_layout = _create_grid_layout(margins=(20, 0, 0, 0)) + self._build_common_attributes() + self._build_tf_attributes() + self._build_torch_attributes() + + self.home_button = QtWidgets.QPushButton("Return to Welcome page") + self.home_button.clicked.connect(self.root._generate_welcome_page) + self.main_layout.addWidget(self.home_button, alignment=Qt.AlignLeft) + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) - section_title = _create_label_widget( - "Supermodel Settings", "font:bold", (0, 50, 0, 0) + self.go_to_button = QtWidgets.QPushButton("Read Documentation") + # go to url + # https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models + # when button is clicked + self.go_to_button.clicked.connect( + lambda: webbrowser.open( + "https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models" + ) ) + self.main_layout.addWidget(self.go_to_button, alignment=Qt.AlignLeft) + + # Add the Run button layout + self.main_layout.addLayout(button_layout) - model_combo_text = QtWidgets.QLabel("Supermodel name") + self._on_engine_change(self.root.engine) + + def _add_supermodel_section(self, layout: QtWidgets.QGridLayout) -> None: + # --- Supermodel selection --- + section_title = QtWidgets.QLabel("Supermodel settings") + section_title.setStyleSheet("font-weight: bold; font-size: 16px;") + model_combo_text = QtWidgets.QLabel("Supermodel") + model_combo_text.setMinimumWidth(150) self.model_combo = QtWidgets.QComboBox() - supermodels = parse_available_supermodels() - self.model_combo.addItems(supermodels.keys()) + self.model_combo.setMinimumWidth(250) + layout.addWidget(section_title, 0, 0, 1, 6) + layout.addWidget(model_combo_text, 1, 0) + layout.addWidget(self.model_combo, 1, 1) + + def _add_pose_model_settings_row(self, layout: QtWidgets.QGridLayout): + # --- Pose Model Type and Pose Confidence Threshold on the same line (now row 2) --- + pose_model_row = QtWidgets.QHBoxLayout() + pose_model_label = QtWidgets.QLabel("Pose Model Type") + pose_model_label.setMinimumWidth(150) + self.net_type_selector = QtWidgets.QComboBox() + self.net_type_selector.setMinimumWidth(180) + pose_conf_label = QtWidgets.QLabel("Pose confidence threshold") + pose_conf_label.setMinimumWidth(170) + self.pose_threshold_spinbox = QtWidgets.QDoubleSpinBox( + decimals=2, + minimum=0.0, + maximum=1.0, + singleStep=0.01, + value=0.4, + wrapping=True, + ) + self.pose_threshold_spinbox.setMaximumWidth(100) + batch_size_combo_label = QtWidgets.QLabel("Pose model batch size") + self.batch_size_combo = QtWidgets.QComboBox() + self.batch_size_combo.setMinimumWidth(100) + self.batch_size_combo.addItems([str(2**i) for i in range(6)]) + self.batch_size_combo.setCurrentIndex(0) + pose_model_row.addWidget(pose_model_label) + pose_model_row.addWidget(self.net_type_selector) + pose_model_row.addSpacing(20) + pose_model_row.addWidget(pose_conf_label) + pose_model_row.addWidget(self.pose_threshold_spinbox) + pose_model_row.addSpacing(20) + pose_model_row.addWidget(batch_size_combo_label) + pose_model_row.addWidget(self.batch_size_combo) + pose_model_row.addStretch() + layout.addLayout(pose_model_row, 2, 0, 1, 6) + def _add_detector_settings_row(self, layout: QtWidgets.QGridLayout): + # --- Detector Type and Detector Confidence Threshold on the same line (now row 3) --- + detector_label = QtWidgets.QLabel("Detector Type") + detector_label.setMinimumWidth(150) + self.detector_type_selector = QtWidgets.QComboBox() + self.detector_type_selector.setMinimumWidth(180) + detector_conf_label = QtWidgets.QLabel("Detector confidence threshold") + detector_conf_label.setMinimumWidth(170) + self.detector_threshold_spinbox = QtWidgets.QDoubleSpinBox( + decimals=2, + minimum=0.0, + maximum=1.0, + singleStep=0.01, + value=0.1, + wrapping=True, + ) + self.detector_threshold_spinbox.setMaximumWidth(100) + max_individuals_label = QtWidgets.QLabel("Maximum number of individuals") + max_individuals_label.setMinimumWidth(180) + self.max_individuals_spinbox = QtWidgets.QSpinBox() + self.max_individuals_spinbox.setRange(1, 100) + self.max_individuals_spinbox.setValue(1) + self.max_individuals_spinbox.setMaximumWidth(100) + detector_batch_size_combo_label = QtWidgets.QLabel("Detector batch size") + self.detector_batch_size_combo = QtWidgets.QComboBox() + self.detector_batch_size_combo.setMinimumWidth(100) + self.detector_batch_size_combo.addItems([str(2**i) for i in range(6)]) + self.detector_batch_size_combo.setCurrentIndex(0) + self.detector_row = QtWidgets.QHBoxLayout() + self.detector_row.addWidget(detector_label) + self.detector_row.addWidget(self.detector_type_selector) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(detector_conf_label) + self.detector_row.addWidget(self.detector_threshold_spinbox) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(max_individuals_label) + self.detector_row.addWidget(self.max_individuals_spinbox) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(detector_batch_size_combo_label) + self.detector_row.addWidget(self.detector_batch_size_combo) + self.detector_row.addStretch() + layout.addLayout(self.detector_row, 3, 0, 1, 6) + + def _add_output_settings_section(self, layout: QtWidgets.QGridLayout): + loc_label = ClickableLabel("Folder to store results:", parent=self) + loc_label.signal.connect(self.select_folder) + self.loc_line = QtWidgets.QLineEdit( + "\n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor_swift.jpg to taylor_swift.jpg\n", + "User uploaded file 'taylor_swift.jpg' with length 46915 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "# JPG or PNG is recommended:\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "image_paths = [Path(filepath).resolve() for filepath in uploaded.keys()]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your image via the Files menu to the left and define\n", + "# `image_paths` yourself with right `click` > `copy path` on the image:\n", + "#\n", + "# image_paths = [\n", + "# Path(\"/path/to/my/image_000.png\"),\n", + "# Path(\"/path/to/my/image_001.png\"),\n", + "# ]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "nj-HtOBSwtdk", + "outputId": "eb5f3b18-cc89-4dd1-a58e-6c39c62582af" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1/1 [00:00<00:00, 1.95it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 78.27it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] + } + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of detections to keep in an image\n", + "max_detections = 10\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights,\n", + " box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted\n", + "# for each image; it will be given to the RTMPose model alongside\n", + "# the images.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for image_path in tqdm(image_paths):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + " batch = [preprocess(image).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels, strict=False) if label == 1]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the best N detections\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(zip(image_paths, context, strict=False)))\n", + "\n", + "\n", + "#############################################\n", + "# Create a DataFrame with the predictions, and save them to a CSV file.\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={\n", + " img_path: img_predictions for img_path, img_predictions in zip(image_paths, predictions, strict=False)\n", + " },\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)],\n", + " ),\n", + ")\n", + "\n", + "# Save to CSV\n", + "df.to_csv(\"image_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pWtdL4U52OBJ" + }, + "source": [ + "Finally, we can plot the predictions!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 447 + }, + "id": "3slKu6Lr2MUh", + "outputId": "ef7d938c-39fc-473a-9b88-6169cbfbc567" + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAGuCAYAAAAAg7f4AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eZQd2Z3Yd37vjfWt+V7uG/atUBuryCo2yWKRLLLJbvaqlnqVtc7YY43H5+jYGnmsM6fH9owkz8yZ8TrHkqUz8qgtS7aO1G6x3YvkZpMi2exmkbVvqEJhTSQSub58a+z3zh/xMpFAZaIyE4lEArgfnCiggHzx4kXEi/jFvff3u0JrrTEMwzAMwzAeGfJ+b4BhGIZhGIaxv0wAaBiGYRiG8YgxAaBhGIZhGMYjxgSAhmEYhmEYjxgTABqGYRiGYTxiTABoGIZhGIbxiDEBoGEYhmEYxiPGBICGYRiGYRiPGHvbPykEGtBHazhXW/duiw4CbZPHxuJ+b4mxbzSQgDB10Q3DMIwHV5Zl2/q5bbcArt0WX/vzP72b7TEMwzAMwzAOiO23AA4WePff+mk+/bf+yT3cHMMwDMMwDONeE9udC9iyrHu9LQeH6QJ+BJkuYMMwDOPBt+ddwIZhGIZhGMbDwQSAtzMNQIZhGIZhPOS2PwbwINN72V0r9nBdhmEYhmEYB8/DEQAigL0eo2iCQMMwDMMwHk4PSQC4xgRthmEYhmEYH8eMATQMwzAMw3jEmADQMAzDMAzjEWMCQMMwDMMwjEfM3Y8BvO9lU0zWrrGH9vp8NqemYRiGcQDtQRKIxf1vSDR3WWMv7GVOlAbSPVyfYRiGYeydPbjjCUwrnPHg2+tzWO3hugzDMAxjb93vpjvDMAzDMAxjn5kA0DAMwzAM4xFjAkDDMAzDMIxHzN0FgPc9A9gwDMMwDMPYqe0ngeitYkWT/GEYhmEYhvEg2UEW8J1+1ASBhmEYhmEYD4odBIAmyDMMwzAMw3gYmCQQwzAMwzCMR4wJAA3DMAzDMB4xJgA0DMMwDMN4xOzl5KeGYdxuq1JJZkitYRiGcR+ZAHCHhLh559baFEI0tiLY+uulMHMFG4ZhGPeTCQB3QQhhgj/jYwjA2uTvNaaCumEYhnG/mTGAhmEYhmEYjxgTAN6Fjd3BhmEYhmEYDwrTBbxLa8Hf2u9aa9MtbBiGYRjGA8G0ABqGYRiGYTxiTAvgjsnNx/DfaWy/MC2DxkYC9FbDB7QpEWMYhmHcc490ALjzMXwStLXlDXrL1YmM/Sr7sd1uaFPO5n6SbN74roEUkyVsGIZh3GuPdAAIOwwCNf3g76Ov2Xo1+uN+YM/sJJDbOHbR2E+mec8wDMO4/8wYQMMwDMMwjEfMI98C+LDZr65d04VsGIZhGA8uEwA+RPY7KDPdyIZhGIbxYHo4AsDdxB9rY/n2K3bZr/cxQ8wMwzAMw/gYD34AqCGPenYY+Wixi8SM3UZXkn2LALVa38yt5iy+PfFlp3Mb3+3rjTuxYNN9qQFlAnzDMAxjTzz4ASCQ3xV3ms/Sr8W2LzfUXQSou3IzcLh9ppKtbPfnPm4dZkaUvbLVeazYr1JChmEYxsPvIQkA1+wmiLnXgdl+NdmYoOvBZ5r3DMMwjP1hysAYhmEYhmE8Yh6yFkDjILtTN7PpNjYMwzCM/WMCQGNfbRYEmuDPMAzDMPbX/geA9+Reb8ZO3eJ+x1PrU+YZe86UEzIMwzD2wH1qAZRsHH54NxmosFY1435HPQeB4L436moFpDt+2VbngGkdXCMBZ5/eK8V8nwzDMB5u9zlaEOu/300QqLXCNFkclM+/8+34uGNvgkC4H6WEDMMwjIeXyQI2DMMwDMN4xOy6BfBuB/MLcX9iz7vtbjb23scVjzZzDhuGYRjG3tpVALhx5ofbbe8mLfqzsO1vMHan7TYOro3HzASBhmEYhnH3TBewYRiGYRjGI2b7LYB6Q6woxOZjxbXYYiL7W+11K5wQwrQMHShy8/NDsOMSMabF9n6Q2/oe30qDeHi+g+ufZKvTTx+ctCvDMIzd2HYAKMSGH73DTfx+3a9NoHBQiFvPlVuo/rLJq7Z5/Ex38H6wdvGah690jJZsfp17uGJdwzAeUTsIAD/+Bm2CP2PrY/HxSR47WbcJ/u6V3XyXHtLK31udYubUMwzjIWCmgjMMw+CjDxUC0DpPWNP65sgXoXX/Dzez07ebFGceVg3DOChMAGgYxu5pzQga0PTQD3TjmNa3br8jLZRSSCGxLItyuUQQhkghybRaL1+klCLLMoQQSCmxbZsoitb/fa19VIjtjZE+6Hpw/7p7DMPYM0Jvsy/Nser3eluMh5pmfQzgHtw7Pq52oLE/RrTmBuH93gxjH30P+KIQJgg0jAMqy7Jt/dzD0QL4IMQB5lp5054cr7WUYsMw9tPngSL9lkDDMB5YD34AqCEvW3GASxqKDBOsQB607SbDdAvapGPebxuDgHEcuh/5CQ1kB/gBSCMtgURiS4nvOAyUKzzz9Cf46pde5OzZkwwODuL7BbTWfPD+B6y2miTAqZOnGBoaxit4DNTqBL0eS6st/uSVV/hH/+h/4O233yaKYjSaLM36LdYHdkd8rBJww7S6G8ZD48HvAtaAttjTwGKvicQEKsBeBsFag9Zmv95vRa1p97uAK3j0Nu0WPMjHSeF5LkXfZXxohM986nle+vwXGKnXce2UVnOJgYEBqtUqFy9e5Jvf/CarzRa2X2KgVufEieOcPn2asdExLMuiOjqGWx0g0/Dtb/0hv/nPf5Pvfe+PiOOIXpjAfZoCcy8Utabdv11UhNjiWBuGcb89Wl3A6w7iBemg3vjuh708Pma/HjyCjx7jg32cXNthYnCA5z/1HF/90kuMD40QdwPefeN1rl+7SBi2EULQaDR44oknGK7VWbqxSJoJwl5IFsWE3YDjx49h2zZv/vb/Qn18nKeeeoqvf/klHj9+nH8wPMhv//bvEEbpFlUwDcMw9t9D1gJ4QANA0wK45/IkELNf77dbWwD9TVqFNPe7BXBjmZbby7BUCy5/+Vf+NH/2l3+FLIw59/Y7zFy6Qme1ycrqIqutBo5to9GkSUqhUMD1CzS7IZZt55m/QjI6Nopt27SDHp0wYGBggCeffJKvfvWrtDsd/s7f+bv8k//lX9KLk/Xt2O5T+kFhWgAN48HwiLYAfpSUcv2ir5QymaOG8QgTQmBZFq7rMj09zb/xS7/AX/6Fn+bqhYu88/qbLF6/wcrcDYJOlzgJ8YTE6k+ZXir6SClwpY0ulkjTjCxNyVTCyvwiURjiFz20SlhcXeE712Zo3bjOL/3yr/Crf+rnuLLY4M33z9PpdOh2Pzpa0jAMYz899AGgYRiPtrW5wtceBj3P4/HHH+cXf/EX+Yu/+ou884e/z7m33ubKhxco2i5ulpGpjFSDUuA6FoViAa00lm0jpYXnF+gFAd00w5IWpBlDtTorjUUcW4BSJHGXl7/3XWSa8FM/87P82V/9FVb/u9/gypUrJElCFEX3e9cYhvEIO1gB4MfOwL6VO//8/W/1E/s0FEqvv92jwZSCMT7e2vffsW08z+OrX/kyX/zCF/jpn/opXv/+H/HOH38fCxiuVoh7ASXXxkodLMeCMIAsxbMspC2wbBsQKCHwbRtcF4AkSUiCgKpfAFK0bROhka7LO6+9TqVY4omvfJ2f+MpL/Jf/9f+HOM4LRW+spffIfG0NwzgQDlYACKBtYO8y5ZQ6AMOu9X7tZgUi3af3uv+E3CzpYGfu/8OBcc8JkEJQKDi4OuPwYJlf/fpLdBdnmH/zR1STCMuyiEgJRUbmgi9dnDRGWhZpmuLbKbVajUZjhaGhUVZWQ1AZWmckcULZdeh1e1ieg3AclNIILcmSvGD5u2+9g1et8cUnH+Pt5z/J7//r79FDoUV+DgtzHhqGsc8OXgC4aSbhg2y/PsujdQNZbzi5i4HoJvh7ROQT+KLSlPHxMX7tV38FGcdcOvceVpYyUC4TRxHYNsXawPp5EaqUVreDEAKlFTaasu+zMHedysA4trSIwgDHthBaI0T+bbdth1arhdZQKBQIgoDVxipzMzOMjk3wtS99ifc/vMgH12aJFSAl8GAlhBiG8eA7gAGgYRjG3hGAIyWTo6P8zNe/zskjx/jwjdcI210826VcsYhdd32MoGVZWJZFmCZ4Kyu4rsPMzDUKRRfPcmivtgmCANvJu3/X5gL2XJd2r4tvSSqVCr1eXia7WCwCMDczS3XgQyaPHefMiZNcnZsnydL+GMX7tXcMw3hUmQDQMIyHnittyp7Pp5/5JB++/S5Ls3OoKMaRFjpL8X2farVKGIYUCgUKhQJRFFEpFAmjiHZpFd92EUC9MsBiJ0KTB4tZlq2XdUnTlCgMKZVK2LZNkiRIKSkWi0RRQtTp0Wu1+eRTT/P9H75CkoWkCNP+ZxjGvntwy9IbhmFsk05Tio7H0ECN61dnCNpdFmdvsHRjnvn5ebIsw/d9HMdBa41lWUgEBcel7Pocmz6Mb9lIpSm4HqVSCSnlequh1hqlFMViCSEEvV4Pz/Mol8v4vo8Uglq5QtTposKEU0dP8MTpMwilEJl61EZwGIZxAGy/BdBcoB4ce3msDvpwzDt91oO+7ca+EEDB9Zgam6CxuMSF9z9A97qszF2n5FqUPEltYIAgCHAch5WVFYIgQCQZjmXjex4DoxUcabG4sABphkCSJDFCCIQQJEmCbdsUCj5hlqK1JgxDyuXyelDpS5c4TknDiKpt89Lnv8hrb71LkGWkyrQBGoaxv3bQBbxfc+2au/buCfb2OKl80t0De0gkW0eAByD72+iTbD7ITe/PDCECypUKh6an+fDdc8xeuoKOQxYW5xiplymO1Wl3m/gdL8/0ba2QJgkyTRisVimXSzhOlXrdp9ezYCWi01PEqUJIgdKQZgqEQugUy8uDwjDuUNIuvuMikKRhhGu79FqrFJoVPnHmDEPlEkutVn8uFcMwjP2zgxbA/QoAjd3pR2l7eZwEHOzsxK0yxk1z9cGy1TmpgP0oWyRQaAqez/x8RNf7MVa710jtZZpZiFuwCZMetgut9gqLyzdot1tYOuPG8iip+CRjIwPUBz6gl7TpZj2iRIBw6Ha6FAo+lmPT7faQIsKvubiWje/YWDLFsRS+49JTKm8tjHoUbMlwpcTZw4d4+a23TEVLwzD23Q5aAA9sM5Cxbi+P0UG/Hd3psx70bX+UbHWc9vEYCfB9n8h/hh+Wfo2s7MEQiN55Rlt/neOHD1EulxgdGeXS5ctEvYBeu8tC6wli/j5al3nnElQrDV764v+XunqbmaVzKJ3i2pIo6JGEIRIFShJFGdK1cWxJlkISZRRch2qlQJwoEgVxFCGAz/3YZ3n1vfcgfnTqdxqGcTCYLGDDMB5qWms6PcUP279GJvLSLdpR6OoRLlp/Dbv4u9THJpC+T6k+xOD4JHZxmCvRf4NSHsgMkUpa7Srf/eNf4sUvBpzohLz95pvrs3nEQY8wjNCxh0WZNFREIoISRJ2YKEiZnJzAcSzSOGVlpcHk4Yyzj53Ftm2INeYh2zCM/XTgAkAp9y8x+UDMEmIYxr2lQbmHSbWXD2sVmgtfa1E+28HyjvJX+Xc2f5lepdXukoSS8X8xRGVFMnppkieWVyl5DhMjg1y5chmtNUJpVNRjbmmFeNFlbHiEsucTdxJsJL1ShFKao8eO0Q1jlFb0ej2OHDnCxMQk87NXiJPEXJMMw9g3By4AhHzy9nvNzAJhGI8IAVLnRZllCk4smf5mlQtCMvxkE7nJEMUkFaw0PJJUUlhwyFxYHVesjsONzv+Wz5e/x49/4QgnZy4yf+MGSZLQ6fZw51d488oc12bnOXn4CLYlyTS0Wl38Qp4N7LouaZLQ7XaoVKt89tM/xpVvNYmSBIDV1VUTCBqGcc8dyADQDOE6IHR/bqu9Ph4HYXY809t2MOzHMdLQWz3HqP0ejeAsAO16RtKxKf+rhP86+O8ZHqhh2zZBEPCbw4/xX048TyIlTlfwxO8VGH7fYeFoRuNwTKt8gt/lBN9Je3zt1Lt8+fEfUU6X6XS7vHHxGivJj5i9Nkur2WXs2HGk0gSdDo2VVRYXF5mYPkwvymg1myRxxGc/+xn+xz/8V8RxjJQSpdQtD6j78UBsGMaj50AEgBu7fS29P9Wp8+vr/nQ3J6gHNKaVoJ29W53I2J/yLHcqh6Nv2YaNN1fTKrzfJLDZ+aXJs8/36ngIpLQ55fwGr+m/TYbgUNRjTlucr47zN7/3H/Dpf1bEfewG3/5PFvnjsQkAPtO5zld/O+LGhRNIt8GfKr/K0drrfCd5ln8ZPceCqvGb7ef4LT7JZ+x3+Ir+DofL1zk5NcTCqZ9lvjSOJ1Z4MnoHyw5YWlhlaWmZY8eOI4BqySVZXeTIUI3jYxNcvHqVDE0XgZL5/MMP6IXDMIwHwIEIAKF/I+6XnJP7ctHbn6dq/cBewcVtv9+t/dwPdyoPs3nLign+9tudzq+1IH1vjokA9Nkf45vTf5qBRYnrp/ynn/lt/uHbj/P3jz/G935KEzgR7/yyQ1CfwFUZ/6fGm/wVNYP9VU325deIwpBeLwAkf47X+fPZG/xR7wT/vP0Mb2dH+X76FN/nKSZqV5j92gA9vwgo3rUcwivf4HMX/1viMCaKIprNJlJKQtsijboMjozzqaeewXFcFldXcDyPIIlo97okcWyCQMMw7okDEwAahmHcC/qzX2f5b/wD6m/6DABXxxT/1D/F6X8xybNnXV77yZhXvh6ja5rhD23+kzff4k89dwXHl+gsQ2cZQmmcfk+FACwp+Zx7jsfE93knqPJN+yV+aH+aOesIMoFiqokLmtTSXDzyczyx+kfU43cJgogwDBkdHUUpRRzHKJWx0lih4BcIgxDLkrjSw8tSsjRDZQe5FqdhGA+qfQ0AH9WxLKal6cGwNqer8eBbO46WZZH+O/8PAOpzeQmY5cmUv8Vp/jNSXvwnPnYscGVGb9Tihf+2wsQvdXGkha0FSuc9E0qDyPT6+DwFBEFAEASMBAv8ij7PT4t/wt8c/Y/pZUO4oeDQ2xa9mmbmiZRWcYrjo0t0ugFxHOM4Do7jIITAth2qlQqvvPEGnU6H5dYq2pIokVcqeDSvmoZh3Gv7FgCuTZr+6BFIIdD9j377AG/jYFg7N9d+N1mYDwfpOFAfw+sJnvi2xcpUxvJ4QiYk7rNXkK9N8MI/8/s/rRBWCtVXuHIu5PDRaZTIyLKMLE3RcYJK83l7wySm0+kQhuH6uVLUTab1B7xReY7adZfp9x0SV3PjRIrXnSPLMmq1Gq6bB6KWZa2fby+88Hlee+cd2kEPgSDNFEqYXCXDMO4d0wVsGMZDSyiFWJjh6PljHH3bobKSEVQUrsp45slv8uqTAYtvfxkAJQPE4/8B7fgGs5dtSr6DW/RI05Q0TYnjmCzLSLOUXhoThCFRFOV1APuB3M+t/DPeKz7F3CnN9DmbSsPiie8tcqj4AXaxjuu6eeHnviRJCMOQgYEBnn3mGbxigfkfLplxf4Zh3HPbDgDv9klU6O2tQ+j9fObdn6us2PBW9/rT3Y/7hkYjNnyytcQX8ZFPKwG9i43c5sljGLcRQlD8b/46J4d+C4CLn4yRwP/5+rcppAnP/ZlvsPj536LZ9agfjSgXLUbcUxQiTZolpEGGyvKxelEUkWZ5MBhmCUmSkiUplm0h+4lHU+k8v774N/nH4SeZGR3j8cZPMHR1imB6hIFShmPbOLaD0KDSjDiMiIOAqdEJLK0Jux0sBBmi/z0ykaBhGPfGtgNA725Lpmz7WibZn/IsirzUxL1n3dKbuFWG6t1L0aT36YYhhMCyLCzLIssykjT56Jg6vZtjm/XLx+yv24crmG77B1f25vc4rRtAmdPPzvIXu+8yMvMG3TBmcLRC8dm8/l7RK+NbNjLTqDglDTPSOCNIY4I0IlEJcRyRJAky1UjAkzaOtBEyP1+yNONIuMQLb/9dfvuPXyc98fvYfIHVpX+TqbG/h6MlrrTIwpg0jEiCCCFspuujPHv8OK/80fcp4CKAUCmUSEGY4QiGYey9HbQA7lcTzL0LkO6Hj36Se/PZ8plE73+QYlkWjuOgerePddzN575/86OaxJ0HmxACKSVpmlLXxxjIDoHM+ErtXzHaEsSuSxZGCGXTmmlAlvHB7A08aXHq2AkG64MIEZPoHrZtUyqV6PV6/XNBgE5vFrIRon991Fi2Q5Ap2r0ApTVzrf+IQ9U/IAq/QGPlmwwPtpFCEscxnudhSYnqZxpPjk8wPDiEuDKztmYepmuhYRgHixkDaOypLMuI45hUpVhiq2LMhnHvZf3yKWfsn4IMameW6ASLtOebPHnkCMthj9/8l3/ApZlr1KsD2FpAmvH9l9+n6HmMTpQ4eWqaqakpLMtaT94QCLJMr1WTX6cBJSQr3S7XV1bpaQhabzIx9FvYyZ9hdvbPcfrU38P3PXq9fGo6y7JI05Rut8vExDiHDh1CvPEGZOahwzCMe8sEgA8RIfKM453ai4xXrfX6YPm1MYEPU8vZdjLYH6bP+6DTWq8nZxxJvwLA9HPLqFRxfWGB559+Enu1gDs6yC//yi8yMjRMyfUhSYl7AR+88y5/8vu/SbuzxNTUFLZtk6YpjuMghSDJNGmSEMcxwHpiRyeKuN5Y5YPZeTqZptfu8P7y/5UnBn6SOD7FtWtPc+zYO2itybIM3/eRrosQkmKpxOdf/DzffPlPuHxjAc/1iFKF1qYOoGEYe88EgA8VwU7jv70IWm4fHrB/wwX2x07KF5kg8GCR2uYwXwLg0LPLXFiIWFpcpNnrUBgc4NTjp7l0+QKt1WV8x+X8O+8RtNo0l1cYHRvma1/4DJVKhW63C/S7e/tjW7MsLxGjtUZKmT8EIZhtrLIYRoTSJ9SK83PnmKz+Xer8NT44/3VOnbpMp9OhUChQq9Uo1+po36Pg+0xOTvLFF79E8O1/zezyUv+97uMONAzjobU/k+Ea+8fcLLZH73AxHkjTfBaPCspdZfxYC6mh1+vx4aWLFAYqnDpymOid93j9n/5z3vnNf4F14RLVGws84fp84ZlnqFardDodxIb5yvPgL81LwqRrv2ekWUan1+Py7BxKCDJpkUqLbpry6tW/hZA3SJJhzr3/6XwquDCk1WqRZRme51EoFqnX6kxNTTJYH2RqcgpLWua8NAzjnrjHAaAErB0u+0XsYtvutNz/WFrofB5lqXaw6JutGrcvD6fdHHe57ZvtVvvy4d+ve02Ctna/YAM2p/gpAKKh18mEomg5uIlm5v0PKTguSdjD1RFT1RLlMKQSxRwdrPOJs6cYHR1CW+AUXKI0ItEpURYTpQlhqogVpEiiTNOLE3pRwlI7YLEZ4FgWQsXILEFKi+Veh9j6rwA4f/6LaAbIkpAk7NJtN4iyLlHSRtoJ9VqR0cEyU8MDjNSqSEBohdQaWwg828ECbDvPul/r7l6jtTYF5w3D+Fj3uAtYcnCz2PY6w+7+lmpY+yTWDq/5GoGWm0858PDdRO7tubjd4O7h2qf3yt2XnRLAcb4KgHvyIrY7hQoT3FjRnFtk8foNTn3iMVqri1x+5U28coXDYxMMT09QHh0itfMSLGmaoATEWUoUR0RRTBinqCwjzTRaC1SStwZ2wpRI5dPH2VmKVIpMWMRCshz9U46U/jJRcIrLl3+K08d/A5HFFD0L1xfYjqJUcpmeGGawWkDolKJjM1iu0gt669nGmc4ouB64DpnK1pNd1jKf186vjd9f8+BhGMbt9mEM4EG98Ozldh2MG/puC608Wna6lx69PXT/7cV3U1NkmAmeAWD42SW80mm01pSKJRIiZq7NcPqZx6jVa3z28y9QSgQFy8Eu+sSuABWhUcRxSpZq0lQRhjG9bkAa5QWi18YAri2Zyv9OAJaUsCHBKgwCKof+DtHsf8blyz/GkalvEQSLRGFEvNKgVK4yXKtzeGKSF37sM3xw8RKHjp2g2e2ysrJCGIbr4w47QZd2GBBGEQClUimvv5kk67/f3jJoGIax0f3vtzSMR4xpjdkfx/kyAsmCeAtZ6WDZNsVikepAlanJSVSmCKKY8UPTlOs16lMTVEaH8QYq2L5PpiVhkNLrRPR6MXGYEQYpQZAQRdH60u12SZIEx3HWC6HDR49znMRo8UOqtZfRWnLu/C8gpaTX7WIpiLo9wnaH8eERPvH4kzSXVtAqo1QqIaRAaUUUR1QqFT73mc/xsz/zszz33HNMTk5SqVQoFAoIIYjjeH3aus22wzAMA0wWsHEHt49Z24tyMUbOFJq+t6SUnNRfAw1XrG9xKkmRaBzHoVQqMT41SlsHNJYWGR+rg+8ihYVOM3pRwGoc0Gw06TW7dHtdwjAkCiN6QY8wCNBJvF4HUEqJ7/uUy2XsVo8sUyitidNby7ekaUoURQwP/wPazU9xY/4sS42zFMszkGQk3YBSoYrIFEcmp/n0s5/k7/3jf0xpaJBr167RWG0QxzECwfe+/0dox1pvn7Ztm1qtRq1WQ0pJp9NBKbXeDWyCQMMwbmcCQGNLJki5N8x+vfe00hznywC0Rl5Hq1GyJKFarVKv1SmVimRK0G63USLEakeEqcBS0Ay6LPVarC62iNoBURyTxDFJkrf8pUmMa4FtSex+q2K5XMb3fXzfx3FsdBLlx3nD8Q3DhCAISMsfMj39HWZmXuLNd3+GE8f/O7I4QScZlgZXWvTaAV/54ku00pR//nu/i23beSC31j2uIU2z9d7yJMnXXSwW8X0fz/OIoijvjjbBn2EYm7iHAaC56DxQTBzy8bbaR+ZUP3DG5FNU1AQJAc7xeT680CaIE4bGRugsL1AolUgzQaPRINUFmlfnEO2AqBvQjgJaSUjaTSHW6+VetNa4rovr+jhWhuc6eK6L63lYlkSj86kQbQetQ6SQbJykMUwzukFINwg4Nv1PuXHjM6w0xrlw6Wk+NX4DlaZEvQDhFtBZRhQnfPq55wkF/NEffZ9Op01X9da7dpXKbjklpZT0ej16vR5SSpRSJvgzDGNLOwgAdxMr7ubis9clVVL2J7o5yBnPABmb7QcB2Nvu2c2PiwayLSqjPJwDzwVbn/+b79cdrd20CO65Yypv/bvh/4BmZ5l3mtd49YNzPH3yBGm9glsfwFvVZEFGc6bJ7Mwy89fn6Ha6WLaFAqJuF8fK5+11bJtCoUChWKE6UKFiZ9iWhW1bSCmxLJCWwnNdtLKxhItcqzSgLUDQTDUN5UAnotq7wakz3+Tdt3+Gl1/9Iqee/v8xPlBEWxmuL/GUxUpnlagdUNCS55/6BCcOH2Fm7jrnL13g+vwN4ij8SPmXtd/XgkQwYwANw9jcDiItuYtlNxcescv32qv33421kjJ7td17vWy9H7a/1eKWJZ915FGocyfusOzB2h/a/XZ/neDHAXg3+h3efvcdLly6zPe+/wO6UcLo1DTadnC8AqQQdkLarS7LjSbX5xeYnD7Ml7/8FUqlEotLi0RxxMpqg27Qw7ItqtUKAwMDDAxUqFYrFAo+jmMhBSAEWgsQa9+7fNFIMg29JKEXJ3SDgErtt6lWV+n1yrz+6qdI0oQ4iUBoCsUC5XIRx7Y5dvgoaZxQr9UYHxujXq9jWXnN1M3On4f7+2gYxl4xWcCGYTxUbF3gCC8AcFH8IVJIklTxw5dfYWm5ScGvILTNyPA4tuPh+4V8jl+Zt/YtLCxw+PBhXnrpJSYnp1hcXFmf51oIgeO6eL63PuYvH/fnfGTM3+3iNKHb6YLWtDsdoqjNJ5/9JgCv/eg5lpclQRgAUC6XqVYHUEpx+NAhnnvuOc6cOUOaZQS9gLGxMaTcz8L5hmE8bO5pALidWRFuXdbal+7ul2E8qEzLzd07wuex8WkywxLn8vF7yqJeH+XwoWNUKjUKXplKfRRLuti2TblcxvM8isUi3W4Xy7IZHRvjT//pX+BnfubreF5hvbxK0OuRpinAeqYtAEKAyOfjlkJgSYlt2UiZt5mnmaLdbhMEAZ1OhyiKGB35ERPjc6Spw/e+9UmSOGFhYYE4iqjVagxUB7hw8SJBENBoNKjX63z2c58l6U9Bt7EGoWEYxk7c2wAQgRRy+wsCKXb4mtsWEwAaDyrTdbc31rp/L1vfwnEclFKMj0zy6ec/x/yNZaTwsCyPTqPDyPAYlmXhui6e5+G67npdv3K5hO/7PPvss/zyL/8ijz/+eJ4QkmXrGbYbgy9L5lnBtr0W9AmktBAiv8wKIMsy4n42caPRIIpCXvjctwF47+2z3LheIUvzn8myjKnpKcbHxpmamuTy5cssLy3x4Ycfcvz4carVKp7nmXPFMIxdMV3AxoNBb7EcBAd52x4ha7t9LQD8QP0+cRIzPDzCmTOPc+XyDCvLTWzbp1QaoLnSoj40SqlUwvM8KpUKruuilCJJYtI0w/d9siyjWq3yqU99iqeffhrf93Fdd30cntwQ+LmOi+t6SNmv0bfhPLCEwLYt7P7r2u02q40Gw0OXOf3YBUDwr//XT5FmGWmSIKWkUqnwxBNPUCyW+NznPsfZs2cZGhpiYX4e13NxHGc/d7FhGA+Re1YGRqylF+idPp3u5dOsIM8q3undWO3iNQfdWkmK2+0uWpF681eJO6xqd2Wk19JU9m6Ne2urhBCdjwe7i9NZCGG69nZCa6pMM8JZFBnX3O/jWz5PPf00V2/MUpg+gtKKOI6o1KvIRoFAprgVH7vo4pY8LMciCxI6nTYDxUF0liJ0gudIbCkpjtTRQxVEEkB/XVppRH8OXumA42mE1EgEjhAoFMLKsJXGtiRKJQjLIUojbqwsMN2b4IUXv8OH548yc2mKD96pUKq0KFXLCMsmDnoMD1dodQs8duoEtVqVsaEhfut3foduHGMh1q9YZu5fwzC2a89bAKWQWNLqd8fasOPl7i5cQggsYfUXiSUcLOHucLGxpPWRRYoHucFUkgfDmy07Z+m8fMxHFi1wsTZddr731spo2JsvB6K7f6u86bsfoC+EQEqZdyeaG/rHE4ITIm/9u86PEMWI+tAQs/NzzLUWyZyMUtlFOgrta6onxugWFZWJQdxagcxWSFeAVLRbDSwSXBEzULSwdIArYzwroegJHM9BOjaO7+P4Hm7BRzo2lpfh+jG2yPCkQwGbiq2peSkVz0IlMVJk9MI2qUiYWbjGwuo8A+Xr/Nhn3gHg+998nnZjkW5zAb+YMTBoUSwqjh8Zo2Aplmau0l1a4ue/8jUeP3qSomXjSglao8UunrkNw3gk7cNMIPfzarSb934YW1zutB92/nn3d69u9W4H4Tjt7X417pLW692/F8U3KRdKjI+MIQQEzQblos/oyBDtZgPHHaBY9Oi0NeVKBcd10YDjOlSrA1SqFbRWCAGOY+Nlbp74oTVZvyi0ZVlYlrXe/ZukKUMCxgZrtJZTRCZQaYpQCi0UY8ODlDyPsNvBLbo4lk0WxywtLhIdmeKLL73K66+eZmVpiD/+zjif/+oSzkCFQr1GfbCO60Y8/niJbickSVLeevd9HMumVCqRdrvYQpAJjdIalDn/DMO4sz1t0jIJGIZh7Jfbu8d92+dEf/q3+cIfcebEScaHRpifvc7kyCDPPv04Rc8m7DSxUBSLHiqN6XY7CCEo9Mf2VasVyuUySimklDiOg+d52Hb+vLyW9WtZFp7nUSqVKJVKlEslJkdHOHP4EKPVMgUJns7wdIajFN1Gm4FSmcMTU7jSxrNsiq5H3A1YXFhEiBZf/trbALz+Jy/QaqY0m6ukaYrnuVSrVfyCz9Fjxzh69BjjY2MMDg0xOT7J2NgY1YEqruuaq7BhGNuy7RbA7XZ/PuxdVbvpBlb6IIxVM4yHy1q2tJSSwcFBxuNn8FfrpFaHn/6LTzMze4WFxUUmR0f587/2Z5isVqmWC5SdYeKoy40PrlGrl4lXY4aGBvlAKdI0ZbA6QLlcxtYRts16K58QgizL1ufXXWsBdF03H/8nJUXb5akTx5m/sopMPcJUk1kRnbBBnGriTo+R8VHcLEFmmlLRJw5Coiii1Wrz3I+d44+/d5qV5Sq/9z+P8emvv8NZz2NkRGJJj8pADcdd5tChQ3xw/iJuscjFq5dZWJgnQaPIA1RpwkDDMD7GtgPAhz2w247d7AMziN8w7h3btlFaUa1WObX0NQAGP9HgF/53/xa/8d//Bp944ilOnjhOrSY5MT1Fc36B2fffpz5Uxh8q49kV6tPTtFZXicKQdrvNiSNH82netA2k68Ge53mkG+rvrWX/Wpa14XuecerwNJem5kgDie0XqI1U6KVNWt2EJE45Nj3NzKXz2BpIMoRrE/R6NFdXGR6b4Ks//Qb/02+8yOyFl3j3zW8z1/hDXnrpy0xOHUMKwfThw8xeu06tVqM5e41PffJTuOcKfHD5IlEYIOQW8zQahmFscA/HAJqAMWf2w71z55kXDsSu382N+CBs9wGlb9uhcRLjuS4zM1f5+ULe/Xvsiwnz12d56rGzpFlG0fdRSYfzb77O0gcXePzYEaqlAuXBAQbGRmhcvspqo0Ecx/iFAocOH0baNlJn0J9TV1oWrpRYlnWzTEu/BTAPAPPtkWh8qXj6sTOsLkfguHiOpF4bZmqqQpoqiq7DarFE1G0jHJuCX0BpWGk0GG21OXzsfSYOnWBuZpLLb/0EjeTvYdsOn/0xmJw8TLlc5bHHz5JpyUq7xfd/+AOiLMV1XcI47u8hEwEahnFn2w8A9W5ixQf1TmaB3svhkVuVYLmTbBev2Y21Ujmb0RyMUitb0BabD2PVILL93ppN7CYTeOvt3k4L9MPf4qzBzZMzhMpLrfie5sVPvkTlO2fQwNjJ1whX2qAylls9FjsrDMgFBlTC1LCHjJuk7YSyN0rabdNemkMnIVooBkaHGTtxHGtwiLS1jCsFmRTrrX1Sayyl0GGYZ2hbFlg2aRKTaXAsm7KjOfNYlbmF69xYXsUrerhFn14KrVaLG50OYSbALdJMNEk7wk00IsjozjcYGfX48kvf53/4jV8kXP4SY+qPmbt8gXfsl8laDU6cOc30VJVQHMMd9GkETf7w29+l1w4QOq8zmKrowb38GoaxL3YQ1T3IJVB2Qtz2+13SH/nDDl64X0HMVsf2IARRW7nTcdrPfXcnuzmHBJudKzsZfvBQB4EiLw0ktKLoWpyZnuRPfe3HOal/gXPfllTGlyiWLoIuIYWgF3YQrkcQLSHDLr5dJM0SQhURLS3h9FwGLAtVH+KJU6cJEkXQC0HY+OUKMuwC3Az2AKk1nrCQ/RZBrTVJphDIfMwgCkXG6ceOYF+xsQslpO0RC49OEObTwKUZQkiSJCHshAyEEb7wiNoB/qjkyOQiR0++xeUPn2LurZ+jfvw/Ys65RLhyg5WZDzh68iRHnnqeycNTBEGP+bkbLM4vkKSQZul9PECGYTwodhAAPkqPk3v5WdduxjtZ537ewA9ymZWPc5C33ZQguie0QCqf4UqBn//SC/zZn/wio47i5b83BMDU05ewhSQJQ3xngKgbYCsoF0sk7VVKJZ/RwSG0EOgkpbvcI1ttkzQ7DOAwVhvg6jsfIHoZJ4+MIfuHcW0c4BrbcfKg0LZJogi7PzZQSI0tBXGaMjY2RqItmp2AMEmp1WrUajU+OH+eKIwolYr0ej10mNHt1uhpi263S6vZYmBqlEMn/ylXL50l7jyBk3yO4co1RBYzf/UKSa/LtHYZOXyMn/jii1Qsm6jT4YevvUE3Ugfi8ccwjINtH+oAGrsrj7PVDBNbu318lGE8LLTWCClwpM1jx07y7/6lP88nj47iNa9TClZZef8UAMeemaHouCy1A5qrK3iWTWu1xaVrH3JmtA5JSnu5QbFaoddss7q6gmq06fRCmo0WK2KVbidATimSMAJHk2X5lHAbWbadJ1v0i3TL/kwgWRaTAUpp0iTBcRx6wTLtbsi7F66xstri/fc/YHh4iHK5RJqmdMMOcRyjfE0QBHS7Xfwo4uzjA1y/+j0uvPslWtf/ArVn/wtsJGG3TePaFZYXGhw99Rjl4VHOHprk//If/nv83//f/zl/8L0/QQqJ0trMJmMYxpZMAHiPCSF2EQD2p2e707xqm1BamYv9I26tNArkQdPDcD5ordezbT/7/HP8x3/tr1LRMXplDjcLiFeGaK3UseyU6TMzIByyMCZsJ8hMMjkyRmWgQ29xjqRkMTp1FM/3We61iIKQwcEBxo8cZbQbcvnaHM0oJswS4jTB9z0gLwWTpje7Vjd2yat+WZi1YCsKItJMkWZ6fY7hMM4YqA5w8fJVfM/F9zyKxSIrKyt0u10ajQZnJg/T6/UIgoA0TSmVyjz2+Le5+uGnaDWHuXzuk5w5/i18lWIDRQFyeZEgCum1GoxPT/Nv/qVf5dzFC1y6sQJao5QyFRwMw9jUozKwzzCMB5SUEqUV04em+fVf/xucnCpgxQsUvJjKUJlrl54EYPrMLKWizhMx/AJnT53hk09/gsMTU8TdAFdIpscnSTo95mdm6ay2GB4dYeLMSQZPHmHqiVPUjx+iLTOuNhboxhFJkgDQ7XZv2aa1eoBrJWG22u4wDFlZWSFNU44dP87nPvc5nnrqKYaGhigUCv3agorLl68BebCbpAlBEOA4DsM1weNnfg+AN97+GkQ2BSmouQ5HB6oMS6C5Qnf+Oo0bM3zuhef4K//2X8rL2PSDZsMwjM3cXQCo73Ix7mxtYs/tLns+TlPcYdnN2kxLxLbs9Ljv2/mwPzQajer/ngdFriX5ma99hcePTrD4wevUfEWt4uGVClx9N+/+PfLMDK60sRFEvYCX//iPWF1e5NKHHzAxPkGhUGKgXqdcrTI2PsrxE8cYm56CkkfmWYiyz8SpY3zicz9G4lr84I1XeOOdNwmTENdz8qnh0Aj6Xb1ZTJYlaJWB0nmDvRJoYaGxiFPN3PwSGovhkTFUmtDrtEiiEPqt9ZZloRGsNrustlr45SLdKCAIA2wBvuNwfPo7VCs3iOIyf3j1l3lr8jRBuYYlwBbgWQKSEE+nWN0WP/0TX+WJs2eQUtw8FbRAaGGuu4ZhrLvrFsC1IiI7WR7M29J+swBnZ4ve66d9ST5K4PZld0dR9MtpbLYYa3b6bbrDoq073vDXui0PYhehFhnaUmgpQQpGijY//enTdN76LmOZxYjyma1P8uunP8mFdw4BMPTcIq7l0mu2IAmZGq3jiIjD04M0ewGBElyevU5ChlW0UW5GmHbzzNxej24UU6xUmD56jPHpad6/fpnff/nb/K9//C20A5IUEUfYWUKatcl0F3SE1hk6zdCxRikfWRglkVUWGzFYZbohJKmFDlt0FmdIew0ckZHEUZ7VLG3COOWtCxdxRgdZitu0wyZxe4VaxaM+WuLx5/8lAFff/Qz/xeN/jr/yM/8u35o4TqfoQ9HHd2ysdge/0+P4yCg//+NfwXMtsAVaSIS28bSDtaflrQzDeJDt2dXgTm1Ft7cbHbzbzUGz3b15L/fsdt5v5+vcGHQc1ODj/tnNcd/9cTrI+1+g+0W+83bj6clxjgzWCVeWIFW8WyzxHz5+irn3RpCBRTyS8J/+mcM0VEar3cLzPQbqNWauXWV+aYHRsTF6QcD8wgJpmqJFXkQ67vXQq22ypVXi+WXiG0sUowy10uLq5RscOv4EleFDLPUSUr9K7JUJpEsmPDJZIPVKrAqLmTThe5cv8t1LF/jRzFVevXKR9+Yu4g/ZHD05RGVQkRUTKtM1qkfqFCbKlA5VqB6rM/rYGIefOUxat4jrAu/oAN1KSqsUk4xYyMNl3vq1IaInW1iJ4OT/WCW2LP7m018h9Ar4nk/R8VBhRLS8gm40+eKPfYZD4+P4rnvLXjUMw1hjkkAMwzhQ1nKfdP8/tpScOnoCnWaoJCMj4xvTYwDU/3UZgKEvX+bJ8uv86EWNE9fz8uWWxXQ2iZCSyIKxn3oOKQRv2BZCKrQQ/ZgoRZMCEVr0QID87DB//n//V/t1B+ECGRdYuG1LUyBc/z+bSez+33qUmGBiw7+CZIxDnOLQNvfDan+BOj/NZRr/Xoc/+Ld/iYnaCpeJ6dg+l5wyn0hWcC2bbrdLe7mBU1rh+OgkZ4+eYGZ+Md+XAjJteoANw7jpngaAUmzewJitP933f26LLsCHJYtxv+QZxztr1NUcjH288RxQ6gDPPmLsC6nza4QQAhs4MjlFt9HEUxqtNF3LQgFHfv0HeD9VZbi4xJDogQ+Zv6Fe34bZWCzy1rDt1cjbfWuZVv1IS+fjAgUgtEZpyNKMNM1YuwSqLCPL1PrrbEviex5ZkmJbFpa0sKRk1i2THov47G//MxjPWJk5zivBFE6zSZpEaKXQcUJnZRXHW6Z6eJxPP/403/6THyC0WJ/TR5tGQMMw+u55C+DtXUz5U/32Zjo4CIHJg0TAeovFtun7Xz9w47E3x9yAvAvYIm8N9CybsaERLKVxhIVj2zzZbPPq8CCyrAi+2mF1OeIP0mf5P5w/j7+4jFcfZejU45y/cJlz5z5gIA2QrSad2Tm++PzzeCWH1NbYSlHqxQgtUVhkWCBdhOWSChstbLpRyOzCPCutJrWhOijF3Lvv0Gp1WO2FFAeHKQ4MMbuwSK1YZsCSFB2LsguHRmpUfYs47DIfC773ozc4d/4CiZJk0qHRbKNtibIVBc9moOTztS9/kbDTxLclx48dZ6g+zI8KLr/72FP89fHvcYM6yhY8M3eZQ41FMsfJH5pSRZKFBI0m1eGYulNAJhlC58GfKQ5tGMZGO5gL+B68ez7E587r3+77mifbB8PGY74f77VT5jw6EGT/QAigUigxPT6Ba1l4jovne/zS7DwXqhXSet7C5yQpP3+lwekbAbONFCkFzUsN5s4t8Op33+alx4+hlkLiay2sYxFl4RPIBKES3DhFI8lEnuOLpZBaUYgDbC2oOZKh0WHiyWEySxCEXZiocGh6ENvxqZTqWJbH9aJPrVxA6IgwCHEdB2k7RNKmnSVcuXaVuevXUUrhuD5RmBCGIcWBKq4vGR8dYnCgzGqjwdHDUyxev0a73WagWucnFi5wWMxTGMpbyp9pXuUnv/8jUBlJptBKQZaRZhm9Zgvdi5Bxlhez7u9Ixd2N4DUM4+Gy7QBw40VjY4uNAOQOb7QSsd4VkXePsOlVaePP3Unejbn5Oh4tYheZwGudQzsl2TrC2nx9cosxSOtVgdbOibtuEVzLTd8p00ZyUKQ4CCHwUAx4MFJycFWEJ8C2BTYpf+Pc+/w/H5sE4HPL85yY6xCqFCfT9GYXGSyNk/QyLKvA6NhhGp2IJIUsy89FG1AIUssFBEpIQPYvaClCKoTSSAS+kDgIUqWR6Hze4F6A5/pIJclixeDRCXQWIaVNRpUwzciwaXYDLt9Y5NV3znFtcQnLcRkqF6mNlHB9l0xrtIiZHhtmZHCQou/QarZRSNIkJQm7FDyXTwUzLMb55/1E9xpeGqL0zWEcWmmyLCUIOqQkpDrG8WyIYoB+CRvDMIzctgPAjSPLLMQtXY076XVc66a0b78UbXqfF9taeaZVPq7wkba2n3YY+Ojdjrfbaqzh5uu75UHhtkOlgbS/utszU5Xa5ewmuymJI9RHN87Yd1oIlHBAQEHHTA6WGLBSRNDF8S3QGUpBUHJBCITWFHVIplziKMBNgUQw8/ZF2osdpqZPMrvQpFaoUamOEEcZZODZFonQaGnl5ycCS/SvOVqhbUm6NqsKGp1mWEBJuGSZwvXyy6dCYbv5+aYsgRASKSxSAZ1OwFJjmavXrzG7tEwsLBwh6UUhbsHn2LFDXL1yBUdC1uuSFgsIz6Xd7OC6HkEQEHRaVAaHcEtQiPNgLhtw81I5/fNVa02qU1KdIaUitjKEb1GuFFlKQiwpcYVNmiVk5hQ3DINdtgBu9v87sZdPoeZattF+Pd/v7n22etXeH8PdbJ85kw6qifFxbDt/ZBTrmbvQ8fOnBj9LULYiSzKSOCPRgkAr3vvgfcpHT+G4HkfGSmTzc1wLAuJ+EAUbz5SP/kmj1x9GBALbvnm5FNnN1mIlbj6kSGmjVEYQRay2uswvr9Jq94jj+JYp+rIsI0kSer0ehw8fIuy2aK62saRDbWCQNNUEQYe4q6iXPLqeT2Va4if5tsdl95ZZPpIkIY5jsizD9TRxGDI6Nkqh4GNZFnGSEROTTzF5d8fDMIyHw8NTFdRc1AzjoSL6A9ZKpSJxHOO4Lkkcr1cH6Hh5AORnCZmj8u7SWBNLG8oV/KFhzl+8wtJCgziKUVrhuk5/7f1uU/KctLV16v78uWuZ6GuBnRC3FjG3LGt9Wft/KSVaKaIwpNNu0263UVphWRLHdfH9fjAWxywuLnLhwkVu3Jin1WzjeyVcp0Bztcu1azfQyiLoxQgkYRDS7XaxfLXeApiUXaz++67NQZymKVmWEQYhQa/H5MQkfqHA8PAQjuuYS6RhGLfYfhfwFiVdDgIhBPK2bkPDuFcexpI1WxWEvm9Z2ZZEoJFSMjkxAUCaJrhSkCQJSima/VjOTxMQ0O51iUKJKnn0LJv6ocMMihXCKGNpcZknp8a4ZjusrjbIshHSOEIJjUgzNpum0HGc9fl0b98/t4yD7v95rUUvDAOSNM2DQpEhhCSKIjKVoZQi67ceFgo+vu+RpilhL+XQoaM4jsPy8jLeYJnmahfbdml3OhQ8G2mXKcT53MRxySYVIJUiDEOCIMCyLDzPI9aCTqdDdeIYaFhZWUYpkWf8myjQMIy+7XcBH9AZA4B8HBDkORCmjIixD9ZaXR4Gd/pu37/PmI/Dsy0bx3HJsowszcgsBVkegHfcfLsLSR4UdZMeUdfFqxc4dPosT06fRn7nNa5fvs6li69QJ0ZKSZKkpGlKohLiLMWT3qYBoJRyfUzq7VMWbtxna616YRjms4z005qEFEjLIoojoii6ZV9KKfE8j9HRUdI4o+xVsKRLvTbE1NRhbszdQGDRaXfJ3ITDhwcBcIIUlAYp6DkavdQmiqL1YBVAp5oojhh0HCYnJzg/P0fSW0sEMYMdDMPI3bs6gHdzldlprLmWAbxFksGBcGDj57XCEHu10+7igx6E43anbdjw0R6W4O92B+1z2baN7dhkWUaapaTo9Yyhdr/gsx/mwU0n7WLjoS2L1SDgO7/7+3zvD39ExS5y6nCNb3/3e5yq1eiGMd0gxCmJ9fF48NEzV942XeF60HfbD6ZZShiFKJVh2RZJ2v8HrYnjiDiKsKTIS9v0h+CtB4AjIywvNSgUihQKJTrdHnGaISwHr1AmjBYol2wyGQJFki64nZi46tFxNXYU5fvItlFK5YFtJpBhRBqnVCoDeF6BbpDk5RaEuO91P++Hzc7rA92oYRj74J4WgrZ2kYmphNrxBUpya21pq/+krjWoXWe57p37vwV3IpC4W/zTzfKx258xRLD1aZWxWYQlAHuLnZQJgZKbX6j3vPtVb7Hd4tYyug9St6+QdyrXs7kDEQTqDdmtabqe4KBF/rBiWRY9Pz9eXi+fcC3zIuwsIEoi/qe//4+YW0546rFnGKkNkBSafPHf+MtUEov2zAxLKqWaxHiOIFMxQmukBqnyKgcCQRymqP5sJGvH3LIshBRgp3mXbpYRZCGJiMnsjDRJSW0brRRx2CMMmui0jUNA2G1jY+G4PgATI6P02h3isEfiuwSqh1cs4ddr1EsV0utzRCHgp+hC/v6tZoBs9qDqEZYsKuJmcsradsbdLl4xQLUzjk8+Thq+RtEfoJMsb9rS+ajQOh9ScCDOb8M4AO5pACj6v7ZrN0+m4rbfN/7pIDzp3v8tuLP8KXizQL1fnW/tKXnbM4ZsVWr29mba7b5CbFoJaO8v4nfYCi3uqmHzflnfbw9wS4fSil6vR5pleTewFEhlobWmW+gXgW4FUAWrbKNDhW3buLZPwbN54uxZsqBNz0qp14YZKQ7jOWUWLr+NN1QljVeoCgfZn3nEEiAzjc4UGkXcTzqxbXu9xUiKPNkjUyoPAnXerKcBZVkoLeklIc1OQKwUSZb/bH2gRiacmy1PSjM6PEK1UqbRXqUXdvCrFQaG6sQZVIaGaM/cwC2Wqdby8z3oZohmDw7VsUYqFAqd9cBvrbVSoFFJgqUlJa9CtVTn2sKVvLrNvlZiP1iklDiOk59Hman5aRj3fCo4wzCMHZMS0c/KbbfbZGne4pYJgUwz0jSl228BdJt5AKhdiW0VSRLNn/sLf4E//O6PkFaK4wt6C6s0Ll6ldrTM+QuXKVWrzGYBRSWpWy621lhKYEtQaUqaRGApoigijuP1DF4A2X9gypQmy0AruV53MtPQiWPmVlpcv75IpvIkELdcZ8yRhHGeCCKlJMsyzp8/j+PaeEWfkZERMqDZbILlUiwWSVJFrTZEpdoEoNtModEDICl762MTN86bLqUkTfO5hDOVB8T1wTqrjTAPfB7N+G89w9u0ABpG7qEPAPd0nMdtrWCCLVqGDsD8uo8CM4fwQ6wf0GRpRqvdRilFkqRo20ZplU95VszTgEvdFLDAkywtdjlz9gRDJ0/S+Ma/RKqQuLXMaNll9sKHfHhuBgoldHWQp59+Eqt1g96FixQtB2lJ0lSBnRHHgjBuEmZ597Lod7U6joNOU5SVd69nqSaMUrI0RUhJmMGNZsDscodmqKkO1AiCAO04+JZE2ul6S53v+yilKJWLJDrFcRxKxTJzN24wfeQ4Fy9cZGW5wZXLks88nyeBtFZS5Go+vVtUctD6Zq3CtZatLFM4AqS0WF5eorHawC70x/49osHfmjRNP/6HDOMR8VAHgHl3yN5d8W4fB3f7rBVb/Zyx9/ZsxhDjYMoylIBMZbRbbWzbJunFJJZGepKe1GR2Pta3Fsn8ccuTCO0zN7uAM9jk8OFJjh8Z59wbLzN9ZJLmSoQT+7x78Rre6DCrHRgsDNHzF2gFIQNFH2lppK1JhU3a7ZFGbbTWRP1kC601mVIIx8OyBSqTZKlASo8wDJhv9lhsR2i/Smm4hF8u4Q5oHNsmbDYR/XX1ej2EEJRKJSxLkvXP31KpzPJ7H9KLUoZGJ1iYcxkarGJZAqU0vZbGauZZz9lA3ip5e/3CLFPYtoNT8FldbdLpdEm7IUI8ut8Pk/BhGB/1UAeAhmE8+FaWV7AsizRNSVOBldn0vDz4c8IE3QqBAk7F5cbcMt979/f4qlcjCLu02is8/uQp2u0GuugQhBmVgSHeffM8rW6bL7zwNLJeozjm8qNXX6dWKhO0O4hMcbRi98vG5LNsrAWASoMlXOK4X1YmVgihabUCVls9YuEyOj1OGIaUy2UKhQKL8/MUM4Vn5eur1WoIIRgYGKBcLXPl2lV6vR4nhwZ57vnnOH/pKiPDw8RHjlOp5AkgQReSRGM186zntOptur8sy8KSEuHkNQ8P/khkwzDuh+0HgPtWQXTDYPxtt+jomy89YD6SWqD36HK8L59VbDjutydw7PZT7GxCOEFeveLjSG7Ntt7bW14+U+zmK1UH8rx78OXJNwqL+dUWQZanKklboLWi2+/+ddsRC1cWGX3xMF7Vw6sN89UXnsUvFahVyjQbDZpLPcYPT4Bn4UYJz596gouXL3N15hJx7wxaekinyFMvfplrV67x/uXXWJy7QXxihNGiTy9MkWnGoGdhhZpMa4Koi18oIbUFTpXZa3O8d+5DlqOEibOP45TrhFmTdpAwNDrBpFugoEPmrl0lIcMSFkop/HKR+tAQS80WczcWSGJFqVjmC59/kURpapUSInkHSAm6Aq3A6QeAScW7pQt4rSVQSIm0bWKVsbCyhNIaLbUpAmgYxi120AK4w5IuGqSw1m/ealvFUARS2P1SD2tdqdt5nWK/iq1sd0aUjT+3sYqJRudZg7uk2c+yMpL12QK1vvlnNJCy87vJVufQ2vo22QKdLx/vZqitgHhP95K4Q4mYlINe6Gen7vfYSgFIrck0xNJnMRI0UknNslAyw7Jcgn4A6LUjukt5l6hTcrCmRpg6fZLf/cY3eOaZZ2isWDRWFSP14ziuR7mwzMhIgcNHnubatTpZHFIu1lld7iHslDc/WOIPXv6QOMn4YHmZY4eGSLoBqhfxzJkBPK1pNBt0Sppf+KWvcOXyDUrFQV5+t8GsmmS1t8BEaYjMKVMfq9BaWWBxcYmx0UEKvs2R6kniKGZ5ZZmgF6AKFoltUa5PIZcjrs+tcPjwYXrNDguLi0wdPYrl5t+7oGshNfitAABVdIh0hqvlevevUook07jlAimKVqdLJjRqrTg15nnFMIzcDgLAnV821idvhx3ECuKWJ9qPf9/9u0FtdxzJx82s8GBcgDfbyrvZ8ju9duvWv92tfy/PiZ1v94Ps9nP3/o2rzL/7GoFSECcJrucBeQJEUMprV3qdmLiToFKFtCWvvfkyzdUOrWaT3/vd38XzPL729a9j2y6vvPIKQ0NDtFpNlFK4rovnurTbHYIg4k9++EccP/M4n//8F/jO975PZWSS2Wabil/n8o0PwVtibHCYi5fnkRX4/p+8Tq02wo2Fa1y9PsfwyDhjE0MUikUmJiZprSxy4sQJ4l4Hz7NoNBZQKqFUKjE4OEpUDBFS0Asi5ucXqA8O0ul0SNMUz/OQQrCyssLpo/0AsJPvGRmkiDhDuxZp1cNtJbfMTew4Fpa0UGmaj429D0fPMIyDb9/GAObVqcylyHTDGA+SgzDlncoUSZLgVTyE6geA/RZAu9kjjmPiZow/5ONXJEvLyzz22GOceeoplufnWVpYoBcFtNttlpeXefbZZ2m32/i+z7Vr1/C9EiurLZ7+xCcoDQySKMknP/VJWklMtOqjCyWcwZgfXZrnSOJSHj7C5Q9eY3X12xw+dATPKzA6OkIUBrSaLZ584iiXL1+m5NnEnkUQBERhhoWF6zj0unG/rqCD67hYlovv+wwODtFqtZibm+MTn/gEtm3z/qVLVMv5/l9e7Gcka3BaEfFwkaTiQitBCHEzANQO0soDQN1v9TOXHMMwbre9/sw72DhP5sbFsm5dtUAgP+aXJeQt7S23r/thIBBIIdcXwzjo1gsM71MmZZ5oofKrghAo8lp2vu/junniw1oLoLWSF0IOVvNu0UrN4/MvvsjRo0dZmpvjzddfZ2RkBNuymZ6e5uTJk6ysrCCl5MqVK1QqFWzbYWJykjiOef/cOY4cOYQQkkOTR3CtIu12xPDkEQJtsRBEhLbH8eNPE4c277xzgXPnPqTVatHttXE9h0ZjlanpacrlMsvLK7iuS6fTZaA2xOTUUa5fX0QKFyldPLfEQHWQpaUlzp07x5tvvkmaprTb+Ry/vU4Lx86D3lYzRal8ZhSr/3nDkn1zKru1INC2cF2XJElMwWPDMLa0Jy2Am94Y9DZ+ZrOXbPix+z0W6V54GD+T8fC6n+frxplMfN/HcTSWlmgBQTEPAEthXhOQvHEMrwhXLl1iaWmJ4eFhnn/+eZaWFllptRgZG6VWq5Gmec09rTWXL1/m0PRRBkolOr0IaVm89945fv7nfo4/+Fd/wMnxUTphwFsfvEel4lKquMwtzjGmq0hKnDpxnAsX3ydJEo4enWawXuXw1Dj1ep1rq8v4vk+j0aBSqVCpDtJtdzhx7DR+wafb6ZLEmtjJGBoaZmRkhGaziWVZfPjhh5x57DHOnJlEiPMkiSDsKWSSEGuNbPSAIdJ+IshaMoht2+j+bCNK6/7Y3Qdj0IlhGPtr+01Qeotlq3/brZ2+z+2JqTtZDrAHcJMfXTs97/b7QB707dvC2mZYQlIoFHBdZ70nICjlXcBipY3tOHSWugCcfvwYQkqKxSIDAwNIKRkaGubY8WMMDQ7SbDbJsox2u81AtUq1WuWVV15hdnaW1157jU67jdMvnzJUcLA6DWRvlaOjNb702Wf5T379/8gv/PzXabdChoemeP75F6hWaxw9eoRi0SMIenS7XVrNJpVqhcmpKQYHBxkaHOSt196iXhtmfHyKOMqIohTb9kiTjG63S7vdZnJyklqtls97rBSTE6X88wb5VHRRFBGFEazkn3etFMxaAOi6Lo7jbLEnDcMwbtp2C6C9RQue0Dboj8aRAnZ13ZFsVf1FI0k3qQkiUBrUbh5y9f0v4bFVy6jSB2/E5M2i2oJ85tTNtnA3ecpbzUe82/XdtvYN+1iydVmZdDd7XFvseCSFUPt47u0wex/I9/cWiTm3na8bv+dr3cR33VIoQAsFIsXWFmUshnwfXyREWUpmOestgNlylzBOsRp5N+ngcJGW5RAjufj+eRwki0uLZFIwffgQJc9ntdHE81y8ssPJ48d57LHH+Rff+AZPP/MMYZjwyqt/TBqsMHt1lsWlBt04JrMsVsPrCGXj2GWaOqLuwdzqPONTI0xPjdJqLHD2sVMMVCs0lpfQWUpzZZnB2gCFSo3pYxYXL88wODSEFjZIB9cv0VpdpuRoPKlwLc1yu0WmNSutDoNjeYZzt+eAXSJWNr00wV3OmzyzagEp84BPyrzr13IctE6xXYm01jLss/zEF+xjSS/DMA6ybQeA1lY3Ob2hVMgeWLtGbfJGbFJVb/1f1v952+5/eLXVTCX5lh2smS02bqvW+g4B4G5L8mx1Dt19iZWN49cstfk7aTTZrtKUdlNYYz+P697d7Dd7WNksAIQ96C6WCo3CUhlFIag4DpZSoDMSyycu9IOeVkyz00Ou5v/f7i3z8vcvc/bsWY5MHyIJI8ZHRhGuQ8H1sG2b8ZERWq0WizfmaXU7jE5O8NRTj6PTiKmxIZ7+C7/C9dlZXn75R2SywMzCEom2iJXkb//tv0OqNQOTw1CSCF/yyeef4ch4HTFRw9IaG8XTTz3Je+++C7hkWnLu/CWCZpvhwSESpdGWTaFcQVv51HYjgzVWGg063Q69WLHaCfDKbY4nAXjQbNmMjR9i9do1UjR2o18MesBHkBeolsJGCgfH9bEcgSXBskGT5Q8dhmEYG2w7APz428jd32juvIbNbyg3//Yg34TvwoF+WN+rjdtZceh78Y67e6ddNTvv6p1256Bv3/Y4joPtuuhAI4Qk6mcAi0xh92KiKKS7nP9doWLx0ksvIa285TMKQyzLotPr4RcLKKXodrvEcUyj0eDcB+9z+PgxyuUylUqFV199lXanw4tf+AK/9mu/xvd/+CaFcoVGO8ArDfDMp57n3AfnWWosUq1UsSxJmqa0Wk2C1WVmLl/i6aefRsq823p4eJj5+XkW5ucpuT7lchm0RloWcZLw4fnzFBxJrVzl2o1Fuq2AwbFJhkt1ojQjS5YAaLUljcYqtYEaWXsFlte6gN2P7K88EJcIAcVicT8OkWEYDyAzFdwBJdb/czAd9LI+pvTFw6PgF7AtCw1IKYnK+bg3txPR6/byjNdOnu3qFDQv//BlbNumWqly6NAhLl+6zOW5a3zimWcoFotorVlYWKBYLHL06DEWFhbodDrUajVOnTrFzMwMly5cZOLIGaanpylVB2j1YgrlGteuXaPT7VAul3jyqadoLl7j2swsA6cOUygWefbZZ1FKMTc3R7lcJojyrunjx08QtNosLS1RqVTQWrO6usqVK1c4fvQQq52Y0cnDNLsBr735Dp0w5plPPU+5lHcBLy0ldNs9jkyMMx+0UEs3xwCu7Rfol+3pD+S0LYtarY6UEpMLbBjG7UwAeEAJKbAQ60GMUgerC0ewoUu4/+ugECKfieXgbJFxN+qDg1j0ZwiRkrjsA3kRaNd1cByXLMyPtnASTp08ycpKg09+6lOsNhrU6nU+NTFGphU3btzIawp6HpcvX8bxPC7NXGHm2jVc16Ver/eLJ2u++93vUqqNoaXN8vIyzSuzXL42S6vTxXItHj99EpUkJGlCo7HCqSPT1AeqtJtNvvvd7/LZF16g2+vxxhtvcOjQITqtNkP1OufPn8e2bRzHwbIsVpstKkWXOA25fG2OQ8dOcGlmjpXVJXwXlNL86NXzHK4NsKwjqtUK3dUmANq1UL6Fm8n1bndLWhQKBaRlMzI8gu04ZEl0oB8oDcPYfyYAPLDyq7UQB69czMGZLWIrN/ed8eArlYrYlk2m8nGxvQ3zABcKRYaGBLNzCwA4nkW1XgYEK8vLdLtdbszPs9JuMjg8xODgIG+++SbHjx9ndnaW0Ylxer2Axx9/nIGBAQqFvJtYa83U5CQ/fOMcw6PjLC0t0YszpLSoDw4yvzhHGIZYWUaxWGJ4eISx8THefPVVpBC88MILRFHEwvwNhoaGeO/d9/D6s3MkScL8/Px6tm9jtcns9Q61wSGe/+znuT6/TC1QFIs9ABrNjLm5RUY9j4s3Zjg+NoQvbYJujC65JBUXf/XmVIrSsvqlYRSlkukCNgxjc9sPAO+YObZfd1qxab+eWOvw22kckkdXu9qMfbNVaY4DG9ysJUXcx6BwLSFok03Qt/2+2b/dc1r0z70ttuDAHtv91N8JQlMsFrBsSaoUWkO4Ng9wJyIMApIkRWhBEmY4vsWf/PBbRG2bz7/4RcqVAaJrs1TrgzieT6vToVavo7XmzJnTJEpRqdcZn5hgaGi4H/hNcf36dQ6dOM7I1FF+8KPX8As+R08cZnh0nA8vXmJsYpgo6FF1BUGrw6UPLuIkCeNj47TaTUqVMsvLyzRWGliW5OzZx0h6IS//4AccPnSI6elpVpur/ZY6iV+pEIQJV67N8f75i1ybvcHpowFwnFZL4kqLlZUVRgou12ZnOTw+ilwNyfoBIM0UEGgB0pJ5IW2tqRRL/bm0hTmvDMO4xQ5aAG+vLZXLBxt//JXl7rswxZbbYJEhRf8JWIPaRuaoQqC0tYvM4f0ZTSM3BAcasDbMhJIc6O5gcYdAKmMvsno/fnvA3uJtlIRkq2O+b7tVblo6CTSIFDN6UQA+Umu0TKgNegiZEkcprvDWp4FzOzFJmtLtdrCkRdxOcXwLrVZZWQa3XOWVN95jYPQIdsEm6HXI4oiZuXnmbszx/Cc/wezMDXSxwo+9+CVmr17jxrXrhJ0u7WaTsaEWh8+cpnHmGMvdFs2VBb79jd/hF37uT3H89BhOnHDjjfeg0WHi0CGK42WmTh5iNXqTa/PXcHwbKRRFx6OxME+z1WRscpS5pRtk87N5cWvbpjgwzOmnP8e/+v3fI505x+yFD8l6Pc6+cBiAqCOh3UI7A3QzgaUlrSjDbvRgqko04CLmY9AaLQWpSEGHlIVmulrHSyDGIRN5ZrWJAw3DgB3VbxFbLLdOFbXVcvc2f/+1P0kEcsP/f/yy/Z/cXamP3bv9Xdc+m9zHbdiJW451f4s/uuzv/rvjFmxyaPcv5DoY59jBJ0EIpICBgQpaZ1jSwrYcwn4NQL+ToLUmjvMpz8LVPOGiPloijkL+4T/8DZYaqwwMjvDmO+f4g299h8tXrlEolhkcGubUqdMcOnQY23H58MJF5hcWKRSKuK6HzjRzl2c4PD7B44+dxpYCy5KcOnGCidFRBso+RUdyZGKcsydPkkUxtVqdpeUGo6PjdDo9lIIL5y+ycGOBlcUVtLSIlSJKU7As/FKZVq/H/PIyP3r9TRqtDq1WmzRJKHouJ44PArCyEDNUq5FEMUmSgJDEaQr96eDSipO3KIv84UtpBUKj0pRapYK1/oBmzjHDMG4yYwANwzh4pASl8F2PwX6XrWVbWIL1FkC/l9DVmjiOyTJB1M57AapDRY4erXJ+dpHZ2VnSl1/m8pUZlhZXqPhFjk5P0Ous8tprb6OkpFarceTwEYJ6hzSImE9SJiYmGS4N8K1/8Xuk1RJf/sIXkZaHlzm88oOXKWQun3niSWZ7CStXr1Oql8EXvPrm69TGK3zwwUVOHD7CxPgUQadLa7XJ0PQE3TBhbmGZoaEhmrPXOXb0GGEquHT5Mq1mEztJCKMIz7Wp1fJdMXe9y+joKFcufohnueBYJEmCaOQBYFK5tRTM2pjcNE3XZ0PZv9ZtwzAeFLsKADe26G23dU9KeUuywL1MHJDIj81KXeuwfNCsZUKufb673Y9a634LmMaSVj6U8i7XubG49d1nB+c1zdijz7vR2hyqe71e49brwp327VbXD60VCImQkoFaDaU1UloI1HoAqJeaJGlKFIWgbcJmXhy5MlRE6wghBM1mk89+4TjC9qhV6xyeHGd1aR6dJPzwB69x+smzDA4N8drrr1GwXY5MTjMyPMLVi5dYvHyNE6fPMHn2SUYeO0wvUVz54AZnn3icdnodR6U4UuF6kvpEHXeoxNTxI5w4fZwsgZLrs7rQpN1aYnFxldlmCxyLWEsuzlzHkhLLLXF55joLqxFnTh7HzTKmh+pYSUR9ML88Ly3GTEwc4tqVS8RRROZYeL6P6LcAJmUHpVQ+/69SpGlKoX+9LZfL2NLOx52qtcHEphXQMIy7CAB32q278TX3sqTJ2ntsNsPGRjqPdO7Zdtw7ot/bk3++LLv7MYmu6+K5Hu1Oe+0ddr91H5kx5C6DSQFS5l1XGwO2vWICv7233eDv9p+9hQZh5edSuVgiTRKE1mgBQSG/bIlGhzRNKJVKJDG0+rXxBoYKTE4OMnbsDDdWOvzBN79JqTLMc598jhNHDnHlwgeQhgiVkZAyPz/PkelD1EoVisUiU2PjHDt6lPN/8Cd0ZheRpxNWbqzQVYLZuTmmjxxiZalDe2UR1xbYrqQ6WGalu0Ir6HLl4gw3ri9S9ouMj00zPDBCFL5K9dA415cW0LJHqTrI9PQ0WiviS9d44skn+Kmf+HEuvfMu85cuUi9JLEuQJJoL569z9iefplQqoeIuWZahsgxW8izhtOrmM4HIvPTRWjAIYFkWUvQHj+hdJMoZhvHQeni6gHf7YLvjzOFdvGbtdXtJb/HnXW7DWqB2N8HfA8M0gny8O51T+7HvhEBIC8uSlMtl4jjGURlR0UJZ+dBlpx3heT4L88sIXESUb5hXtmm1WrTTLlZhANuyOX36MZJYszC/RLVSI+w28R2LselxVtIOYa/H6uoqMtO40qK1tEJzYYnTx07TWW7Q6HV46/JllpebjAzWSHpdKp5LmMUM1EtUBstELvzgRy/zzJlPcOjQUZbm5kkTheP4aCWw3QKPPfEUaM3yygqvvvYax48f48jxk0xOHabdatPrBUgpefKJaaBLc1XhF4oMjo5w+PBhZi6dBzRplkFjbQzgR2cDMQzD+DjbTgLZy4SOvU8WkYCVZ1ZucxFaYm1Irtjekr9OaLHpsl8D+z+SICLyZbdbEMcx7U4bKbeX0b2zbd3813ZpDUoJlBJofXt6xy62pz/XtNB5pvXG47u3SUsPsv1J5LnTfpYSLBRl16FeLEIag6VplfNnVidI0LGi0+4hhE0cpyxfbwLglSWkCSXPIYk6VEoOv/vNb3Bp8QrPfuF5UttFZ5KlK7Oo1RbdxUU+ePcdmr0W569doBm1qQxVKY8PcHHuAt3uMr4OefbENKcn6zRvXMLOEjorK4TdNqNDg4AiCLs8/9nn8QdKLLcajEyM4pd8tM44cuQwwvMIFKx0Ao6eOENtcJS33zvP4PAIrfYK5995m6SxipdoyoV8BpBGU2CNDhEWPAanx0lUglIxUqXopbzFPq24aKUQGiwhkBpEphBSEGYpqdDr86U/6me2YRg3bbsFUMrd3XA3s9VNdnddfGvrsXb4KoUtdvheWqO26NRU9HuUN73CZuxl38vGEjFS3DwuGoXaYv9t1VG8sctcq73tH9rYHZx/frm+f5RW2zjWa13Jt/7/TZsVSLzz2qTeuP9uhscaSDdEy3vRtf7g2uq7vvMySLsLqDVSaKwsY6joU3MdZJigbU2zmK/LbcfEUYYULpb0QUeoTn5g/YrNSLVK6no0ZmbxrIQznzrM+ZUP+ff/5v+NH//0F1l+/zxnyj6DmaIyNk6p6JJVPHBs3HqBd956l4nDNZq6SSdcwJpfYWhoiMOFlCRtkErN4uIiveVVRgaHmHnvPaY/cZZ2t8nSyird1RZxPMyzZ55gdT7i2PFpVsoVKBT5nd/6Bpbl8plPf44bs9dZWlog0z1KXplinFIQDmNDNhDz3pUGv//O6xx79gnGamVSS5OGPaKepDCX5OP6LElkg6c0trRQqYI4RVgWzahH1L/OibWRNyYKNAyD3TajHDg7L+kiEDdbg7a5rL3XR1uz7k/r32bv9PAVFtnb/bp/R+pBdTD2kFIKBAyUKlhKE8cRcRzRdtcCwIgszYiTmDjOl95S3iXqliwsIei1O4wODUOWcnR6kiToMDUxyvLKInEWUR8bZH7pBmGnh6UhS1K6nTavv/oqZBnDw0MgwHZsXNeh3W6RpQkrS0vMXrvG4OAgruvi+x5JkvLBufexhKDo+owNj2DbNiurDRKVEeuMazNXaDaWmZwYRQpFt9NkZKhOwbFw0pjl+eu0eqsUakWK1Tyj+f2LS8wvLHJ1ZoZipYJlWWg0WZqSxSminZe+iUoWWZaR9mcaydIMIQRJktwc3mHG/xmGscHDMwbwPjoowYNAbJl0sduhi48qIYRJELmPtNY3M4CVIo4ThEzo+v0u4FZIlq3VBrSwLIusm6GVRkhBueLSaPfodkNOHTnG6KFphken8PxhwmbI8WefpNBdJl1doeIXKA0MsqRC2kGPiYnDFLSkPjREvV5neHg4D7y0JkkS/GKB2cV5qtUqQT8oO3HqFC074+13z3Hy6Ekq5TJxFDM/O0dtoEqpVObsxBidIOS5Z55iYXaOxvwixeFhCHvYQcDk+BhJt4dXK1As593ZH1xcIo5jVhurFH0f27bzYvdKkWUZbiMgG/CJSzbZch4AZ1lKlmV5jcQkudmCflAuVIZhHAgHrgVQSrnlchDILX8djKurFPIjiyWlufjvkBBi/bwzYwL3n9YKy7KYnJzIgxmlkFIQlzwArGYvDxIFKK1J04Q0Skk6eRd1teZR9goMFEp40qZkSQZLPioNqQ1WKA4UiK2ESMVIpSnYLipKiHsBot+F+v7b72DbNlLK9coFYRgShiG+76N0HoStrDSYuXIZW0imxsbJOgEl20MnKfWRIbxqmcQGoVPiXptec4VucxmdRowN1nji+DEOVaqMVEuUB8sMHx3EL+RR2/uXFsiyjJlrMxQKBUZGR7AsC8/z8qB0Jc98DksWYRiSpvmUcGmWtyCurq6SJsl6lrA5lw3DWHOgWgDvdHE6CK0xW9YsW/vvfd7Erbfv/u+7B81OSpkY90IegB87eow0TZFWXteutzYNXCsEwLYdHKe/aIu4meBWbWxPMTRQx40TEiGouA7tVgffG6TRWKS1cJVC3ORovYalQWaaLIgoWA61coWKdPGBd95+k8HBwfUAcHBwEK9U5MMrl2iuNsmyjLGxUQrk7+PZDkVskm7A8NAQpaE6CYp20OPa1RlsMoI44MyJo0SdgKPTE7xx5RKTlQrStYgdD+HlQV23p+iFCUJAp9vGcRxUppD9LGghBLLRQwFJ1SXLMrIsQ0pBlmZYjkO318tnBjEMw7jNgQoA74u9uLdv7F/dbH130/+6lw/s97u0xy3bYDqlja0JS2C7NidOnCBN0zy5QUNYyANApz/2LcsysjQlTVMcBNFqTPlQAa9qk11KkAg8x+HD997j8BNP8u7l60SZ4MT0GNFKj3bQxRMupWqRoufhV0oIrUmiiFZzlYGBKkLkDwG2bdPr9Wi08lY1KQVJmqCFoFKuUD02xbuvvoUrLXzHpTY4yKHTJ0kt6ARdJIruapOkWubNV15janSMV37wfcaqJUbLPpeaCyhfkGZLACytxPR6PRCCbreLkJJ2u513c+d7aX02kLRyMwDUSNIsxXIcOt1OXlT7YHSgGIZxgNx1FvC9LOq83W3YaPuZxAJw7nqb8jcFiwwhsrWNQG0IbjK9my5YxV4GSDYS3d8GzYYZMPb0XT6OxfqdSGtuzk+18+zSm+vbzG7Xt7mNmaz3ohi18VG2l1KsCIbHy6Qz1/EyEKlNVMy7gN2VCAsbSytcoXHIiJKUqJWXT7EqEum7xGHE0OAgaWpz8b0LpEFCY6XBik548uwpwuVVuq0AEXUYHqjQjkKCqMvkseO0W4uoyKLXblAeqKJlRio12IqBgsNq2GXq5BSlkRLzzRVuvL1EwXdxhItVtKkND+AXXXBtChWf5eUKSdbGtWwcYgppRG91heJAmRtxiFUsUpMWNasFwIWZNitJSqohTjNipahVq4SdNq6w0ZlALa0FgD5JDFKA5VkESiH8DPyY1OmRqXw6OKHB9AIbhgG7rAO4N7X7duZO77+77dmq1tnOlzwXOK8reLO24FrlNLHh/7a77G3WpSCvE2j1l3tX2e3OW7Fer3E9ENz4592ub3/q1ZkagftL65SzZ44xUHJRWZyXNkkhLOVFj/1WgiMsPMvGd21818LzXcJWPvbNrzkoKbA8l1Rr4ihjpDbC2EANX2U8deo0BbtAvT6EdCRRFKDimJlLl3j1lR+xsLzA7LUrLM7fwBIQRgGKjFQnKDI6rSYrK0ukZCRkXLpyiQsfnCOOA5RQhFFAFPRIoxDQBK0W6IwsiVlZWuDZp54gCwOGSiVGhofoiIzZ+Xk6yy0sOgCcu9QgQZKhidKEKOjh2g62yK8zWgtYS/yo+Wgt+oskVYog7DAxOYiwFaw9nJrBwIZh9JkuYMMwDpyyV+BrL36epBtgI8myjERokv40cIVuRmLbeJ5HkiR5UkQmSftJINpNCYIezSDCr1TptTsszs+TZRknjh4j7oW0G02qBR+JoOQXkRrGhoap64x6pcrI8DBNpZhfmEe6DhOHp6E//k5pzdjYGFEU0263qQ3UmJm9RhLH4CjCbpeFuTkUGr9SotVu01lcpiwdYmmTRQnjExOEjRYXrlwiUAGDg3UKwmdiIm8Zf+/8PFrlNUTDMKTX6eJ5HlE/I1nAehewqnk3d55SkGS0G01OnzqNZzmkcZo3uJv4zzCMvj0JADd2jx0EB2EA/62zXRyM/XKQ3Lp/xAO1hw7C+fWw0TovymlJC0tK6p7Hc088TXexQVXapFrTLuT7XSYKL9Fo28ayLBzHwfM8siBFh/nPxPSQUpKlGVOTUyid0nZdJiYnOfXYY8xfv069XEGkKY7rU7Ad4iRjqDpAiuba5atcuXSZernE6MgofqWEVyzSDQOWlpYQYYxXKHDl6hW0Y1EoF/n8iy8StkNWF5fRAhzbIYpiUhSO57I0c5WVhQVKrk/cDTh97DhxN8CzCkTLDWqVYQgzBmoS0Jz7cCkfKiEFcRTRarXwfZ+e7eTt3ELeDACrHqlWuFrnfRIKkm7A9PQUA8USvW4zL230QH3TDMO4l+56aPBBK9VyEMp3CCE2lGExj9yb2biPxD53Rt+Ng3B+PYyEEFjSIk1TtFY89/iTHKrU8DOw+1P2dYr5mE+3HeHYNq7rrmcAu65LqVjEpwiAchJ6vR6lUjGvydcLCdtdZKq4cu4DSDJkphGpQqSK1nIDV1ioKKG5vILMFNOTU9TrdSzbIggjMqUIwhApJUmS4nkeR44cYXh4mCiOWV5awrVtkl5AGkQE7S5Rp0truUFzcRk3Voz6FYJGk26zxfz8PFoK3GKBOE5I4hjHTbFtTZZpZmZb+fRtQpBmGb1ej0KxQKFYwPNcHNfB6qWQ5i2GcdlCKYUtLWwNYbvD5OAQj586hSMknmWbXBDDMNaZLmDDMO67tcLbxWIRgeZP/+zPIaKMiuMjwoSOZ3HlsxP5z0qBXfIBqFQqJEnSL9QssJJ+mZiyZGVlEatYJggCKsUSUilcyyaJE4oDNTzbodVs4VsOKk3RaUoaRwyUK+g0Y6A6wOrKIs1OG6vgg5MH/YVCgYK2SJIE27IJgh4D1SqxylC9jJLnY7kuOkmxEIzUh+gFXaSSlMsDxK0OnswLOlfrAyw1G0xMjFPQDiUnn993fjEgSrNbcuWllBQLRbJqFRmGKEsgLYuoGaKHigTDRYpXuvnoWKXptTtIDV/98lf44SvvkMb6jsXiDcN4tGz/gVCLnS8G0E/p0Jsv3Glhq327t9v2gDS+7dDOpzMTcOfjcdsiuHPtyl1v95bfqT1+q03d8YTc0/fJf+X58lrnwwAylXD28ZM8++Tj0O7gWZK0ZPPNv/wYC0+MAhAMePzJ/+YJrKKHX/BwHBvbkvgFn4IsAeAWbeabS5SrJbqdVXzPwpbQbCwjyXAsgWNLgrBLlEXEWUw36FKulAGF5zq0200cx2F0dJTxkRFKhSKdZgutFEhBs9XMy8D0x+MlYYRtW0RhQBwGFFyH1tIyRBF2mqHCmPbKKp1mG0tIkihi4cY8vW6XxfkF2s0W5VI+hvHy1Wa+Z0Q/yUzkreSeV6BQKOE4Ho7jcO3Hn6VZrwHwyr//s7z34lniNEYLjSMl7flFXnrh8wzVBrBcCyEfyi+7YRi7sIMeAXsbi4OUXn9xkdLadEaP/ew6OwgzizhC4m6yOMg77E2Jra1Nlrvf7jwruN9FLfOZQu5rF764dQaTPVnhlnt26/XbCpwdLtaGLuE9Ob+0DdrZZNmq5M29kAHpJsveldbR/fdRMkEhUNpGIxFOytd+8jkG6VFzNK6neeMTZboDHnoteBHQmChy49NjpGmEZwsKlkSphETYoPJ9vypjBsdqjI96WE6MW4QwbeGVBIurs8wuXCIUAR0/xRopEnopuiiIRUQv6xJmMZbnYrsulrQgTpmoDzMxNEqz16E6VEcJsIRkZWGJguWwtDiPVZTEaRdXZIwUPOx2G6/VJUtielFIpVJhoFRGhQlly0V3I5Zm5xCpwvXy+oYXrzSIhQbbReBgOz5RlIG2SROBygQLpyZ56y9+hVjkHTmuVLz+yy9w8cwYaVEg0ojmhUscGRzh5LFDhFZCKkxRaMMwcjvoAv74oO2jcZ3Y9GX7NXD+IMwsIu643/QW/3qH7b7L7YHbkhjW/+5+Jszc3Ii92Yat9t/W697NI8l6Od49O8fvtBX73HKzD2+XN2rm+8lC4Dku40NVvv7jXyHrhRSkQFuSTtlGaM1MMIBGMOZ2KJLSLlmUk7g/bZzEtWxsx4FEgRehnYhOt0un02J5eRHb8bAdi17Qo1Qq5fMH6wzLtZGOhSNcllaWKJXyVsRCuUShVCaOYzIUSmuuXbuGlJJyfQDX80AI4jjGdRyaq6tUqxXiIJ+nuLGyTMHxyMKIoleg2WyitKZUKhFHMaVikU67Q6fVyuczlpLqQL7jL1xtoOl/V/u9AUJKXNfDkhYZsPzMcUSaEdk2XW2TASLNuHpqgqNz7+SPQV6Il2b8+I//ON956y20Vh9zTTIM41FhxgQbB4NJqHjkaADZP/Ra40ubLzz/GaYHR9FJiuvms1v4S10UghtxmdmoSqwttIRSI0SrvDC3JSWu6/YDwP44wGL+b37Bp1av56VkkgTbtul0OjQajQ0BFsRxnE831+uRJAl+qYhb9LE8FyUl2pKkWmF5DuVKmSNHjqyPWwzDkDiOCYOQJEmpVirYtkMURdi2g0bnn8XPxy4Wi0Vs26bb7ZKkKYVCEaU1lYF8Wy5cbeR/2PC1kP3PKPtlYCyVV3W+xCCvi2kWqIAQyCQljSKyKCYOQsJewOc+8zkqpdKBSNQzDONguC9XA3Fbt9n9cqfu4f3qNpZb/Lpf7kdXvRCSQqEAmLIqj5T+sExNPh62ZDt86TMv0L2xhI0kTVMsy+LMmyukiymptrBQVGTEyNU2k28uYNkWQkpsx8FzXVzHQcV5d7lfknz44Yesrq6C1utTpTWbeUkUpRQrjQa9oEe326VWq+F5HsVikSRJaHbaRCqlEwWkQpOgOHr6JGNTkyilmJ2dRdo2mVIMDw8jhGBkZIR6vYZf8PNMXcchCHq0W21UpsiyjCDIS7eMjIwwOjqa1/aLInzPoVTOd83V2TZCQJZmKK3RmSLshSiliOMIpRSTP/gAoXV/Vh3I5yPXHH75A8J2F5Eqok6P7tIKhyanGKnV0cp8vwzDyN23APB+z6qwk5lF7tW23vH97kM3zf2a8WKtdWTtz8YjYkNuji0kZdvj2dNPQLOHzBRKKYQQeErg/ChvEZtsr/L8t2b5/DcuYSOxLAvXcSmXy9TqNSzbRoX5yJaBIR8p83M5TTMKhQKlUmm9ZbFQKDA0OMjq6iqXLl0iim7OL1wql3F8j0a3w8SxI0Qq5cr1a1ydmyVIImauXePKlSvUBwaQQtBsNrEsi2armbc6+oW8NRKwbYd2u4Xt2Ni2TbVaRWtNFEWUy2Vc180XL59fOAgybiy213eT1hql1HqLpevkgeXAXIMX/6vfwV/tASAzxfP/+W9RvXwDUoWlwFKapBdQLVWYGp9Amu5fwzD6dpAFvItlO6/bbP0Pqn3bdrH747GVe530eae3VpogCjZsxoN8Ejwk9uq8+pi3WOv6l8DZE6epuwWcRGHpWx+BrhyZBODZ1y4z9eYCWZSQJAlxFOUBl+fhuh5+ocjSXBOA+nAe7PV6PYKgR5ZlFItFKpXKeitfr9ejXq8jpSQIAobHxhgcGgKtWVpZJohCgjjC8T3GpyYZm5zA9jx83yeOY2ZnZ2k2mxSLxXxcoeth23ltwjiOCYKAOIkZGx/nyJEj6+VuXNcliiK63S5ZlmLbNsViPo3dwkK4/h3IW8Q1SivmFxewLItSuUyhUMCyLIbOzfDi/+t/BkBkisE3L6HSDEuD1GBrQbuxims7TI1PmvDPMIx1204CsTZcOrbbTaeQ/Vond6DJCwGLtXXnUx8dVFvPAiEAa4ebrslnaN/kfW77y1uCIi22yjxAbrEBaosr/1qJmk23Ya13aT8SAnQ+N7Dszwqy9pYaRT6H1V5Zm0N4MyZDMpfPRLE9+tY/7uRc0RZoiRAKSwQ88dgEaes6RUKkyuv9CZURWXB9cgyAU5euk7R7kCnCTocsSZGIfJYzZVMsDfLBh99n/FMT1EdK6OIAqqVxHZ9CtUS73WFkYpy569dJ0xT+/+z9SZAkWXrnif3ee7rZ7rvHvuVSuVRmVgK1otAFoGUKzUaD0+RMd88IhcMLmxyyKRS2zMiIUGR4IA88UyjCwxz60iNDETaFw2my2TswBTQKVQAql8olMjMiMvbFw1fbF13eezyoqS0ebh7uHh6RkRn6S9EMczNT1aeqZs8+/Zb/J2ChPM/Zs2cplcu0m03CwQBrLZVyhfJ8jc21NTqdDuVyGSkl3XabylyN6vwcW5ub9Pp9LtWqbG9t4kqFW52n3u2lhl3BZ9Dt87CxQ8XE9ExM1S9Q73dwHAev6NMK+7T7XQrDBMBe32Vhbolmu4lIYpSVaFzub3ewQY3YKoR0gDRnsdjoAmB8l6jo4+kILUO07SOspNveRuo+r79yEUcpoiRJpWvyvNucnBeaAxuATibPYW2ak3KAdcwBJD3EhC1jbfaD/3wagJOhUWvthAGYTaSHleoYGoCz9sXEvqbOyR7n1YLAzDQA93PWSPZ+8VldBSEEDl46FjF9Do1Nhp+JY9kT+zu9v+4u6OPi8ZqJY456vkR6V6IlghjfTTi55EG4jUeC1AaBQhrD9RPzGCVZqLeY32pCkqRafLGmGARIIXF9j1JtkSgSfP75LX7CSRZXy5x8/W28epeBbSNdh8gkrO9s4ZcKeMPvb7vdHuWgDqIIKQTdbpfKwhxKCMqlEoHnI5UCa1EyLcLQxlCeq+EUAjZ2tilWK1T8ArVCOQ3XSolTdKjMzxFGEUmS4AqLcRWlSon19XWSdpNrt25Smaswt+AACQ83QurdFpGOEVajrEDrgIc7PQjmwC3gej2isIdUEg+J1+4TVQpECxXs1gaxDUmsj6c8BBFxp84rL53HdSSxPq5q+5ycnK8zBzYAxx6Z6b9nMUvg5HFrfX15Vsf7eImT3e/Yby+HF0x5GjzLUTwfR/z88oy9QjY1yQu+z4mlZZIwQnliqoXi56dPAPDyzQdorRHDYg7XdUf5cdYYCrUqH9++wbWrd4EfUZnzoFTASSyODlnfSEOo0WBAMlHslG0njiKsMQziGN/38X0fISRxEmNMehNSKBRIkmSUnzgYDEg6HZRSOI5DHMc0wgYAnU4HY9LCD99PhZuz0PHGxgZLS0uUy2USk/BgY41SMc2DbTZBSZkaadZiEGgJD7e22NjeRgLRUIAaC0mSENQ7RJUCg4UybG3gCEngeZQKJXrKoVdvMD9XI/A9emH8DC9wTk7O88rTKwLZWwLwAKt9fcISeQglJ+foZB0ulBUEymW+XMVGCdIwDAekfu/PT6UdQC7duEeSJGitSZIkLRDxfarVKpVKBa9c5KNrn7PxMM0BnFsocPnqF3xx5yZWCtbX16nVaiwtLQGM8gGziuAwDPF8n0KhMGxJJ9Baj4w9SAtEBoMBxtpUD7BcplqtcurUKXq9HsYY+oM+QghqtRqe5xEEAa1Wi0ajQRRFNBqNkYGZJAlz83OcPn2acjk1Mrs9lyBIc/ysFGgBGkuj16avYwrVEtJ3QQriOKbb7eJvp0Ujg4UKjlAUPZ9qoUStWMZXDnoQcnJphaX5ha+8AC8nJ+f54Ei9gA8ih2IFaHv40J2QIs0JPCayO/enweyQ8PEzkoYRkBzhvL5oTHYUMfn5ei4RgHI9vFhTLgRIbZBGoLRFxwk4irVCka1KCaU1Z67fIQxDpDEkcYzWmm63S7VaxQ8CuknELz56n7X1tGLYcSTGM1y9eYcebZaXlymVyziuS7VaHWn3xXGMlJK5uTkq1SqNeh1rLYnWGFJdwOXVVQb9/khKJkoSBr0eZ8+eHWn7Afi+D1YShiFJkhDHqbft3LlzFAoFPM/j8uXL9Ho9FhYWqFQqPNx8yPxiiSDoALC0fIk333iTW3/xi3QeBbTViCTk7voap08vYhoObuKhlMRxHIJ6uu5goQzG4FhFJSgQKBffQHNzmxPnLrC4uMSttfWhnEyUG4I5OS8wh7a0nqZkihDj/qrHsTxNvqp9wTe0de9TIP9xe94RmDj1+J0/cRpPKJRJq1eziPyvlxYAOHt/Ay9Oq2QzI0xrTbVaRSlFpVLhixtfcu3ebbCKbjsC4I13X+G3f/rXWVheQmvNw7U1mo1G6sUzBtd18X0/7c4RRWxubIz0B6VIPWylUolBr0c8zONbWF6m3+0SBAHGGAaDAYPBANd1kUqlnUE8j0RrwjCkWq0SBAFSSjqdzsjwevjwIV9++SX37t4D2wKg14NGs8urr7zC8vJSOkOnbYCxQvBXH76HWwzwCgW0McRxTL/fx99KvZ6DhQpSCGrFMgvVOVwpEdpCnFBwXJYWFtLiF0CpZ9leMCcn53njaK62A0lDiIO/d2o5osTJ1zaFa5/j3Y89zveoaOQQmzkQX9Pz/cwS3Z/lufkGff6llGAMUlgunD2DqxQSO6wDT/loOTUAX7p5D0i97LFJUu+cNvheQKFYBsflF+/9ima3jUDQ3BkAcO3mZ9zfeEiiDdaAkopCoYDjpJp8UkriOEYpRTz0Kvq+j1KKZGjcFYvFkVFngXgwoNVqERSLqaEoU49fHMcoKUc5gnO1Gqurq6N9lcsVlFIUCkU8z6der7OzU0+rgZ1UBqndEszPz/PDH/yA3/3tn+BINS7VF4Ivb1zHK5Xo9FOpGCklQRBQGFYCDxbKlMsVFheWwIAjFMIYSDSuUpw5dxbHcdBa54UgOTkvOIcoAvGHj3ZXpM54v7W4In3/btLE7f1qUg9nl1phMHLvMN9kuPpphmn38wTODkMLZl8CzawKYXdGdbVBoO0sW+Dw1dVKjJPx7bD6e3JrzwKBg9izutqCSJh5tEcK+87yiFjS67HHGjN2YwUkE5+9J09FEGDdGS/N/qw8v1iMjXA8iWs1r790Fin62IKl7SepF9D4XF6YB+Dc1RvEcULfxmyLNiWhqJgA2XfR5QWu9AX/8r0PCfyAs6fPYIft4OZLLspUaO5oFhfPsLK6RJz0gZhut0OhEOB5HkKkhle32yWKIiqVCkk0oFKpjLyEWmtq1SqtdpszZ84QeB5xHOMFAZ7rUiqV6LY6uJ6HMYatra00PBsELC8v4/slHNXHUQGdTki5VKLRaCCkQ62afs+aXcHD9TVOuC4/ffNdbn/xBb++foUwjsEkNFuK9c0mjreIEc3htQd36AEMFyosrqxSmF9ERxKjAWEJ+23ifouV5SV83x+1vcvJyXlxObClJYREiMw4e/wiEEibPtq9CDt7rVmt0Wb9t1/RyFcZEj7YPjNZkt3LbBkOscf5lEPvnwAsEsTu5fDHLvbc13h0z4bsPMq9F45yzmfva/a1ONwaEhBWjMZ3PBx+fM89AqzQ+K7i3OkTCJuANGiZamNeXZhn4Cgq/QGLD9ZT718cozFIqXDdAl6hxPLZC/zxr95no93i5OoJfu8nv4srSgBU5zy69Sa16jyDQUy71aHd7mK0YW5uDsdJjf5ur0dnWM1bqVTwPA/XcXEch42NDQaDATs7OwwGA3zPY25uDmMMSZIwGAzodbs0Wy0skAz7DWfhacdxaLc7NBpNhFAsLCxRrdRQykUpl1arjaNSD2A/9JhfWGDjwTovnbnA3/79P2CuVEZYg6OgF/W5fvMWc8urmOH56Ha7FIfdQMKFCouLSyAVVshxMMZoMJpioZD2Ec7Dvzk5Lzx5Z/CcnJyvDm05ubTE6vIqQtu0fZlNi3g+Xk6rdV++cx+bpB7OJEpwY4UjPVSxiLewQE9IfvaLX/LSpVf4m/+Dv4nnebSbaQ6gW7BIJVlYWGBxcRGAZrOJHubnpaHZMuVSiZ2dHfr9Po7j0Ol0QEC1WmVhYYF+v09QKo0MrjBM+/Faa/Fdl52dHRylKBbTCuKsIrjb7SKEQClJr9fl1q1bWGs5ceIESZJQq9Xo9wcUgjQv7+qX20SRoVKZ587te7z12pt87+13R17/fhxy88FdinNVomGBSRAElFupARkXfdz52szTXSwWMcbkBmBOTs7RDMAnLbr4qnrOflUFIzk5+WdtD4xFasO3Lr6MKx1cJMqCi8BqPSoA+dbdNeI4YjAYoMMYNQC0RBbLiFqN965coR6GfOv1N/nZn/5J6skzaWVuoeIwV5sDYGFxgWKxyPLyMpVKBWM0/f5gZKxloV5jDJ1Oh0F/QLOZhlYXFhZYmJB1McbQ6/VSDb4gIAgCHMeh3mgQxzFBEKSyMsOWb3EcE0cxc3NzuMNwsVIKrTWXLl6kXEoN3PqO4OoXN/iX/+rfcuWL66zfXeNv//7f5OKZc6luoePw6ZdX8BbnKRSLAMRxzGCrjtNLexm3KwFCSixDjURr0MaQRBGVcjkVsdb6QGoOOTk531yOvQr4oNvIRFiftQEoJwRgv4ox5Lx47P7c5Z+3lGHzRC6cOYOKYjwhUQYcJJ2gwM1qBYA31jaH1bZ9dJTgRZJSUMX6BWylwr/55S84ff5lVk6c4ttvvkmtVoWhAZiIHgz1AjvtDtVqFcdxCMNwKMzsj0K5hUJhZJilPXdLhGFIFMeEQ7mYOI5JkmTkKcz0CLXWNBoNXNfDcV2EECPDMMsxLBaLFAoFwjDk5s2baQ/jIGBz8xZKgtZw8tQbhKFGWA/fLRF1I1Yqc/zwN76LIyWx1tzf3qAT9ZlfXBiNQWs9CgO3ykH6GRvmWmdi2TqOKZfLI8Mz/xzm5LzY5LeAOTk5z5w0o1HgKZezJ09jomQc/rVweTUVfz7faLKg05y/OE5AW1ytKPplgtoc6/0etzc2+OFPfpdr129y5sxZtra22V5PhZHdAvSH+n1CCD788EOM1iMplkqlysmTJ6lU0grdNFybVgpbY1lYWCAKQ4rDVnGQFpZVKhXKc3PUajWCIGB+fj4VfnYcomF4WAgxMih7vR5bW1tIKTlx4gRnz55lfn6eSqXCqZNlAHp9j4drW3Q6fRzlsbp6mqg7oOR4/N3/8X/I299+Gy0sjW6H63dvkRg9Ckdba6l00srnRmF2eLdUKqUi10Px65ycnBeXp2gAChCKUbK6FRPL8e9LWIEwh1ueb+kMAVbOWGavI0l/QKUdKkcMl+nzv3s59MieE2acn2MfodhnycmwVmDNHsuen7m0iKfguiwvzJEkPYS0afGCdflgIc3Xe2djB0e5JIkhSQxGa6SN0CamurTC1Rv3+I3v/jZbD+uszC8yVyryrZcuEXVS48YtWh48eAjGMuj1OXPqFMJaPMdFCUkSRdTr9bS4w/eRMhVWdl2XTrtNv9ulXCxS397Bao2OE5SUaK0Jhl1DBoMBnueleXUCypUyxaFETCY2DQLpKoJSgc3tTfr9Hr7ngk64cC7N2UuSEu+89SanT56kWq3Rbne49+AeH/76AwpYfvd73yMQAoTh+oPblE+sYoOAMNGE/QHBTmr0bgcSKy1WWpAGgcGRAonF9zzcoYcyrwLOyXmxObAMjDygpMU4rJBVLabYKbEyzaSsxqxclIPKtsjRD//BscKgxfGN4fjZTw4nZi+rTTLdAWOyE4tGzrDzZkucPLL9bNsChDHPgf08w9NhDQc9pseTfZ5nfVVmS9G8aFiGxt4uBHJGg2pLyS9QKjpEcR0ROAivyEAEfDxs1/bt9QaDgSYcJEShQYcDFhY05eUyqjzHn//VZ1SXTpE0+pxYKXF+eZFep4/jV4AdCmXFSy+9TL/bo9nYxnEFxZJHFPeYm6uwtbGBkQ7z8/PIoWEH4DgOBc9HWoGNEmrlCnEUoaxl0O7iFgOifp9WKxVw9n0fay3RIKRU9Eb5gdbaNEyrE0ILX96+TrfV5uTSCjaKWVlaouDdB6DfMTy4fZVTJxZxnIBStUyZGlutdR5cvcKPX3qVP1pc5tr2Qz6+c5W/9t03aHsBkZWoSJPcvA8/fJV6ycG6FlyDiA0CTdFXuEJjkpgoikbHmZOT8+JyCBmYx+f4PT43cI8fhyfNJzzif8c5huPn8B6nTLZldHx2co0n92B99edkN8/SK5d7/w7Gfufp0eeEBVe5FItFJtWsb1dLNH0PP9GcX9tga3OLTqdDs9lK8+uCAm6pwnsff8zN27fY3t6gFLgoYanvbJMkCd3mgOy+zSvCw7WH3Lp1i36vh6MUJ0+cxPe8tBdvrTbSxcu6emQh46xaOA5DPNdFa02lkuYmWimx1iKkTFvAkYZY2+122rJumO9praVYKBCGESBRyiOONe+/9yH37jzA9YbFG+20oMPzfBKd8PDhGguLS1gruXXzNsIKfusHv4USis8+/wLpBThBgcRKhOPjbqUewGbBG55NS2aPi6+pWHhOTs7T44lDwIf3juU/njk5OYy6WBSLpannP16aA+DNrTr19Q2uX79Ot9ulVqvy5tvvcP6tdymsnOBf/+mfUJmr4rhQCCDst/H9tPjDdX3iXjrPFGsuURTxxhtvcP7CBYy1aQjWDzixeoIwiuj3+xQKhVFnkMxzp5RKw7uehzYGKSWDwYB6vY5JEnzfp1gqjUK91WqVoFBgdXV1lFMYRdFQ+F7x+WdX2d5qoKTHG2+8hRAuQSGVc8FWqNVq1Os7OI5DFMV0Oz2KxSpnzlzAdQq88fpbLMwtsba2SbMzwC9W0cIhMqA2UwOwUXhUMHws3v9VRTRycnKeN45kAE5WNGYVc9lkOYsnrRY+zHKc7B7DUfiqKkAzsWwlZG5y5zx1Di3MbSHwA4JCYSgyL9DG8NFimhN36dYd7t69y87ODmZYMVsoV5m7+Ar3dpo0+wN+76d/nZWVeU6fmOfs6ROjUOznn3/GzkYHgJv3vuD27dusra3RqNexJq2K3dnZJk5iisUipVJqhJZKJXq9HmEYEoYhQggKhQKe66bi0MN/K5Uq/V4vDQEbgzGGIAhSWRjfZ2FlZdRaLqsQ9twCb7z+Du+8/Zt0OyFKBviej+enGoDbWxFKKYw2aXs4z6PV6rJ2fwOTSKT0+e53f8Qbr7+NkB5/9eHH1JZWiQw0eyF2Iw1HN4LpdAWtddr7WEAYxaPjyg3BnJwXmyeWgfE8Lw2DCEG5XD7Aekff10GW4+ZJt/08aB7m5DwLDvu9FEKwuLiIsHY0LwyU4upiFYDTn12l2WwihMB1nFRYeRCysdPi3733IT1t+LNf/pxmcxNXJZQKLlEUUiqXOHv27Kgd3MJKidXVVZIkGXr3DPV6g2azmc5ZQ108Ywzb29ssLS0Rx/HIG5gkCY5S9AYD4mEFsSX1EFYqFTY2NoiH3T8azQYAa3fv0mw2aTabKKVYWVmh2x3Q6fQJgjKFoMLZMxdQwx7AUSTodGI8z+P0mTN89sVnJEnC9nadQrHCYJDQafe5d+cBb7/9GzhOwHu//piVU2dxC2VQHtG9bQC6vkMsp8+5NakPUBs9mq/zuSEn58Xm4AbgjAb0aXjDTCVQ7/f+5y4PZb9xPs/jPgr7Hdc39ZhfCIY/5F/5NTzcl0lIwclTJ9BGj9b/bGkeLSVLnR7cuEWz2URKiR8EqXyJVCSOxx/9/OeE2uAHAZVykatXPiWOB8RxTK/XY6deJ+yk++lEdU6cOMFbb71FpVJhZ6eO53nUajXCQUir1aJSqYw8eO12G8dxEELgOA5SSozW+K6LkhLP93Adh2gYOi6Xy5RKJaIowlEOy6urI3kYKSXdbpf19XUaO03u310btaP7xS/+kjDaAKDfVYRhSKPRYDDoUx5u7403vs383BLtTo/bt+9y+859Ou0ef+23f4dOp48WkrPnXyIoVoh2uogwDSc3C/7ENWGUC5jo1AB8GtGSnJycrxcHrgJ2Ju4WrRlOKoAVAuk4qRp9mOpQZUUW0u6d7WeExLB3Y/s0LPFkv1pCTK6vR9ubDnkM5VQOcxNsQYjZVZ8HCakc9K57/205sOfrBsTeFdpKyHEdsB3nBFkExqphlvgjo0j3s8eQhZQ8yXl4qohMEOcx45g4D09hCKgZH2W5R6XscDjsp04087zOlL6xIJ5ltaeZMY49EGBtwnzNwyZdXGlQCD5Zmgfg0p0H3N1pUI80JJKV6hK/8Zs/wF9Y4L/6039DvRfy7g9fp+AVePNbF1DmNI4ynDp3nqXl02xs/Vuk9YCYU+dX2dqSaBPiB26am4ci8Cv0B32ESI2hdrs9yvErlUp0+j28wEfiYaTASBCOYn1rC6+QijzroUdQa512/Wj3uX9nDSEEC/MrtFot6vUd4qjP6uIiL52/yNnTJ9h6eJ9b927wmz84B8BOwxJph1qlwpfXb/Pd73yb3iDk4dZDPrx8mYoXcHr1JMVikQf37zO/MMen9SYffvEFZ2plup0mpUCgNhskZ5ZpBWVWZI+YkEQmhCJAi4B6oz3KcfzKv6c5OTlfKQe+BVTIdLFipDGX/WtNGlJQUk1V2O7WoksXgbQCgUKI6SWV9XiyRaCQwplY9su/k4dahBDIGXmJB+WgeY37G4qZxM5ey97vl+mZQSHJzIXpKuG9tjVrDOK57qoihscrhdx3edpjzfQYdy9q4lpMLg5yZIwc/LxmV3Gv6/es+72aQyypFNTifBlhI1wpUMAnJ1L5l9PX79GONNYN0NIjwaGyeIJmP+Jf/MmfcPbSS1w4exFf+nz60WckGvpRwvsffMhHv/41mzt1Pv3oOgBWhcRJSLfbptls0uv1KZdrzM0tcfbcRRYWF+l0OrRaLcrlMoVCASEEiTFoIIwjEmvohyHtXpellWWMtTTbbZIkQSk1EptuNpqsr28xGMQ0mx1KpSquW6BQKOEqwcbD+1y9chnHFbz73Xdw/DRP8d69PlEi6PVDFhfnsSak1djA9R1OXjiPLfi0k4hEwN/4vZ/y+oVXaLQ7/NnPf86Zc2cJSgFCGJzNOgDNQoAQCoPGSgOui5EB/UEascn6GOfk5Ly4HFwGhmlz4KA/nXsLZzxOVmM/iY/DLIcZ2ZNu71lx+LHtJ8rxfB/r15dn++l6jq7hIQ5WScmpU6eQUuI6DjvVCg8rJaQxzH9ylXigkcJFOB4hgk6S8O/+6lesPdzEcwP++I//lL/8i/cYDCK2t+pI4XHrxj0+/eRzHjxYo92I0h25A65evUKj0WBubo4LFy4wPz/P1tYWN2/cIBpKtmQt03q9Hv1+HyUlcpgrl+X4AXS6XYpBgDOUf3FdF9d1aTabuK5HFEfUajXOnDkzajPneR6DMKTT6YzkZTY3N6lWUo/955+vcfXq1VHeoRCCkydP8tJLL1GpVKhUKpw8eZJer8e1a9dYWFjglUuvcvnKVfrGoioV2oMIZyQFM46wZDI1ylF5WkdOTs6Ip5YE8hw4gvZF8PyPMedFIw252xnLN4HsOISUKMdh5cQJtNa4rsunJ1cBOH13nc7dDcJuQhxZtHAwns96t8u//sXP0UYQ+GVKxSrgohNJrbbExmadBw92CAeG1157ncBN9foq8w7KEayvr/Pw4UM2Nze5fj31DiZxgrGGfr/P2bNn03xD36fb7aK1Jo7jYSePdMye5xEEQVpVawztToedej31GCYJFkvgB8RxjO/77OzsEIYh9Xod3/M4deoUSim63S5zczXm59MpuN6wrKysjApVisUiURSxvbVFo9EgjmNu3LhBo9EgiiKuf3md5cVVmq0eV+8/YPnSS8hiGX1vK91eoEaVvlnPYcdxiKPoufHU5+TkfLUcqwGY9dFUSh0pwfhpy75MytZIpZDy6GPNyXkaGGtGIbrJ5ZtiAAKj4grP86jNzREnCVIpPhkagK/cekC/3cexLp5XpDK3xMq5i3xw9Rqf3rjJysppXnnpdRbmV3ntW28RRYb33/uIJFKUC4u8/94nXLt2HR2lRo5bMJw/d3akyddsNlNx5zim2WxitKZYLNLpdEiSZNTaLU7iUY6f47oYnfbe7Q69eABYSzgYIKWkXC4jEChHYYxhbW0NrTUnT6a5e57ncefOHfr9Pv1+n253C9e1qUagqDI3N8f29jb1Rp0PP/yQq1ev8hd/8Rd0Oh2MMURRRJIkSClZWV7h3W//BqGGf/vLX7J86RKRdLAPGwC0St5IzNpaS1Ao4Hk+vW53NAd+kz5TOTk5h+fARSA5OTk5x0mlUsZTCtdxCGPNJ4tpAcjpy9dZ74aIRFAsVCgvLuNVqvzTf/JPiJXH26+8we3b95gvnaJWrNFv73D5sy9IEsFPf/qHXLv2Oasn57j06ingM4QylGsFTLKIEILV1VXiOKbRaFCtVml3OiOvXL/fZ3V1lSiOkVbiuqmIdDgUek6SBNd16fV6FItF2u32SOfPcRysNegEer0e5XKZOI7Z2toiCAp88MEHDAYRDx484OLFi/h+G5B0OoJ+L2JlZYX79+/jBz6u49IfDNi6c4+5M+fSIhPfw4QxUkmSJCEKE3A8PvnyOrWTpwjm5ulupCHgVtlP5WscF2do8AlH5WGPnJycEUfSAZzMHhuxS+VBzKiwfRrTz2PDGTOUKATjfwXjQpX95TMEjza2H27hK7+hnjE2O3tsT3Q9vnLZkf3Y71xMXLOvOGfOTv33dedw5/zsqTMszM1h4pi7y0v0XZdir8/CvYcYDXFsiGNDsVLj5r37bLc7/Ognv0u5ssjd2w+Yq1YJfI8z585w4swZzl68yFajjlcqcOPWHVqNAVanU1yzu0W9nhZIeJ5HsVik3++nmn7GDjuSFCkUCmnrtuHrYRhitCYcDMBafM8jHAwI/IAkTkjiGKM1Ugj6vR4Wg+MKQBMnEXPzc7RaLdrtFqsnV/jeD7/L2995CyEFc7X0rLXakpNnT9MNB5w5f4Fz5y/SD/ucOLXCb/3WjykXq5w6eZb799ZpNrrcunUfzy/yg+9/n3fe+A71Rot7mxuce+kSpW4qA9MIFK6jMCb1rirlgHSwE3NlHgbOyXmxObABKIU3WpQcP3aFgy8kvpB4CBwDjkklMNShKxqPzmzRWXfPRViFGo7VMeAicUW6zD4pgtRpusdin14HkoOfL8Gs45281GmV7PA/cbRxK7v3IuFI4fnjJTsPM67V6Jrtfs9X84NoJv77ehuBB6jSFy4CByU9ziyv4vR6VIHPlxYBeO3BJg+2mtS7A6zrUZibZ+HESX7+q/cIKlXe/f5vs76esFBZ5dsvX2R7/Rq9eIvi6TKr3zlDvGL46MEV7uzU+dmffkI/bY5BoVzAGMP9+/dZWFigWq2yuLhIoVCgVErz7RqNBgDnL16kNjdH0fcRUYInFC4SPYiwUULJC+h3OhDFVAsl5stVkn6Ii0CR0Oo8xNAjitpAwjvvvM3qiVXq0RZtUee7v/MuK2cXufhS6vFsdx02RI8vmzv0/SI7keY7v/09ikuSZr3BrcsbXP34AdLMcf78O7z06ve4eafOres3OXtqhcGgxZUrn/Lqm69yolAAoBUopLRgDVK5SL9AlIARalSEkqe+5OS82By8CljIUbsmEMO/RfbXxH+Mn9urI4AQx/47O9v4y0azh6QLIvX4kRlEInt2vz3NWI6fw3cQOXh96RN3N9l3EaNr/9Ux+7o/KnPzOMmbnIPz+PJfgUg7UmjDydVVTK+HtPDxair/8srtNVrtDtJ1wFG88tpr9KKQX1/+mGK1gpWCly5d4vTJk+gw5PTqKnPVCj/5nd/lr//+77N0YpVOv8cgSlCOR7eVmtTFqsPi4iLLy8sopWi1WgghCAIfbQzz8/PMzc0hhOCzTz/ly6tX05ZxdliYYwxYi4DUG6jT6t5Bv5/ebjgOgR/gKEkQeBSLAcVSgdde+xa9Xo92u0OlXGbQ69PpdHA9HynTcO1m3eAWCmgkJ8+e563v/AYfffIJ9x7c5/r16yipSOKEQlCg1xuwvb2DkAqQvPraa7iezy//6i+ZX1zEDw1SG6wQtIaVwGnxSoCUecZPTk7OmHxGyMnJeWYYY0CA8hTnL15k0B8QeQ43a2n7t3Nf3uZ2s0liPBYWF/AKAf/0X/8rmr0OS3FMo91kYa6AFZbWzja9ZpuVi6dobnX50z/+Be99/DEnVs8y51WQfUuvlYpTb3ceksSVkcSKUoogCNja2UCrJA31DgstarUacRwTFItgLXEcp+Me4nkenU4Hrcdt1aIoSitsSSuH79+/T7FYZf3hDnFkePhwHU3IqTOnWbuzxmCgqaaHzJ21PidOvExjq8/7v/o1BVexsnKKYhBRKjVAeGysb+C4DrW5MnfurtPr9TDyHV5662WkE3Dz9n02d1r0BwnlTkSrFtAoeiyT3vB5vr9vr/acnJwXjyMZgOPQgcVYw2Ti16ywwuQEKhBI9XiPy1HlLw7qMZu1ZSnlyCF00DEIIYbdMY4+7v2QT3HbOTnPCotFCYXjKFaWl0mShC9WF7BCcLbRwtx7QH8woDg/x+qZU9Q7Lf70V3+OFoLPvvyCwvwCryyeh+aAl5cWWK4tQii5dfM2YqvJ7//0b3Hx9Al+9s//GNGPcGgCIaun5/D985w8eZJGo4HWeqTJ53keURRRKpUIgoAwDAHodTp4nofv+wwGg1F/4E6ng5SSarVKFEVEQ2kVx3WJB6khmcQJCwsBjXqHpaUT3L//gMXKHIuFee7eXGNgNJUfpnPix9e22OxtcGLpHNXA8OXVz3j7jXOoJY+5uRorJ07Rau1gpeXkqWVKZZ9Go8lWo8VSb4BwCzQ623z8+VW0dKn0Elo1qBdcFrQmitNKciElQopRJxCl1NS8nJOT82JxpCKQqWW/12aEGtP1Zr/3icKTj9nmwbZ9+DDm7mM6To7jvOTkPA84yqFcLlOpVLl44QLWGi4vLwDwrTv3uHfvPkpKLr70EmcvXuDP/vIXtLpdIq3RwKuvvcpbb7zMb//oN1FaowcJZ1bO8eqFNxm0DYO+JRxotrZ3KJYCdjbTMKsTpAbf2toag8EAIQSDwQDX9RAICoUCjUaD9fV12u024VAcOpOKySqEM2MvSRLW19fpdNJOHoVCAdd1KRQK9PsDGo0Gvu8PRaQjet0ecSeisVZHaoXnJEgJUSzY7jm8+cbbnDl9geWlU1w69wqnTp5DJ+B5LtZGaBvSbtd57/2/oNttIh3L+nYdVMBvfO9HaOny3oefUFtYodpP2//Viz7W2qGnMjX0CkFh1Ls9v5HMyXmx+WpDwLPmn+fFxrG7/s3Yb3x2xuPn5Zhmsde1EPu8Nvn647b3dToPOcfKZG2zALTRRHHE2bNnmZufo79huDzs//va7ft82WxSrlQ4/9IlWr0Of/HBrzBAMuzpffXLK/zg3EVUPxVwRhtu37iLmpuj4FX4r//xP6HX2MHRPVZ++rucrawADYKKoj7U61OOYmNjA4A5rwpO6mH3vFQ7D0hlVqJoZNQ5jjMShg7DEM/zkFKys7NDoVAYdQRxhCFJYgqFAuvr6xgtqdcbdNpt5gnwqi4DKxjEW0CJBxsRq6fPsbJ6ii8+ucFcYYHLl79g4+ENvv/9V7l/7z7nLs0zt1Dl4cYDSqUCp8+coNXuYkKBQfGf/oP/LUnS45fv/ZITQYGL3R8C0Cj5w7M+vgbVYdw5M24P3Q89J+e4edx9SP75fGoc2ACc9DxN3TkKmUqnMLyOM+4qhVCjJGo7fvcj6MkfjH28XUe5e51cxyJmVu4KYUbHJHaFivW+1pDMNp4Wk4zCyNNh8idl5rXYFzmU4di1LSxqhvWXbvvRc2QBK2aHjrK9pOdwfB7ShHqeqy/0Lh/26NHBRVkks6/twcJrk2MQ3wg5mGmsSBBWYK2DUhKlLG++cxE3CLlVETQCDy/RXHrY5nKsWD13nlOvvMZ//d/9v7nTbNI1lsRa3F7IonHo3XuAtorOTod2a8CtLz5g4dRp2laTNB6StBooadhcu4v0Es58r4rwEza2N9DrCY7r0ut2WVxaIkpiAt8bdfzI9AB93x+1cAOo1+tEUUSxWEQIyfrmFtoYXNejXJ3DdT3CcMBOs0mj1WFhfolCscziwgmUW6DfTxhsttlotKFQ5KXXTgBtbt6PaHUNH177knanxfz8IgsLZS6dXsaLBE7Yw+vXWfUsGzLiXNWj//AWRS8Av0jU3uKf/X8/58tbt9hsNmj1mnitNrDIZi1AW4FFYHSIkD2Wa2XKrk+UxCTGYoXI789yvhKyiJYUCjnxW5wJ3xtr9v2dyXlyDmwA7p2Dll48hBq9ZuzeF0xOFKRaDHLPC5tuO3tlVsjzqHlwj67z6OELQJoERuMbVi6no0Pvud9sjGo47qHBS2oPWxtxXAbg7nOSeSwez6xov8WZ8SWzNpUo2YtkhuifgJHxnH4+xMQ6GvMcWTdZ2D5laOwOB6/Nk5/XgxiA49r5zOh8jk7QsWAxMkRYB6kdrLEoR/P2dy7QCL7go3fTdm0v1+ts3G7SC10WT13gYS/iX/zyVzRxSJTE17CkfH7npTe5UCzTaNbxai4nTtVYPVfm8pXPqW9tU1Yu82WXt99+B2s19c02UMUvSS6+colqtUq9XufG9Rt8fu0L3njjNdxC2hUjy+/zPI8kSVBKjTqBGGMoFos4jkOSaBZXTtIfhLSaTbabXZQaUC6XcIMiVrpEGnB8vGKZVqvPw60GtUKFnc6ATnvAd34QALCx7fL5F3dwVgTtrU1uX/+MRQQnfc0gWeQnr7/O4mKVTV9zyn2NV159lXt37/HKyxf47u//LT578JDB7ZAwiXALLram6b2dVv/ePVPmV3/vAhf/6QN03AXaXDpziqVylbXNDVwhiSc+dbnxl/MskVLiOA6OcHHtsHe1ZdSBJyGZ+TuTczwcQwh4L+mV/bAz3/FsL/PsURxtfM/HUe3N7GsimO2VS5/ewwDf55ie57Nw/Bw0Bv5ik3n9jUhvwqqlEu/+dJlmdYtr6l0ATi9sccNdwy+XOHnhHP/25z/n5p1bGGFxHAdpNaVCiVajwZ2ddbq9DqValfXNNZZPnuDC2dN8+5236QwiPN/HdRyUUiyungd2cIuCwC9SKlb5kz/5s1S02UqkcIiimDAckCQJxWKRIEh7+WY3V57nYa0lDMNUP08p6o0Gi4tLKKUoFAqj4pGoD2ARUtJqthD2Ie+992sWF1Z4/70PqO/U+Tv/6VtcuJQAsHLS4/RCiT/8e3+Ta5cvM68U9Rs3eWl5Gd1q06xvUi4Iir5DrVwg6rU5fWIJYRJuXv6Yy3cf8N4Xn/Mf/0d/j3/yj/5vvPN/+immmN54hDh0l3xu/q2TrH44wA4GnDh5gm9/+9s8+NkfD7/b+ec05/lADOMf2W+OyD+hT53nTwn0m1jk8A08pKNx/BqQOc8/cniDYZVAeQ4/+clvomtNYiG5I+YAuGS2Of0/e51CrYIs+vyrP/rXI+9xFEVoozmxeoJisYCO+vRaDcJui0G3xaDTRKGpFXwWSgXmiwFxp01zc51Fcycdg4Rvzb1PfeMO3c4AcFheOsn9+w9xHDXqDpJ6+JKRAWitHbZUcxBC0Ov16HV7eG4qBVMsFnn3Bz/gwsWLo5zBS5deolQs8sWVK3Q6HXzfJygUcHyP/+Q/+xY//v05PJkaad/97YD/0R/U+MGlC7y+tMjJgsdKyUNEHVr1B/jK0G83sPGAou9goj4vnT/DYq3EymINZTXff/cd3vuLnyNURPmVJXypkdagsBgp6Jwu0Ol26DdbuAj+4A/+Fq7jzIzW5OQ8C7JInh2mBu3+O+fp84QyMI+GYydfO4rEgMzEovfY9pNy3DmFB2W/8/UiISfyAF7k8/AikaYEWIySWNelWCjxH/zdfx/YQWH5j82vucsci6KPXSxw9uWL/NWnv+bLWzfRVmJVupHAK/C93/weSgpWF+YoBw6nz52hp2OCcolWt0O3vkWttkg06LNcKrKw2uNHr7S4khTQjqJa6fMb9kM+WV6hUKjgeQ4ffvALvv36KTxHE4V9CoGHkhCaLtYklFWE1TFWa1Aapwyep+j2BsxVKxQLPey1f46vI96tRdhkgI77UDD81mqA1Z/znW8n+G6XP3hpnqUTGq6vEa2kfeC0lHz/x4p//Y/+G6rFABsNMO1temiSfoNSrUrJdzh//jyffvopd+8+5J03X6NQ9PEVvHzhDBd/60f88r0/RyRDjyWa3+bm+F7LWmw/ot/qsNm8w+lTpyiXK4SNBok9aKpDTs7xkuluGgxCipHRF+t44l25D/BpcmgDcLcRNfkjfrTihKmtT6bcHbuBMMsAfJqGyGiXE0bti8vQuS9e9PPwAjHstpPVSC0sLvD2y9/lnvk3SGG4QJ0L1MGA13Q4ffE8/4//+3+Tdh0a3j9KIdFGs7C4wInqHMHWbUoLNSpFn4unznPlxpf4jqBYq7K8uMD777/Py6+8zOvnBlgLTqLRjsJECZVBl3/w/Vaakwz8J68VgI8Of1wLANvjv7NugjOJ0n866c+Zam+g/+ou/P0f4xckhV6b5UqBtfoWp+ZKKDS+7VEsuMT9DusP7uIIw8nlBeYrRbSF9s42tVKJjz/4FRtr9wibbaL31/DePcHoAI1l+XIT24+wcUKUDPjVr36FNXYkDZOT85VhwZBKFeV23rPnK5CBGVWC7P3S7uf3eJ/Y67XjDi3OGJ9gZqHz0xnHYxBCfP2MKXuA87ibPHT83GMnskNF+sTkXyDg7bffQnUSaneKNL/bTd8kBE4Ipz6bY7vicfPBPTR2WLSefr4X5xf48W/9iPq1aygJCsP62n12GtuU5qoE5SIOLp3GDoN2g/bWJuqiRgg487PLyHKAM3TEyz0+S8YKtBXDf0EbsEgMMv3XCgyCxIBBoo0gNpAYUI6HNqCtIEk0rVaTMDIUilXCyLC51aTbi/CrFX7vD2soT7D0f/0T3CubOL//Jg8Th4owqLDParXI2bMnEMKwte1jDDjSTfMLA5elpZN88dknrJ48RWnlNAvzFe6sPaS+uUG1GHDm5y2cuTnWz/tg4eRnLc792w3mzp3FkYrl1VXe//ADev1eWpWff69yvmKstSQko7/3b8eac5wcugoYdnX1mKhK3S8cPElaGbt31YFCjwwDNVmBay1mooRUkXbrGCWXH7Jzx27G3sFUrmKvCk5hLY5IHnkeUomT2RIx0/uZdb6OwnFv72kipn5vpiuEZ+Uj5TVgXx+0MmjSHtuBcjFZq7RiEU/AyyeX+M//3h+i712jtqUpb0REJw1SK1b7y+jyHJcfPOBht0mDHkZ5WByktSj6RJ2bRN1rYLpIz8X3FG5JUJ3zqcxXcRJFEcG7b7xOp9Ph+pcdFueK+Btt2OliXztB4ij+8R8Jvry/hRuU+PZb3ybWCUFQwFrLzs4OkBZ+KDcAqdjZ2cEZ6gFaYzAWgkJaLHLnzh2KxSKlUinV1cPSqheIBhEnl1fptbp8+vEDqqUqF0tn+Xe/iPje33KoOg4BID/b4PLncGZlBWu7FEsKQ49yqUQ1KdLrdOm16wCcPrmAkKCEIBo0WJk7y9yZBQqrS7xx5gzFsMfry2dw/2yH7/zcICJNtzWgi6C0ukQyV8aWAq7cvcUAQyLHN2H5T27OV4UVZljtmzL5WfzmiWI9XxxaB3C3gTErJLx/vt2EZt7ktrBgBUJM+RHSdSb+/+jzQ3PtiKHjR49BkEm6TI/PoOyjhQypLt7BElf3C6EfhScPuz8bJlSAxs885nrlxt/XDKHT7y8AEkyEkgIMvH7pLP/n//1/zqsLBQa3bxDQw/YN/m2D6wSUTs8zKFS49eA+rX6fRJhUiskKrDXEgx7Xr37EnO6jHAehBMpTBAUfz3exJPR6XfqdDjruk4Rdrl8TnFkIOQ1gLYnj8N9/5GKUw8WL51GBh+NLPFEmjmPa7TZCCMrlctrbV2uUdFk9eTr9O45JjEklYpx41CYuayUXRREP7t9HCcmFM2eplMq8cv4ihCGteoP7N77k9hcDnParNOuWd4Gdf3GTcOEUzMdg0lyoJAkxxifwPQbdLo4jEUAU9nEch0q5RHVhDr8gWb9/i3/5s7/k/rWr/K//3t9BWYvrumg9AAFaQGw03bBPbf4sOwZ6UZjeNENel5Xz1bJHOC+f858dz18VcE5OztcTa9KgqdWIJEJZw2K5xB/+1nf5v/yX/wVvr86xfe1jCkkbJ27imAGO4xP4JVSpBm7AtWs3iMIIhUylOC04QhC4HoHrYYeFDo6j0uraIKBQKKS7txrXFUhpQGgKBZ/tu+kvzE7k8Ud/ucCNG110t0tra5Ow1UImmigKsUPDKQgCXNfF94ORcdfv9wnDEKUU5XKZ+fn5USeQMAwRQhAEAdZaNjfWae5soTC06lsQh0T9LnG/R7/dwJOwVF2i6a0AcCIeUC4G9Hq9kcxM1nHED4Kp05vJ0kgpKRaKFMoBxcDl1OICr58+w2999zeRUiN29VnX2vBgbQ3UhMsvJyfnhefABuBItVtKlFJIKZFSHrj37uQyez257+3ontsS4hHlmCftLfx17bd73D2Dxa7/cnL2wxEGhcaRBikSVhZq/E//3t/l//gP/3ecCnzWr11BRX1IemAGaJ1gEkGSCJrbLUItuXXzHlI4OEiUBWnAsYLVxUUunTmPJxRKjOcPbTRCChzHIU3CiPEDRbkSgEjwTNoLuB35eAYqjqTmu3Q212lvPMSmwn0jIeg4jtnZ2aHZbNJoNBgMBvR6PaSUaJ32Ew6jkP6gj+/7FAoFtNYolUrJnDl1ku//xjusLNRYmquQDLo4aL792ssEClYWqmyu3eXzZhOAWreFSELCQUgURQghRvOr67oUSyX8oQ6h1pokSdBaMwgHdAdtHKF5/dxZ/v2f/ITFUgHXFRgSEIzm6tG4O11cxxm1g/u6znM5OTnHw5FyACfJ2rbAwUOQWTPy3aQ5bHuvM7MrCMBEq7X9jJ/Jse7H1ymvbpKnkl84KduSO+dz9qHkO6AN1WKJc6dO8w/+/v+S1196mXB7A60jqsUFVNwm0X1wDdooosgSRwOi9kOKxQUePtwCFOgENcwZ9ZTiW5deQSQGE8UIJy1w0Ebjez5SSvphSK/Xxgx6aJMgpMaSUFYhAI2eQag+K5UyvThiuVzESsGda9eYu/AtBmFItVpFCDHS/Au8Ao7nIYSg3W4zGAxGRt/iyiKdTmc0p2RagS9fushyJWB7Y4P5SpVf/vxPmS9XqJUCTq0sslArU3AFKy+/TPzBPVytmR908U+fwBhDoVDA8zyCIPVAztVqmChtUwfj/Os4iQjjHjK0LBfL/Pjtt/FNgjEhiYlwGN+sZ8ZpEg4or5Q5d+4cN2/eJEmSr83clpOTc/x8BVXAOTk530Sktfzo+9/lb/zeX+f77/4GvnLRUUigajy4f5OV+RK14jxGlIiTHmhwEpcotCAdvvzoMxo7baRwsSZBMswdNZY3X3sdtKEYFBgVDFpIdIIxZug1ExihQWgcV7C8PM+82wCg07cIZ4CxGk8JlqsVuoMBX9y6RVya5+Sp02ml7bAfsFIKx0mnxzAMMcawsLCQGpv9Pr1eb/S+LAS8sLDAztod5qtlbnz+Gfe//JJaMeClC+eI+l0qxYBuq04kQJiIDeVyWmteLQU89DySYeg326bruiSuOzWW7N84jgk7Dc4u1Sh0FcUgwNiQTjxAE+OQ9jAWQuB5LrHjIITE933OnDkziuAcRas1Jyfnm8HBi0D2qtqFcab+MUUT0nYwk9s/gMcu2/1j3ip2VRVkSdDHgp09hpnN5Z6Lm+9hQc6e53lvmYjsfD8Xwz9WxMRBSaaP8Ot6tPvlfR1dBmQvr/9vvvk6/4f/7L/AVS5WSe7eu89nH36CbsNHH/6afrvJf/C3/wZ/7Yfv4voVSg6Ibog1EUp5vPfnf8FOqw6ej5UOFokDlAKH11+5SNR8gDY67T9OKixe8Hw8qYj7ESZOwBqENWA0ShiKMs2bM14Fx5EM+gOMBQfLysIciVQkyoUoJuz18ApFjLVEsaFbbwASCyRxzIN793Fdl0KxAFLi+R5SCrqdFkuLc9SqJXpbkl+//1dc+ewTzp86TTFwiHptlBAkYRffkQSuolKqYFqn4PYNio064vwlfN8HGBWUJEmCFRblKZSrsBiEsFidpMLUsaHbaqMijS8ckiRGuaAcfyyNYAUmMThW4To+AsnC3DxyKB+1+5ORB4Vzcl4cDh4CNu6ez1sLekLDZ79Q8UEQwh1NQukE9fj1pE3gAIr2k7IyBoiPUQhV7lOyqqXa+yVhh5pk6avHfTd+8K4ssz4GergMt5eljAqmnj8OpJgY61cmUDt5HsZpBbvPw9NESjnT1DzS58Puc23Fkx1Tep3GJsN/+b/437C6GUPR5b/6V/+Uf/bHf4zTM/RigzEOUvv8o//nn3PlbosTqwF/46+9w6ovsD1NGPf48JP3SIgJjQW/iBISn5jTJyqcWPJpDwwRCY7wwFoKjkfJ8SnioBNQscUmpB5CDQURIQTEVmKKRbQ1ONLDsUAE8aDPUrHIw60dksTiKJe1jQcQFFBBgYLr4klBojUi1hSUi0QS90MiJej0u9RKAYqYu9cv4+kOJcfyyft/wTvf/jZLcwsUHRdfSUwUQ9zBcX1OnzpPFIV0qmk3kEq3g2Vc5AHp3BfHMVoanKKDFzuYTogOYxIMgacItyS4lkjG9KNuWixjAnRsidEIq5DG4hkPIoHoGERoWJlfSOdCO9RazGQac2dgTs4LxcE9gHvcG+6+f3zSpOK9JVL236YgzRs82J6nFYaOi8wbttcYZnr/YOzrfApizgeXh5k1vtnX9rhzAZ8PKZu9zsN+V/bpjWGvr9GRO+vsydM5pijWJGg+vPwB/+aPf0a91aEmAqSjQLtI4dFp9/k3f/Tfc/pkgYJt8ePXLuGqItfvP+CzWzeQ0sV1PcLIgIQw7vHOG7/F8sICzVtX8BwHkYzHrqRM9SSNGQqMjouXSjJtKdXVHspxwOhRsYfjOmm1rZacWC7wYHOL0uIylWJAK06IwwF60KfgpV65cJDmEmqtUa6DLPgoxyHsD/Ck5eULF5EY/uU/+//x5muvcfrESVwE1aCErxx0HPPKyy9RLBUIwxDf9+kvLQNQbDbT8Q9v2LKCk3SsIpWYEiCkQA0reZMkod/tEw4iHD/9nFprSSKDlhbcNICeFt0p4kQDAuW4nFg9gRAiFYLOyF1/OTkvHMeTA3iAyeOpdqz4ZsYjc3K+VqxtbFEpF/jo089ptPsor0gSQxzFCCtRJiG0sFANeO311+m0OtS3WxRrLpdv3aKjLZEVCOuA1rhSEDguL5+/QNTtIY1FmL1vTIx9tLVZUaaFEz3jpbl1jqJYLI6UCIwx9AcJJb+MsZqba/eprJ5EDCISnRAlhvrW9kj+RWtNGIZ4vs+g20VgOLm8iIuhsb3DzuY6J1dWuHDmHAXXw0HgKwffcbFWUDtxEm0TCoUCrVYLPTePVg5KJxS7HXqVtDo3MwABRObFt+mxSqWwJg1J95IoNSYdB2fYMSWOdeo1dPzsDKXV0slQpl5JTqyuotSjOqc5OTkvFgeWgcmShicXJRVSPfp8tkySTbq7ZWQOus6s0PLu9x3UCylgauz7vvcAMjUHHet+zDonxyHXcNzby8mZDNsD/NX7H1GeW+Te2iahhlDDIDLEcUKSxGidiih3O10e3H9AwS8i8WgNYn55+TKNRKOli0kknuOjLJS9AudPnaVdbzBod3GQaccgpXDdNC1Fa02v20UbMwqTK6UoqdQA7OMjpRhVw2YVtoVCgblaBV9Z5itFip5k/d5tHBLa9S26nRbGGNrtNvV6nW63S6fTodVsosOIXrNNc3sHZWDrwUN6zTYnF5exUYQJI0RiSPoD9CDERRCFYdphRCmCIEAqRbc2B0CpUR+dx0wKxlgz6pFqhx4+ozWOo4iiiG6nQ7fbQSgHYzSe52Os4cGDBxiT6SU6KCVJdIIUgqjV5uTp09SqtdH5y8nJeTE5tA7glM6cmNCKe4wG3e7XDqLTd1BNu937P+ABjdY76HEfx/sOsv6TaBgeZNs5OU/KXp+nK9dv0h5EFKpzaOmQWIWVLo7rAoI4idEmIYoj6vU60kqEVfQ11AcxkXRTA1ALrDZIbamVyizX5rFRgg1jhJ6WfMrkjtKgdppUkY2pKIYGoPVRysFxHKSUow4eruumnUqSCBP1KQcevVadfrtJMujQbbdG26/VahSLRQaDAa1WCxdJ1OtT9gLCbp/G5ja1YhlloN/qMuj0iHp9TJRgoyRtLsxYbzCLhnSGBmC52Ridx0xaxgzXGd+4ydFxDgYDtra36XS61Hd2KJTKowrlu3fvjrQJlZIUiyUqlQpKSdqtFrVqlVOnTmL0s8lrzcnJeT45vk4gdtcy6/nnLVT7uLE+r+N+HsjPU84En934kgcbm7zx7XcwKLSQaCTaGCyp4TOZSxxFMXFkCEpl/lf/8B/yg9/5HRyvgEDhSgclBEvz8yzNL2BjjU2G1a8HQKHxZVqc1jf+lLRLJrOSJAlxFJGEfZKwh6vg7KlV7t66jjCaJImx1mCt4eH6Q65cuUJQKPDKyy8z6PVxhKRUKLCzuUmlmOb6Rf0+Oo4xUYyNYmycYGONSfSe1diduXkAyq1G+sSE4HOSJAhSL15qvA4LN4B+v0+71WJnZ5vA8xAm9Rb2+32u37jN1tZWKo8jFdYYojDk3s2bfPD+e0RhyMuvvDI0zCF1MeZf3pycF41D9wKeeg6BRe1dIGLBoMf9Xs1YSFgg0GK64m2v/ezXd3j6NQE2s2XtrrGOJ7ap/dhp61dO9qY1s8ocBFqMT9n0GMxQZ2Z6rALQxzCxPh+FEhNYOXW806+ZJ0oqnzx3e0lV5DyfKMey2XxASWkulh2utbboKEFsBrh+QMEmOGFCzfcQkSaJQjphnZpc5URxlf/5f/j3+da59/l//Xf/LTvtNQJHc3p5ASeKGLTaxFGCRKImxM6FEMRxPPo7ozD0/kVWoYWD46iRIaWUSo2/OE7bvA16xGGIimOKJuJbqwvcvncXUZ5nEAcozyWONcVyGYRDvdVjYA2VaoW1h2v0mjtcXLpA2NiiWPQo+AV8V2BJSGxEjEDaVOEg8wBmaSK9+QUAyq1mmt8oBBKBSTQmScC6KOWhlI8gxJiQONIksaXb7pO0Y/xY4QxAIwk1fHJ3g8Xrtzh9+hKOUIT1NmG/x9r6Ok3HYc33ePXkGWwEWAUkme/0mX1WcnJyvnqeKAQshECikDiPLAoHhTv6W6AQdrigkOLRfLTdeXSzXps28DIdA0lqximkcCaW2fmB0maLQA2PRA3FTsavTS8CByEcUtvZAdRwGZ/KqbHKY2jJdoQcx6ePnLE8ebhaCokUacjr+TnenMfRj7rUOxu8cfEUf/sHv8GKk2BtA+3EJPTQokVQSCgWHQa9Abfv3OfOxjq9VkzFzqMaDr//w7/BH/7+/xDhWowNOTFfI263GLTbGG0wUqbpGxOpJEmSevqmDEA5Dv/unlcy4yvztMVJjE0SZBITmIQTBZ+TBZdBa4dWcxvPS0PHVgiE49Ho9FhvNIiEZW39AadPrtDZXkdFXQJhcKVFCgtCo0VCIhNimYy+GpN50HGthlYKpTWldhtlU7kqoQ0kBqxACBcpHIRQYBVaA1YSDTSNzQayZ3AjibIKjWKj0+fW9g7WDzBCYuKY/s4OvfWHlHstwnt3uH/lKsKIVCrGMuq6kpOT8+JwLFXA+0nEjF6zmcfwafhzDiZlcpA1stUeL+mSbftFLkHe+yzlvJgk1nD1xk3eXL3ASmmBn771Q66s36UfaSrlCtZatjc2SeKYgVSstwdUtjTlFUs93mT1/Ku0dZ+CU6FSqmL6MSsry/S6XeJhn1xjzIFuW4silW3pG/+R18wwXKq1RpvpkHKWI7iyvMydcJP765sorYitpNOPsMJFOR7VajUNHycJ5XKZxs4Gc3OVVJIG0DqZKrLY/a3IjEDw6NXmqexsUW41GAwrgWehlEIhiKKIwWBAFKaVwHEcYZSb9hCW0Gi0SbTGGIPnuqhRb+EyW9vbfPjhr9NCkdzqy8l5YXliA/Cg8i7Z+/YyFp/mfp/Nth9fIJIHMnO+6cQIPr7xJSfLiyTNHmVZ4Lvn3+TimXNcfPklWoM+/+0///9w7c4tujrm9k6dheWXsf4CkQ2oN3s4RcW7336bn324wvZai1KxRK/XgyRJ9TKnBLpTYy5rjzYdAh4agEMPYFYcksm/JEmCHhpIk1/NJEkQQuD7AS+fOkvjs+ts3r5HYX4RHJf7D9coV+dwHUG30eX84gKtVotSqTRu2barD7e19pHnMgPQGENvfoHKzhalVoOt0+dG61hrMcYiJlItlFIIbUdt63r9HuFgQBTFWJWlTAh2mt2hZ1QSBAGnz5xBb6xRqFZ5sL7Dw831oeBkPi/l5LyoHLwTyD5dJQ6SnzZZMWiEnZp3Zm17d5XhrNfsrsl1Fvttb9b79tv27lD0rBFIOZbbP+hYnyYHPQ85OYch1JJPbt/h3t01zs+fZGVxCddxcFFsbW2x3e/Q7HTAd+l2+qxvt5nfqbP9s5/x0qV3WFiq4wYJD7dvMRh0iKKIn//5z1n54Q+pKYM1BunI0Xco7XPrpQLISZLm9g0delkIuGf9VDxZpL11pZRDj1k8MgCNMSPpFaXUUITZUFEFLi2d5NbGJt1unzOvned+s0lkNfdu3+X0whxBEOA6Dp70UUqgjcaVHq7rorUeeS0ziZqRELXjkCQJYRjSrlRZBUrNxtjLSfq9jOMIJdzRTamUkn6vS6vVRilFGIY8XF8nGYRo34GiR1DwKBcLFItFvL6mZ3uUy2X8pk+lXIbNOnY4ltwDmJPz4nLoIpCDF2bMWH+/DhMH3PbuvLCDGlSHHet+79tdaDJ7c9PdHb5q4w+ew4KSnG8ERihCKYisodXeohR3cBBcf3AbR0n6SURfxyRK0NcRA5vwxb1rkNzny3t3uXj+DKWS4YOP/4yNwRZSa+7du8eDtTUKK/MIa3DE9JQVRdFQ4kQRJzGCtALYE8MKYOuNKmmFEGOv3wzGAsmCkvG4sHySUrnGjjE0o4gvrl8hQdJv1XGSiNfPncbzS7ixRQh9kNblwNgDqJSiv7AIQKnV3Kdv8wQ2bcMXRRGRioiiEKUcwiTBGgdrBJVKIe0t3O/jOA460ZRKJfwgYDAYECfxUMcxv/nLyXlROXgIeDgvZb6sR+apQ99Jikc2kmkKHm+49NH9PDX22s1jz8tjxveV3qGLGcc0zuuctdq+l/Aox7TfvnKeD6wkUYJECgYioR62kcayoxOyDscGcJQLQmAkrLfXkSagM2jzcOcKmDaJ7RJKQ80vMFetUioUiKMIR2XGytiD7bouYRiitcZ1XJIoIRiGf0PjYIRCknbOsJ6P6zkkw4hD9tGxjG+EMokY0KhEMF8qEyvFIIn48OoX7DRbDKzBtQkPt7f4+LPLXPjJj3BcF6EfVRuYOj0T3v9Jj+OgWkNLhdIJQbdDv1Te9zQHhYC5uTlY3yBMElq9PlYpVk+cYK1dZ3F5Ad91MCZhEA3QOqbXHyClohuGPNzeYaATlFNg5DLNycl54TiwAehO3ChaOw7ZJtgpmZPJ6rxJJu+6JQpv2EUgzXOZDO0mGDEWTd2dN7PX9vYPac7KGLdMbO4YwqCKPa0RC9hkJJkyPVbB7N0avvq7cwl4jz5tLR4Cu0f3eIMgmakoMZ2/dRCUEKPEeizoiXZfX/XZyZlGWoXUYvrCWBjgMJZigrE61DCnT4ZENkIkFms1Qig8o6naiJPlgILVyFjjSIW0AtdzcRwnLbIQgjAM04panWAEBDI1AHv4WCURjsJTDtJa4kFIEoaYKMZECSZKSIYyMlmY1fd9rB2AB17RYxC1+HLjHr++c52OcNDKBQE9HSI21/luu0NteQEVSpTQw9BtPPImaq0hjhFxGrLNKoDT82FBSnrVGpXGDqVmg7Yf4DhOGjI2abpMVpwSJ3201iwsLVHY2GQQG74c9Hjn5VdwqgsMrt/indffZnEhodvZJAwH+J5HfbtDuTbPTePwpw/WaQhLonujy2TyO6mcnBeOg4eAJx5NOoDEPhp+s7QDp7doJx7ZKYmHY9EHnDGxCUz6mjiOMGi2j72Mzf3GerB1vhpm/yCMr+Be1d9wXMeUGQmTn4+x2mPO84Yg1bDbfXHM8NXJf6a8/GKsEYqQYAUSWJqv8torL+NKgbDDecJOS1IxzIvLDC5j41EFcM/6I2+1EAJhU43PVGNPY7R+pBtGlp9XLJZIQoGVFsd12Kxvk1iLFmClBMcl1gmNbpftZhP/7GlUorFa753nOyHwnOUGTtKtzaUGYKsOKyf2Pr+jeRE8x+HUwhJ3HjzEFYp+p0ettEjSDfn3fvRjarWQzYebfP7pVebKC9TKCzihpuVqPr52jVDrNCxuDPt913Nycr65HF8nkJycnJzjQgguXbpItVoZpxzsQVYIEQTBSA8wawHXs49KwMBYBiZJkkciCVklb7VawXM9lFQsLy8TDsLxBqwlFeMTxMZy5+49kqEhuZ9uZSY9kyTJIwZitzYPpIUgB0EKwXKphBdGRFvb7Ny6TevefZY8jzm3wPrdbT7+4HPu3l7nyxv3Wa932e5FfPTJZXq9Ho7j5Pm/OTkvOEfqBLLbM7fXnDdZzQZM9b/caxujf4XADP/OJBv2Wmdye0ctTDluntV+8yKOnG8+goX5BZJEp+LNwzAoIjXgJkOonufRbDZwXZc40SMJmI5xQYIYzhWTLdb0hKcuiqLRXl3XHf4tMMbiFzwunj2HxaS+aJFWFE/m7a5tbhEOBvjGIi0YY5HSTlUYCynAiNFzswzAYquBnZjzkiTBDXx83yfsjkO21hikjlFGc2Z1mblSQOBAbXGOne0dPrt6m/VGn82Bptfc4vJGk81Wi7/cXKPdbecC6zk5OYeXgXkkvJHlaO2aT3aHgKdkYGbJuWARUpI1z9g90U8mUE/+OzmhHkUe5jh4lsbf5L503tA95xuIkpL5+XmklOgwRjhq4nttpr4HeuiNs9biCo03rMbtGReH1FuW3ZBmLeAy71/mDZxk9JzxObG0RCglg/5gaAAyNAAZzXm9fp9Wu02tXMZqC9Zg7ViuZrQM95ctkwzKFYyUOElCcdAnHsrbZO+bzBuEVFqqMF/i1Eun+Pb336JSqeGVfR6sPeQX73/Aje11tjtdHjaatBPDvZ06kYW2SZ4LKaqcnJyvnkNXAT/yt5jx+pNgZzz+OvNNOY6cnGeA67lceukShUYXu76153smvXqZvEuF1Ps3sC5mIsNFKQex6/3Zsnt7KYKi53Lq1Cl++cUVBv3BzLFGYcjGxgYXazWsnvFVt6C1mfI+Tr08LAQpN+pUux3qc/NorVE4GGvQxk5I1KT9xR+0N/n1revoUoG33nqH+59+RBgmuCtzfPrZJ9xcX6ceR3S0JXYUCInIyz1ycnKGHDwELNPJJ9Vw1lhhRx14pxTxhvOanfgPxhV2ux9P7UMIxESFsbQCY7MU8untHXjc+3b1yBLWxS7lkmkP5Xjc45v/Q40B9lwp2+eT2IZPsyPKUTnKOcp5MXg0XWP6+cyzV/RcTi2t4IoWnWYHEUfDAjGJlRKrJChJbDWx0aTFx4JgaAB2jQdWkvXqFlKl3j9tSWzaZjcxoC2YYYFJZgBamxpb5cV5/Pk5rj14SNdINAJlLIYYIxIslgRBKCSbnT6hMXhSYm3yqPcvk8EZeiGTJMFxnFEqi7WWbnWOcqNOqVVn59SZdBzWYrXBCglSYqRAS0toNJ9fv8vVu3VC8ym3H27w4x//mB+/+5uEToF//O9+zsZgQCQEsRAgFDhO6j3NowY5OTkcRgbGSROq065GMXvWe1qme2sqsEP5k8nKt1kVwtZaMIxqPVMJhMwYs1hpR1W7u/MLJ7eRvbaXPMz4ByiVjM0eZQYugDYxo9rFYWL48PBS6Zg9xrAfckahnUVgpTr09qa2vUsa56s2BoUQSPFkx5TzzWX8+RzfVGUYY4baeJJqUMBPLEpbPBRWyPSz7ihwFcJzkIGHloIIg5agEQRmAAp6JgAcsA7WOsRaEoaGMIHEKBIrSaxE2zQ0nBmASimKxSJLy8usvHyRhqt4/94adVEgIsE1A4SJGDhghGAgXNooHvYjQikRWiMmcv9GOYBGIN10uo3jmMFgQLlcxnXdkQHYqc6xChQb9dT75zgYbTCJRjpp+NkoSSShZzTNeof5YsCrF1/izW+/yW9+510KvkeoDSiZ3qAbi2Ml2lhspKfayuXk5LzYHKIX8O5Yr5h6Nn1l3Ov3ScScJ7dxHNubtZe9Od797BduyafinJy9cR0H10mLOqYLxtJwbiainBlZURSRJAklJ/MABkx++7TWJBNh3yRJF62nvf0AQRCwsLBAUCzwoL7D2sMNEjNW5lSAMOlgLIpYC+5vbBHMzxNvdPdSzjwQ3eocAOUZHUEmn5FS8MrFi5QqFc6fP8+JpRWEsQSeR8GMbwpHkk0WbB77zcnJmeDQMjC7vWqHWe/rwtdnpDk530wCz0U5atqrLdL/KTVuozYYDEiSJK0UFoKizCqAp82wyZy/bNmrGMPzPIIgwPd9tLXcXbvPnYf3idEYQA8XaUEYBTgk0mW7N2A7jCjMzTE5g4yVCh49xt3FGL1yBSMkThLj93swDB3vVeCmlMMrly5xavUEpaCA0BahDb5yiSYla3JycnJmcOgqYEj7amZMTtBTMi8C0qkyZXd17n7yLpPrZO+zIs3X2et9R+kYsl8lnJByTyPQCiZC0vuPYbLbBxM9N7/qEO1ezDpfOTnPiizVwlqLkgrlplWwUkqsMThKIYVESDFSB8j+zXLqXBvjCoOx0E0clDvefvb9zHLwslw/sSth1XEcSqUSnucRSfj0yuf04hCrAqxO0KTeNGkFAgeLQwysd7tcefCA5YsnEVJM5RNaa5HGIEX6b7Zf13VH86eUEmMt3UqFSqtJsbFDo1RGRzFy6OkUQuD7Pv1+H0cpHG0QSEwYQ6IpuD5xGGN1/h3Oycl5PAf2AE4aT5NK/DOXGevv3sas9xz2fbP2c9D1pt434xiPMoZMIeeontOnzUHOR07Os2D0GRTgDIsjEp2M7TPB0HDyRnm5nU4HSGVSCvQB6BtvqgIYpj2A+938ZZ1AlFIk1vCrjz8mwqa5zTKtKzEibWcpcQAHbSUhkg+uXqMwt5B22BBi176mNVCzsQghkFKN5stOpQakYeB0rb1vLAWgLEhtUMbiWFAm9QLu1aEnJycnZzdPvxOIfXQ52gQ16iH16PKseaIx7FkNMn529/b22tfz50QcY3f9mz3+Oh9TzjPHdVJvoE4SJj8cQkh838NRaRSiUCgwGAzodrsURSrV0jWPZuFNev728nKnsn4CRznDHEOHO/fvc+P2HSKTjOUOhjqAEkXasE6CVCRIvrx3n24U4jjuyADcvY/J8WithwbnuACtk+UBtpuPPUfSjhdMmi8tEEhrcxMwJyfnsRyiCGTMZMgQ2DsEjMU1Ltm0Z9LyYQA0JpV3yW74Z4goT3kCrQC8sX1hzEQ5isbIcYj1IOHgydcOKowqbHrXnR2hMWPLZSQjs+fMqyZm//F5EBjUZEgZOVpfW0YdUR5lbxmH/Tx5B65Ylo+/J0i9Enu/lnkmxu/NjimV37B7ViFaJqV3ngfkxL3R4cWHYLLK/FGev+N9VkylRuzyio0qZ7VGSQgcTY8Y5YB0FXgeOC6uUDgWdD+k3WziCYV1PMqkBmA7cUmSBKXUyOCbzPmbXBIsVincBDw8fMq4zjwUlvjZz96jbjwSmyCswVoDSKx1iRAYEqCffp8ltGPJ+1du89sr83Qb65SUhxnEmIImkg6uHs81QgiiKMJ1XVzXw7qpPmGnOvYA6jjG2rTVnZQS3/dxXTcNT0fx8JxZrJJYRxJKi1QQS9DGMDHl5uTk5DzCoQ3AvcKde/5tQSHHP51TBpvYc/39WroBSOuk27Ng7MQPiLCICTfa7o4hk9ub9dpBc/OkHe7VgjAThkHmHXiETC1xtCMmf/iEyQzhab+oQWZuiT0wU9sYbWuG8XfQYztwGNimEjaz3HZi4qRMXOlUy3HmMT1fjM+FxR65fHKWMf3i6rBNntfdjHL+sCgJjtBYE6VpFFKAlAhHoYRAWkjiBJmFPbWh5KbFD+1kOrfuEU2+KX2+9EZLSoeiV2auuoTyStQHmo+u36UeJiQWlNWI0fdIDq+gQQqTjs1CP9J8eu0Wv/vSRcz6DhJFksRYY7HCIq1JcwGHY8rGqJQc3XilhSACN4lx+z3CcmnkuczyJCcFoQ0GhMAoQSIhlhYzSmv8mnzZcnJyvhKeegg4C0tkj49je3s9fpaIGY/3X2PSQkwfi12vPGpD7v/q88g38ZiOxuPPRM5sPJXerCVJMuXGEkKMwr/THTUsJZn29O1ob2TgOY6zv/d7GEJ1lcLzfcrzc2hHcb++yWfXrmKGOXV2GFYdfWuz5N7hc1lP4au3brHd66KKJWIrsEikVUijDjRfWanolisAVA4QBh4zLm7LP2Y5OTkH4cAGoJRytOzmIEUhUsqhSHAq1yDk7PfMYvf7su2JGdvbbzkOxmM4uCk6ve98ps7J2YtyuTj24ik59b3Nqmyzfr7WWnxinGEFcE+7U16+LP8ve+9kFEAJgTKpEac8l0haSicW+eWnH9Hod0c9eGd50bOCDmPTba83m1y+cxenWmNgAeGCUYhYgmEqHJ31JU5Dwox0DUeFIO3myE+avR+YmoetTV8zWoO1SDHdGSifYXJycmZx4BBwJv3yuC4cez3O/hZCDMWd5VS+2+737CXVsvv50QSIRQqZxh3F48Odk/mKTyJ5MjUe0ny+gyTcPHJ8Rx5BTs43l/n5uZG+n1JpyzSkRAqZdseQZmQEaq2pyjT/r2e8dIYZ3iTGcYwxhjiOR0ZgZggKIVBIMBYlJF4QUJir0LExf/XFZSKdto5jn5vGcVVumrvYR/OrK1f5zqtvElqFYwUyASsFVhiMtKOOJ1lun5ISIdLqY6310AC8S6XVGkXKkyRhMBjg+/6ohVyW0pIdFxMdSNRwfhRCkjf/yMnJ2YunXwWck5OTc0iCICCKYwSPVtPuRVGMO4A4jjNaYHeoeBoBeEpRKZWYX1zAKQbc2ljj05u3Merw/jPhenx2+x5rjSZOsYRFIaxCmumu6ZknMF2mb0QnPYBHqeJQSuF7HkLIIxYw5eTkvAgc3AC0AqxAIMfyB0xPavsxeac8UrKyuxbkofP6pjLp7NFyro4tLPscx1uOX+dvj+tnRS7pknMsBJ6HiRMsFm1TA1AwlDzhUU9/aegB7NvgkXSV/QxAhEC6ikKxSK1axXF9PvniCh09INTmUO3TrIBeFLLd7XD15k2KlUqaK4hNC0iGVblp2NaitSFJDFobLAIrBBZBu1zFCIEXR3iDAdaK4XrTc5wZGnd2eD6ssUgEjlS4not8juejnJycr54Dh4CFCIaPLK7jje4rrY0xNh4+nh0ezl5PtyCGFcIpxuhs0wg5DKfusb1Z8i7ps+O/xxOfxdiQWRbJrPFNVRjvs87uLiNywsh63jpqHIdEzBRWsbfMiQGSw28vJ2eIsFBTAV5i2Q5DXCkwAgINBS1wpCQMw6m8vswD2EqcUepIHMejitmsWwhMf4e1siRFF79couiWCBOXTz+/xVZoMGLvjkD7oQX0peGz2zf59956C+FoBBGaBBN76QSXHiWOo9CJYNDXFIoeSEmCJhKGbqlCpdOiUG/SdUtkqlnaVRitSLQgArSCSMf0+33CXo+l+Xm6Roy8n0KIPASck5OzJwfvBJL57mxWCzf21gkO3tWDCR2waU2wvbtmPLbbh5j2/mVjEdnzs45nj4KQrHPHYdZ55Awdu6ftyXk6xTCzqlyfv+PP+fqhhi3grDEINcxns+myu6sHWAojEWh/tI3JG8bJkOtkrrEQAtdz8UsFVOCx0W7x+dWrWGOO6MkWSNfj5v37NAZ98D0irUlThDNZlul5b6QkKsbfoVZlDoBqpzV+nwWdaEDgqLRXMkKQ6FRDUCd6VLXse96BND1zcnJeXJ75DJGbBzk5OY9DSjmqlt1daLbbAPRtiBIWbQU97ewZ7p3VCk4Avufhl4okvsOVW9fZbO4grUCqWULe+yGII0O92+fXX35J4nqE2iKMxJqDi863ymkeYLXdeOTYAZSjplrOZUuSJCDA8/x9q5dzcnJyDhwCngqbjJKyUxkWS1ZxNm3e7Q4tZpORsgItzNRz0+vsXfk7KxycSsM8WjmMIK0Q3tVx4JH37XOsB52wjxshxyHlr2oMOTlfFVmF66R4e8aklIq1lmDYA7hrvFF3nmzu2N0CLjMAsxxBazTVShW/WqKhB/zlZx/TM2moeChdfriBG4kxhoEDf/HpZd59+SWk8lOZFmkxwkzJXWmtJzqCuDiOQxRFtDMPYLsBmdTMsMJXSvlIcUgURQwGg+EcKXB9H601SkmSJL/tzsnJeZRD5ACOjZHdXRIEY1HUyfZqe3biGK43uc7ktiddhLO6jDzSMSQLRg/fPnl3zx77ydivMpAZ+3pWzDqmnJwXATH+0j4Sis2+CtlNYUGmBmAn8SbeMy33NKkLOPm6qxx818UoSd+xfH7/NuHQiDp0hsSoYsQhspZ7jQYdC1UvwE8MnaQ7maA8pVOYGXfZHNUu1zBC4McRfjQg9Auj96aePabOSyZEnSRJqikoVapNOLMdYU5OzovO8YWA7cS/B7VV7PTjJ75PPcoYjptdx/TUxmJnLM89hxv0Xu/6WhxmzpDZH9QDfQIec7G11hRFZgC6hx6dGmoLKt/lwc4WNx7cQwuLFaReu0OQ6hgohHSJkWx1u1y+eZOgWMFqgZQHN8aMUnSLZWA6DLwfURQRRVHq3VQSrJiKqAD5lycnJ2fEgQ1AhUAhcIRM1fOHlbxSKFJHooPAQQgXIVykmJ6MJ7tmSCFQltHiWDFcO932QQoUxrIyY3mIRxZDWq06XLK9pONUe3YWEWIobzOSvVHTy8Q64+4mQ2Gc4X6VHZ8vte9xCNJK2uFi5XjhUe/n+LzIGcuj3pKnxUEKSay1GGuGXRIMYEAM/51a9h50Jm+hrUkXLHpirZyvC48agJmAiZn4yIrh98gRAl8qTBgjGX6OsFgJRgqM0aMuGkmSjFrAtRJnqshjMl9wUvx5lIqiFK7r4AcOTrHEB59/yWa7jzWgEg3aHPKm1IJMQA4wNmGQWC5fvQ6FgHbcGxWzTHois7B0VtGcCl+n80yrPAdAudUYeQmzNBilFEJOby+rdFZWMFcqYwUkwpIMpzN1HDfZOTk53xgOHAJWmXyBTTt52FFo0hlJG1gLMsvtwwIRI5GWybw6bUd6XuPqt3QtM9EhJJu0dzNVVcwuORY7bU6ICbmZySJka/XQQmSqswiA1lm13vQ6oy2LcYh7ZACZyU4gaY10ugk70uva6zgE7nAMYCf9ITYh0294tDvKxFimMMDhvBZHYbfhN+s6AaMjT3/yZxt7szBYdB7+/pqz93W3Yni/k904CYEjBJ5UFJSLCWOc9MuGsRYjBValVa9xHBNFETqJKanUAGzHLlrrqXkhiiLiOB61XANGN25KKQoFn+p8lVBb3vvkKr0oHe3wW8nEVPB4BFji4W2KwljF7QfrrDV2WKp6JEmU5kzvqk6e7GiiVFrckSQJrUqN0+t3qbYbU0beZDcQeDSULIGl2nxaITw8v0Knk722YHIrMCcnh0PJwEzPg2Lmq5ksy8G2d1ziIXuMYMZeDrq3w6/zZGtMPncQjvPsPWOOckmOehlzng8Ocu2Ghpvv+DhKHejyFlWMFJBYQd+MQ6yZ9y8zrLLH2d/ZDVyhWKRQqrDVaHH1xi0MCoNzZA9zJlWTevFdtjptfn3lC5xq5dC3ZmMpmOZj3zvpSWQkA5N/QXJycmZz6BzAKd08Jr1ju984kci9zzb2WG3vfe23zozXnpfp7zmUBvx6kZ+/bz5ifJld191XgmXy+14ehn87iTtVHDIZ/p2sAjbGEIYhURShlKJYrmCkw8Z2g3q7i8ZBo9BIDhsAxoKwAmUEWAeDQ2gVn9+5Sxcww/DzbiWC3Ut2fJ1yFQsEUYgfDsa7sak/fRzNsFMGoLUWL+sEknvPc3JyZnDgELDrTuf0ZRW70ophfldKJhEjGMosDJ/fLQkzq6OGlJPh5T3CRhO5M5PsDguPxmOfTbbYXmHRjOdBVubrihBpHuWsz1HON4Qs1GoZt3E7wKUuDlvAteI0bDopsZJJwIRhONVPWGuN67okSYLnB+AE/OJXH9Lux2gcbJaneoSvqUQgbJpnbHGIBdzc3GI7jlnA4iFIkgTXdUefZWstYRjieR6u66KUSkPXQtIplqn0OlQ7TQYLKxhjiON4eJ7USOomCyV3ez3KYUixUBzmOevcBszJydmTg4eA9+iaMc6T26Nzx/DlWYUC+xUQ7NcJZL9t7dc95GkzuygiPRFfxZi+GYzPX04OMBXWrTipZl8rTu9lD3JzJUTaKi0IAlwvoBdpvvjyOgNtscIBkXrvrJCH6gUMadRDpeVyIFJvYiOK+avLn1OsVDHG4DjTYtWTmoC754mRIPQBwsAAWIuSEs9PQ8C58ZeTkzOLJ5KB2TcNyzLM8mYo8TJ+vH9Mb9Y6T8As5ZEZ43mssTFaRzy6zeNm17jFjOe/PjIwOTkHY1YEYLLzRcVJ+5C3DyIBY21axDY0AP0gwC+VuLO2wfXb9xBCMb5zldgjJJqm5V/jinzhOITacuX2bRKbFjSNDMBhKDe7UU41/GYYgO1HDcA95wJAKYnvuii5u5dxPkHk5OSMOVInEBhPzsJopBlX/mYVZlYIrPUn6l/HlaLCaoyN9t6RBWmy6jaDyR4LS2SjQ+eDyRlznrESw/hHQ4qJUyHjtEqY3SFbAZPCqnYYs0q3yJNOsGNpm6HXa1YIbMaurDhahd+scPzzyNdprDmHJY0cKEelIdqwPfKKTXbBGEmmYCkNPYDt2J2qhgXG0inWkFiD0hZXDyvSjUVVizSCAv/qvSvc6ScYJRBJmObhHakMRJBg0VgsIUJE2ESgkVy5t82NZosLpSIm0RAOENJi/VTzTxuFJ7xRqNpxHIwxNCc8gNk50FqnVcDaIGODTAzCJhDFECUw6FF1Na61SONghAISjNDDaSP3pufk5BwxBDylgze65310Sf+fKuJhs8fp3bFg75CtsBPrTz0+2gHOKh5NvYupfp5ADvNl1FAHcL9w7oRO4OjvJy9JnQwhZxH0PZX+Mo3D3c8fw36fd75OY805OgIxkkvZ3bkje2ytpTSsAI6NYDC6aRwXU4w9hcObz2GFricdSsUipVoVgoAPrt5gYCAxCWKi/OPQ32qR3vgaAUIYBBpsgkXQjzWXb96EYoFBnCCsGHokU43M3a3qss94s1TBAoVogBeF410JMaw4tghrsdpgEo1NNFbH+Ao8IYb7yVJyshLlnJycnOPsBJKTk5PzDCk7Q/2/xGW3qTZtACZTuXBCCHzfJwgCdnZ2+PLLL9FGz1QtOA6shY+/vEEfGFjQQgESYV2EUehkXK08iVYO3UIJgFq3NXP704LSqZ6g7x44wJOTk/MCcmgP4HTXDDFSrldKPdJRI/MSTnbNyJ4X8tHtTb9v12tiovPGrvfNGuvu5ZH3ycd7lPbbxuSx7tZ6GY/76+Wtep69bJNit3kl9TeMoec7qwZOu8eMxdazOWbSsMs6gDQjZxT2Tfvk2lEFcLZYLFonmOG6fpCKKX/22Wfs7OwghXyqn3kL3K83ub6xBUERjYPRChuBjeyou0kURaNQb0YWBq51UgMwM/Z29zjOno/jZDgvO7kMTE5OzkwObABOtT6bMOqyiXnSCEwXgZzx/OMMur329TiDMWO3wbbbAJ1449QYZrHffnYf057rfI3EWPc9X88Jkz94Od80suSMsbTJpAEopZxqiVYedgBpxWqcXyzGEjCZQaR12qXHGDvy/lXKFVzX5VfvvUe/3x8ZVE/v0AQ94P0r15ClCrGVWKOwscDEFmuYCgFPznPtoSB0rducCm9P3hBlZNuQcnhT+hx+h3Nycp4P8hBwTk7O15KKm1YAZxIw+5EZVZ7nUSqXqdVq1Ot1Pr9y/WkPM90/EAn49MZtWmGEkQprFdKqUdHbLHZ7AB+HlKnB7Cj5tYtA5OTkPDsO7gHMSjrEsDBjWIwgYM+KVDEWQxgWNIxqW5ET5QtZgUhakJHJMMwYgwVhGO/bDJdDHPB4fLNfOW7P12T+tbTjx+KI0i1ZXndW9TtZFDy5r8PuZ3dHgv1e++Z54Czpmdy9HFVfZ7/tHXALT3y+9xnD81IMYCc+s1iktThKgYDEGowAbe3w8y5GZ1BiKKk0X64VqUfOUeYdS5+zCCxKgON6uEERr1Tjyq0HNNq99Ao/g8+zFZZGt8P9hw/xggAjDEYajEzlYHZ79LJ5qF2uAlAI+7hxNHp+0us59VkxBiUsrqOGBqDgqHXNOTk531wOnCUcyKH8iYWEBDv8AQmtxTCeiCZlOlzDSAZGCIMZqqoa4Qx1ttINahtP7MnAHl0zhQVlIOsWYCaE+o0QaPn4bhGTuW2W6S4hUzI3VmLtdFXhkRmOe7yFcVjGAPFhO5UIQE13WMmGJxmeoz3Q8mCmx35hsFndTZ4lT1cGJmHvuxl7BBvQDrd3dI7F0BYzjuk5QVhQFqRJjT+JZK5cBinpRiFSSmId46oCxhEksQYsZRUjBIRa0E8AzEjfTwhBFEWEYZjm/xmDKwErkY5LobpCyxb5009vUdfDjhzPxFGWhrA/+ewzvvN7q4QqxIoI6UhU7BDHMdZalFKUy2UcJ30ucVy6hRKlfpdap0m7XJkKWWfSONZa4jgm7HWg6FL0MpkrQSpKrdOq4WdxqDk5Oc89By8CGf43+TdTzwyf31VAMFrPjteZfGVanOVxY5h+l9j1yuM8d48v5hCjlJnjLoSYlJ8Rj5zNw23JZkakEHvqah/urH59eCYFKntpBj3HBtTjsXsf03Pw4XjcEDKRZDu6VRyTVQC3YgelnFGO4CwMFqEkjucRlMvExvLJlS9InqGWpDXpPHjz/gM22k1kEBDGCZNnYTK/b/KY9uoIMnlzMMqRHErKOCrVUswjwDk5ObM4HjfOASaZscjxE21maluP3V4++32j+OaGn3NSHp0jZl3rskqjBh3tjQrN9vu+GwFWSVTgEZSL3Lx/l+1G/UjC6UdFWIlGsd3t8umt21i/QKFYAS0nBPPZ0wBsDsPAtWFHkN03QlnhljWpAamUwnOdfA7MycmZyZE6gUz+CDvSIbs3z6QXHrs+Bm3snq9hx5Nh1gEgRYBQTPoBsjGYXXlas7qW5IbDk3GUsK+cuMcwwkz90B2FvPvHNxdHKjAG3wsIw3A0n3ieNyWUrLWm7KcewE7ijT6XWQrK7lw6qRRGaoTv4pWKaAmfX79KK+wTGjjENPhE2ESQWEHHWn519Ro/+cF30d0OKrb0wwGe5+F56fHEcTxSWdBa0yylBmC10xx3AplQYsgMRqUUcRzjA0IeMO8jJyfnheTAM9+eiccAdhjKFLty7CbU7Ce3ka2z57aHImBZcHRyGzbLXRkmPE9vzz6yrd1jnfw75/AcxZMwdS3yX6KcxzGRfjHp7Z2UdkkNPKiocQ/g3Z/NJElG+XQjHIn0XNxiAVXw+eiLzzBSYI9a43NYrACrMEIQIrlbr3N/p8EJ4SFMgkCMwr/GGKIomjLy2tV5AIphHyeOEK47Nd9OaiVKla5TLpcZCSvm5OTk7OL4MvntxL9HnFAPNE1NJr09q8n7OWOUlrZPFfZzjZ2x5Lxg7HXhH/0gZF49nQwFjjEU1LAHcDJ9D5sVQkTRdK9xoSSO7xKUinT6fW7cvUNsdscOjoO9P9xpvmNaXJZISTdJ+OWHv6ZSmQcrUU7qxcuKOaIoIhker+M4WD+gGxQBqLYbj+w1M/4cx8FRDo5SLMzPDw/+WA8wJyfnG8KBPYBKqdHj6R6dIIeVrMbKCW+eIJ5I4M5EXMXwNTW63U+reNMb1XSaNMPXsnUAhJAYMx7D5Kyd3gmPK4cP6umTE5XDB11nLw/js0bYiTndjh2gR5nnn21O3X73G3lo98UilagRGLR00cLBEQJfJFT9GJUMiGMNRmOjBCNirKtRymXOS7/rAy2JrUpF54dh4DiOR90/Mi+gsYZQG1ZKRYSjuHnvAd3YEgkfLSXYR1UHjn5MmfTOrlcE2KFmlTQWheTqjbv0f7dITxVQURc1FJRPkgQhBGEYjoxfgHZljtKgR7lZp7l8Etd18Txv9P5srtRRgu0nnF5dwnMtvWSAtWIotZV/z3JyclIO7AHc3dFjdMcpJIp0cRCjx4q0DdqeHUSGmoKpnmC6jhTp85Mt5Ka7cGTagWIYQh6rDO7uLHIgxERbugOuM6sryLMm00HcvUwZhofg2RRXDK+bHWtAjpfcRfGikX5zLQiNFQIrFBqBEoaKD8okkGhIDDZOH6dSMVBxsvCvN/oeZt/JLG8wyxXMtAB9z0U5EjcocP3OXdqDhNBIrHL3GeVRGMpYiT0WFYOIkRikFXR7ER/fvIuaW8RMfAez8cdxPOoNbK2lVUkrgSvtBtba1NvnOKPXMwPQxhaVWFbnq3jKIEQyHEP+XcvJyRlzLCHgaXGXg00xsyVdDrLm4df6prCXQslzoupxQJ4zLZKc54Phx0Apge86+xYcFWUIQCeZNt6yIpEkSaaKhYQQ1IplPNcFR/H5retohiLRyVPwiM36SA8dhKlCpCW0mve/+ARR8NIw74Txlx1HFgZWSo0KQWrd/TuCCGsR2rA0N0fB9cb647nzLycnZ4JDG4AH1WF7RA/wII8PqI03lfz8lI2I4/D0HamA4tDbFuSiXzlfZ6w1SCHwPe+Rft6THq5SZgAab7RuVvWbec0mvdpKSgLp4Hs+najPzYcPEK6LRCIO15zlCQ9wvC8NRFJwc3ON7X5nNITM6MsMwUxVQUo50gIsDXo4cTQVjcnOgbWWJE5IBhELlSpLtSrKknZMskcRNM/JyfmmciQZmMm8lMnQYTYhQRrtk2mJ7iPr7/W3tUNX1j4TVCZ3sHsd85RntSfpPjFZpQeHyLkTWTO9/SVsRsawyLZ9qOF97Xi6nUByvkqMMeBIarUKdphXvDvMKaWkRGoA9m0wqhjWWhOGIYPBYOQ1i6IIpRSB71NUHp5yeLC1yVq9TjsCRWHkkXsWyImPqxbQQxN2WtzaWmPZVSRxNDJeM0aFHY6D9gN6foFi2KfaadJeOUUcx3ieRxiGY8PXGIg1xWKBc6dO89ndNYwFhi31cnJycuAwnUD28OhNGjd7PT7s+w46jsOu8yQcR/eJo2xDwLDZx/7rjLZ95NF9fXgmnUByviIEUjp4jmJ5eXnqRmZS4sRX4IlMBNofCUBnYdPJZbxlQaAcFubm+PVnl+kkMTq9Z0LxbMjyc+XQ4DQSQpPQMxGf3fiSQqEATFQ8a00URQwGA6IoGsnhZHmA5T0qgTM8x0Vqg68cXjp3Bk/ItN0eecJFTk7OmCcOAc+aUEYZXsPiBIkYPd7XXLFivLDr8TGy39YeaVE3Ct08Zgyz5E2+YV65qcbzB3Y57tYJOvzJeZadQPKuI0fh4Nd2/GnIUjksSgmk61GqzBEZQSIctHCwykE4HsJxCYbh34F10UIhlAQp0MYQJTGxTtBGY4wefYMlAke6aC24cvMeVqV6fOneZ41xvy/z0b7oYuJlA2k/dUdx9dZNdrpdlO8jlYMUAmsMcRwRRgOiOCQxCVZAcxgGrrQaaeRDptOjwQ63KYniBKkcbGJ461uvU3Bc5HB8uR5nTk5OxoFDwMFEtZwy4/BrLMSon2aWg5LhmfGUKIUctV1KZCoZkzEZypPSY5woo9EjiQaTyjUc0g6c1RVk+Nee6wjhIhiHnWyWPW0tafbOHljJbH/CcclMPD8cKfwqhhWSz3q/RyC97vmP5eHQ7P0F3fs8GitBKIQxOCS88tLL/OFPf5/auddR7TaytE3Y7xLphKKvcIplrFkDoEuAKgVEUYR0FXEc0ux3GURdtE4Q2qCswHcUJddDuxXu7Giu3GnQ12UiEnAGYC3CODNuSjWHr5yY/ZmZbDsnMnsx1mw2O1wOB7xUrRLEBieMwSYYaYhtTDOKMZGmVKpRrw49gK06fTlAVVyEdrFYjB8QOz5RUGAziSiECS/NL3NpYYEP1taIPQMa5LPsf5eTk/PccvAcwF0dObIuHIK9Q7np+8bTauYFTNcZetnEXrltk2tN3lUf/sd4r04kGbM9O2JYS5Edh2HsKDXsnaiYPbeXQzU3IsY8gWDhMyY3/o7CYXSI0u+M47iYOOHMiRP8T/7Of8RibY4vrlynLARlz2H55FlKvoON+lgd4Q/SCtidSNEa5vhJIUmEQyIEeqgjKmwqMwUSIRXB/Dx//uvPafQGJHhZVQTY/Qw8O+22exLEXn+m0QVjDZ/fus23fuvH0E9ItMXJNEq1xUYJxBrHCAon5uBjKPe7/LXyAy4nJ2i6PkYaHL9IsbJAsVwEByQu5aDIqy+9wuXtTUJhnmnv45ycnOeb4+sEkpOTk3NARgkW2lLyA/7wb/4BpaDAoNOjXCgipEOj3WOr3qLdC/FMxI/X/h3nBncBWIr7+IUq1imiVYFY+iQ4aKEwYvgvCi0UiVD0hObTm9cIrR1HFczx2XdPhIAHDzeJrSCxDtIN0DgYHKwWiEhABGUS3jq5gy2l1c+lZpvvVR6yUiuxevIC1bkTtDoxN+6s0Q5jCtU5/KDM26+/jWMl0khEXgWSk5Mz5FBC0LtFVzMFfjnMC8wStR9buEC67m5hZSnlVChmtxTE5Pumtpetu+t9ebFATs5zigVHObhCcPHsed598y069QatnTpxPyRKLMXqAt0wITbw3Tt/TLH9ENw0zWKuvcmbnRsUFk/iVpeQpXlKC6ugfLR0GCQWLV2s8nAKJTYHXa5trGE8B6vkaAziGJ18R8UA7cGABxvbBOUaoRHEVpIIRagFEQqjPJbLfcAiFtOWcGKzA8DZUoh0q4TaZac54N5mgy9u3+GLG7dotrpcPHOJ+WKNAgHim5eNkpOTc0QObABOdr8YG1xDQ2u3ETeUZpiJYKoLx+R6e+13r/dNh5155LWvultHTk7O/ggLrpT85tvvUC2USPohNkpI+iFCuuD4hFqgOg3Kva1Ux06Q5u3FmpOtWzQHUO8b1ht9guoi3//xT6jML6P8EjgBRnpUF1f45OYNHnZbdONwFI6VFtRz4AG0VtANE27efYBVLtIroIVDZCR9qei6Dk0p0NlsPRfAWhvefwBC4DoeCQGD2MGIIrglYscDLyAoVjl/9hI/+s4PcfRBlVZzcnJeBPIQcE5OzvFxiGJZAZSCAhfPnsd3XQLXQxqLJx2k8oiNIChXsVkfcm3g6jbc2Bnp9z3caVHvhrilGgurpwi14fVvv4XjBRgh8YIiRijeu/wFXa0xcpyn+Nz0oRHQC2Ou3bxNN0yQXgEjFDj/f/b+7MeSZN/zhT5m5sMaY44cKitr2vN05qG7T9PddEOjS0NfQEJCAonmAo+8NBL8AQgkkHjhAdTiASRekBB9W5cG1LfvpXX6nj5Dn73POfvU3rtq76rKqqycImOONftgZjyYm7uvFSsiIzIjZ/+WvHLFWu7mZubmZj/7Dd9fhFjpY7fWCG9eY7d9AymAaz04mMLnB8hcs5NsYolAxKioS399m87KOsNJQpaDIuTv/v7f5d3td1HIuefwBpMVNGjQ4Am4lAZwURvntH9i/u8Fnr9FjZwUT9iF1iPlztHgleW+Ivkt3zaOuretvS8GFoqMF4sR9a8HluV5PvuwVnD9+k1kEPDg0UOsFKQ6dz+bDGE1/W6HUbjKcbzhyIzBBUYguNP5gFmmyS20e6s8OB7w2d4RMxmzunUdIwOilT4Pjg/54sFjZhmoIHRsAtaAlZgXxgR4DoRAK8vO4IjDyZBMSqYqIN6+xke/9ft863f/LmsffpdHK9/kj7MPse+sQDeCRHPv7gbD3jdQnZhMamwoyHTOaDBmOJ5yNBqS2pwffP+7/E/++/8D3tncIpQStcAd+rqNtAYNGjw7LiwABkFQ+vjVhUEp5PzfZxylECglQp4v2NUzijzpPC+AvmzU6/2kur/uWGzrq9D/bwqMNa8x/+AlBEArMQhuffA+7dUeozQhxSDbMbkSmHxKP5JcW19hc2uLP//Of5eEqLzNr6J3+VfyO0ipGA7HDAZDjnLDsNVlb5LRWd1GRm261zb5yRe/ZPckweQBZpYj8gxhDZZXQwA0GHQIozzlF1/fIWmFXPvOt7n9m7+F6Vxjfxjw1cMpn9094l/vbfF/mvxd7nz0bQCSXx5zMDwmVxobGUyYIpUhtCHtTpeD0YBhOmaWDPmv/4O/w3/1b/+XaElJYK0jhrbPhWa1QYMGrwHebEmlQYMGLxDi4oeATqfLd7/7XdY3NsiyjDR1GT5ynROFEVrnHB0dMRmPOcwDhrILwB/d/Af85+t/QGIE0+kUpRRRHGMMZIlGqYjN7eu0+itMjeXPfvYLTEk+Vcse7pyHeRVyaFsDqbH8/IsvyIOA7uo6Uoak05TxYMTJ4THpdEYoA47HGb/68AcAbH7yMbnWGGMIwoBWu8Xq6ip5ljOZTBmNR0glGQwG3L17l7//d/8ev/2j32S120fh/CBfhUCYBg0avHhcOhXcnJm3/N/y85f9Xf5/yXXnG4df/iTdoEGDZ0NFsC1YX1/n3XffRUlFFEUlx6gxhjzPEULQ7/fpdru04oiN7ASAnWCNJEkQQhBFEVtbW9x+913ee+c2m6tbSBERxB1Wrl3nz372cx6enJCbV1fCEVaAERghuH9yzF9/eQfV6dJt99FpTjaZIXJNJBTZdMZap8e9D50GcP3RfdqTMe12mziOGY1GJEnCjRs3GI/HHBwc8OWXX5bp8la6Pf6n/+Q/Ynt9k1YQEQgIrBcEX90+atCgwdXjwkTQQRDMUbB4E5XWYMgBJ+QZY1CqMqv48+pmUWVBFcmJYCGDiDGw5Bpb/NSgQYPXF0IKwiCk1Wrxd//O3+EP/uBvEUjJjydTRG4YnwzodjrEKmRrawtw80M42CUwOblQ7GQKKUFrTRAEjEYjprMZqYU8z9neukZmBDZq8ycff8xY8IoTIAtsLtHAxMJPfvUZv/Wbu5wcjgiiFnqauAjpQNHt9ohlgNjYZv/6O2w9fsh39nb4dGMTYwwbhTY1SZKydCkl3W6XJE1dRpGoxX/zH/4H/F//7/83sJbE5FghSE3DEdOgwduEpzYBi5rp5LxggMXAEGdyAZZlEFmmFfRax6etaIMGDV4pCCFotVpcv3Gd27ffZXNzExUohIBWHBOEIWmasr+/T5qmWGtpnzwE4DheZ5qkaK1ptVq0Wi2EEORZTjJN6bZWGByPWdvc5rOv7/HF7mPSQGJfATPvmbACrAKh0EpxMJnyYO+A8XBKrGJuXb/Btc0tOlGLfJYwOh4wPD7m/kffAmD147+k1+vR7Xax1qK1RkqJtZYsy7h//z737t2j3+vRjmJ6cZtf+/4P+Fu/8zfAGIQxWN0Ifw0avG14Bh9Al0hdVH8CL9lQW88aZ+FCtalnr3omPoTFi18OscLl0tM3eNG4ADvKSy/x6XGxeqRZyng8ZmfnMX/0X/w7/vRP/pT9vX0mkwkWp8W7fv06rVartAJsFebfvWCVMHR5yYMgQGtNmqakaYLJDSIXqCBiPE34yccfg4pI82cwHbyArnWuLxKEBKlIrOXh7h6SgNl4yuhkQDKeMh2O6Lc7mDynHcXsf6fyAzRaE4RBaUofDAYYY2i1WnS7XbrdLu988AFCSB7ev08gJL/z679Jv9MlFOpqG9SgQYPXAhc3AZPhMm1qhND4bKlWCQQKrTUYi7QQSkUgJGmeldPKE8mhC0ghS4/kRSoMqSp51dTtwVYAqpamt9jNWnA8+6fvK4CgylTsJOHiT22No5x4gvw4Z6K2GutTTAnLRZPISynL+5hnsHFbXDdoWFrvZnp/dWDOMUmel5q2Pt7mx4pxuW1fOjSnB5/Lx+vhMwUZY7BJxs4Xd1FSEmhLol2u2jCOmaQJUgiU0bRbMWvJIQDD/nWMreaHyWRCkiTcvn0bbWFldY3DowOmoeVwOiFLNYGM0Ofm/D0LirP3yPlTlLccVhgsmUtPbCAdHJKkI6ZhxnR0SK/Tp7fSJ4xCtLXEcUwUx+x98BFZFBGPR2zs72B/8CMe7u2SpAntTptWq8XJyQnj8Zi9vT0+/+WntPtdVtfWsLOU99ev8dGNW3z81eeFBvAV1pI2aNDgynFxHkCry0MJgxK2OFzSdR9JJhEohOOZehquOHGGSVkUO+W6Odn/UC4y8vTnMyQfz0koC/Ny+fmCdZ0PivHV8OF0tdA6Yc+ZV6+WS88LgcuOZm5/dXDWc7qIiLJ0rAhDKQSeOq648udicdyf3gjleY7AmYDXVldR1m3EOnELJdx0pJSi0+vSXemTGc1oMmZ1sgfASfcaWZYxm80YDAZkWeb825KE8XjEJBmRkXM8GZBpDUYgzdO4kPh5RC05nkOnCoOwBmk00mge7dwnsSkiclRXWZ4xmU1Js5TRZEySpQyzlPsffAMA+cf/loePd1jf3KDd7SCFZDwek2UZxhiGwyE//cu/4vHuLkIINlfX6YYx72xeRwpZbJmbbWKDBm8TGhqYBg0avDB4X98gCFzU6nDI3t4eR0dHc9p+IQRxHDv+0TylnzgT8N0sQOd5SZYNThOYpin9fh8pJWurayAEaZa6+9kLqPNfISglOTg8IM8zsNBqt9je3mZlZQWA1dVVJpMJo9Go9AO8feczTk5OmE6nALQ7bTY2NojjmLW1NZRSDIdD7t27R57nHB0eMp1OsdaQ5RmBCnid+qhBgwbPjgsLgJ4E2h8+MENQI2U+R+tXZe44TfZSz+rxuoR7eLJetxC9rsS9DV4n+PH2Oo81gTP/ttttojBEG0OWZYzHY4IgoN1u0+l0GI/HpGmKlJKN9BgBTFSbnVGKUoqVlRW63S5RFLG6uooQgsPDQwaDIRYYj8aMx2PAkWu/Ttota2E0HjEcDlFKcXR4VEb2WmvnfCCnv/s3ANi6+yXX2m2iKCKOY44Oj3j48CHGGI6Pjzk5OcFaZzI/PDyg1WoB8M1vfIs4jN2Yej2m3gYNGlwRnloALP2R6mne5rJDzBc9lwlkQTgsr5HLI4FfVfjF2NqKuqZBg+cFv+l4nQVAYw1Gm1IrBZCmKXEcs7GxQRiGBIELZsgyRwzdG+wAsBeucuPGDbq9HoeHh/T7fUd6nOdEUUS73UJKt8m8f/8+o9EYayzmteozV//pdMadO3dIUqfFVErR7/cJw5CDgwOSJEEKyW6rzXBrG2kMG5/+nCzLEELQ7fXodDpFv7Rd0dbNWVIqgiCg1+1y7do14rj1TP7HDRo0eD3RmIAbNGjwYiEgDEO2t7dZX1+n1WoRhiGDwYAkSTDGEMcxWZY58uLxLgC7wQp7e3tMJxPW1tbIcxeIoZQqKamMseRa8/XXX7+2mzJjDJnOePDwAYPBAK01Dx8+RGtdEmRHUUTccsTPD775HQCu//IXBEFAGIasrqywsbFBEAT0+33W19fpdLsEQcDR0REPHj5kPHEa0k63jeV1EpIbNGhwFXgqAbA08db4+U6bfM9wWIcqbmPJdZeuyzNd/WyYD0S58tLnIzjKaIHncKsGDZ4TXEhUkf2jyD+rhKATx5gsZ3B8wsnRETrLGQ+HdFptJqMRcRixsbaO1Yat9BiAyeo7hEGALjKFDIdDhsOhIz22FiEkKlAEYcDO7mMMFlOwFbzyKOkSXIS4kgEPdh6BhHa3TavTwqARSoCE3OQcn5ygtWbn298DYOvnH5NnGUmSlsLxdDotzemz2YzByQmz2Yz1rU1uvncbpGCl2yN4bZxvGjRocFW4MA1MHZ5kVFmLRqJE8bdS1S6y2OXnWjuKmBoEAiWrTCCLv1+mDgDSWngJqZ7qvo5uB32VZhQfcUihyajxC9rstTKVN3i7YYUug+FjFdAKJO9tbGDHE2yS0g4i9CwhDEOy6YxWq8VkMHSExkKwPt0HYI8ucRASK+cr6ImOO50OcRxzMhkT9Tts37rJJEuYmRyNE6he9UB4XzeDJTGGBI01Gb/6+gvWVvvEcYywgu5auwziWOv10Vpz79Z75ErROzoguPc16qNvYtMcKSU3btwgSRIePXpEr9fDKEGaZ+yPB1z7xvvYXwreXb/Gl/IOxya/IHlVgwYN3gQ8uwn4jGCPZb9VJ7n/PbP27yUmcj9FA3O1pZ9zNGjwGkHMf7RGY4yufa6CWrrdLpsbG6z0+7TimDiKMMe7tHSCQfBQx4RBUJqNvV/xdDrl+PiYPM/p9fs8evSIncePa3zNr/a7U9bORdShggAVBCRZyt7BHgjI8ozReETcikGAVApjtPPJXllh5933ALj1xS/J85w8z4njmJWVFabTqTOn53mZXeXBwwfcvXOHlZUVOp028hXunwYNGjwfND6ArznKoIDXws7V4G2H1sZF+wuBxQkyYehyA3c6HbIsI45jVPH9RnIEwKi1hoxaJEnCaDQqCaA9wbwQgrBIIffpp5+W/m2vG3yktw+s++ruV+wfHJDnOUEQYIwhiiLCMEAqRavVwlrL3fc/AuDWF5+xurpKf6XPbDbjwYMHXL9+nffff99FTnc65cb75OTEZQh55x2aCaRBg7cPl4oC9tG6T4OSPoaL+2bPa9kaWXUZKioag30JZvAGDS6DIFCsr6/zrW99C2tcyrIgCFhbW2M4HCKEYGVlpYxm3UhdBpCj1ibXrl0jSRK01kwmEzqdDu12m3feeYft7W2EEKRpyuHR4Wsd0ODz+RptODo55ss7d8q25XnOaDSi2+0yOBmQpo4WZ/e7Li3c9S8+Q2lNHDual36/z/HxMQcHBxhjWFlZQSlFkiRkWcbh4SG3bt1ifXXjVVaSNmjQ4DngUlLVs2StmLv2EpNzKQA+1V3ffPiFrnS0b9DgFYYSqsxMIYSg3WqVOX09kfPBwQGTyYThcMhGEQByGK0zGAyI45jJZFLmuU3TlNFoxGw2o9/vAfDv//zfv7a0Jn6+85p9YeHLu1+Wwtt7771HHMdsb2+zsblBnudkWcbw3fcYd3sEWUr/05/T63ZZXV0ttIUha2trAMyKKGuvSV1dXWWlv0K71W6UgA0avGV4Dmq1q8iUvhD08FJmplehDqdxKh/96xgYvNiIi3SvXfj8SjySsxpin6qJrz6evVXaGoIgKDV82hj3byEAdrtd2q02QRAwHA7Zyo4B2A9X2dvbI0kSlFJEUcRoNEJKWZY1Hk+YTqecnAzw26Knb9N5x4uDsYadvcfs7++jtebBgwe0Wi0ePHhQkkPPZjM2Njd59K3vArDy078gy/PSV3Jra4tOp0MYhhXpvpS0Ws6kfnh4yPra+gttV4MGDV4+Lp4LWFqEcAcYhLQoJdz3UiOVKQ6LkAZLjhCUvizljrYkhBal+bKeCcSVZ4pDY8nL48XAFjlUNaCrf8vPxdJSM726TCCAle5AVp+tvPI1w0gX2bh4vPoChjznWI4yxTIgLWWeafWSgn9Ow5x5aMHSw74qVX8qLGvrJa62htWVVay1HB4fkeocjUUqxWQ8xuYaco01hm67xXrqUsA9Fk67l+U5q+vrZNaQGs0kmTEYj5gkCdduv8Pdxw85mgxJ8gxT2A3O725L+X5f+Hhx2sVMWGZa88X9u4xmU/eeW4uSijzP6fedr9/9+/e58+77AGz9/K/Z291lb2+PVqvF/v4+jx8/Js9zlFIYawiDEGmhF7f54MYtfv83fos4joF5hoUGDRq8ubgwDYyQFqmcmVEbjQSsAiUsWhiM1UgMCIvOtUtuLoJS4NNaF+mGCgGwzKJhK7MHFoFBFnQqxuZY6wQ/y4tMVWRwAh/OXC2KCX9hUjw9SRaCzKm582oXjDod4Jzg90oLFl7ykUvquXyxmWNZ9OrOUvArMjxcfUUvieVpxizu2bzaz+SycJu/098VuEBblZAEKmB7a5vf/e3f5ovPv2A4GKCNcZkqkozJZMZoMGTTDFEYMhkyiVfZaksmsxmHgxNW11aZzWZ0el2klI4XMJnxq3tfoZVA527OeLLLSiEAvqLPyUrJDM3n9+7y67/1m2hj6LQ7DE8GJNMZeeqCZowxPPrWd7DA+u4ONwQ8Fi5Cen19nSiKEFISzMac3B+RJTN0mqG0ZXIy4v3r79ButZimyTOzMzRo0OD1wNObgM9b3J51/qiX/dIX0YuKGItULc+v4l4WKs2/L72PLoPLUds0ZDivIJ7hYVgLa2trZHnGn/zpn/Lll1+SJgl5ntNutYjiuAhgEGwU5t/j1ia9ft/5Dg4GTKcTdnZ2uHbtGsYYtNb0+j1GoxE/+9nP3EvxJgySog3GWHZ2d3jw4AHHJ8dMZ1Nacczq2ipKqTJ1ntraZv/WbXfpn/xbRqMR/X6f6XRKEAR0Oh2M1qyvOdPw3t4eRms2NzcZDge0Wm1MQbLdCIENGrz5uLAAWJ8QqmCOq6uIpzLx4R6NCaLBIuq5cF+n4TFf77OPtwFSCvr9vqMvMYZer8vKygoUPnx5ljGdTlhZWWFt4gigj9ubgGMSiOO4jHz94osvynJvXL9BHEccHR2RZukzBay9StDGuKjdWcLdu3fRuS5zJ0dRzLe+9S1arVaZS3mviAa+8atPy2CPtbU1Wq0WQaAYDodYrAu8yTWHR0dMp1M2N7ccH+BTsjw0aNDg9cMlfABlSQUThmGZyP1ZjXB1XzptNGKJz6A3IzdoYKwpj9dJZKr8Rc8/3nQEYcjq6iq3b98uhF5Bt9tle3sLrV16szzLActG6jgAH+gWWmvH+ycFN27cQGtdXLdNGIbcu3+P4XBYZhV6UyiRBJCmKVpr7n59FyEE+3v7NWx4jwAAdl9JREFUtFot1lZXGY/HdLtdhBDs7e1xv8gLvP3ZJ4hCmxeGIdPplNlsxu3bt11+4F4PqSTWGMLQeQL1+ysvsaUNGjR40Xjlpao3ZSffoEED5wPY63X56quvyLUmDAOEkEghuXHjJpubm6hAMR6P2SwoYMYrN5nNZiil0FqTZVlBXozLcKE1nXaHh48ekWWZ8yV8Y4Tpau4bDAY8fPiQXr/H4dERg4HjARwOh8xmM4bDIfe2tkniFvF0wvrDe2RZVtLujIYjgjDk+vXrIASrq6ukWYa1ls3NTW7demeOhqZBgwZvNp5ZABQL/wIlW8JSse2JE8tlJx6fBcOCtUXAQO3zc8LVCKUFZcgLnmzfNrPjS4Vt/BbrcNksLFIIxsMhRhuyLC0Inl0Ks9lshkwnrJgJACftDVpxzHAwwGpDmmbs7+/T6/UYDodMJu68r776as5/7U0a3xaYJTMe7jwkSRImkwm7+/scHB5ycHjE0fEJJycDklzzsMgK8u6Xn5ccizdu3CCKI0yumU2nbGxsMJ1OWVtbRSrlgkX6q/Q7XTd3Fpla3iAOowYNGizgmQXAAElsJRGKyEoCDYFxR33SqNPBnLc793ksLzN5Syyh1YRWE5i8+mw14opnLp8NxbfnWaFfgvlvnsLmTdGUvFoQVO9BYCBCEiOJkKi3WRyUwvm0pSntuMVKq01bhfS7XWZZSmJy+lvrvBfMABgHXTa2r7Heb7O50iEOROlHCG4sdzodAP7kT/+EPM9LM/DrDh9JnktHPpNozV9/8nNOpmNEHNK5dp1gdR3R6tLurdFf2aTbXuP+Nxwf4I3PPuH4+Jh+v48Qgl63x/HePiozDA6OCKQiarf5nf/y3+MbH37E33jnm1wjpqVChJJlLI1ohL8GDd5IXIkGUIrTrG6LWo+6Kfe8+eSyu3ZforAWwcJhr1a4qaemuzI0k+sbiYLwpjjE3PEWi3/cuHadNE1dJo84xuQaVZA59/t9VtfXSNKU/mQXgJPWOkoKTo4OybOULE2YTMalaTPLMrrdLgeHh4zH43lWmjfAdaRUwAkwWIbjEY92dgijiH/0H/5jNq9dp9XpsraxCUgGJ0N2v/NDADYefI0YDnjw4AFCCB4/fsz25hZWG3Se0263GY1GnJwcM51OCXNLN4oxOne0PFQcnA0aNHjz8Mr7ADZo0ODNwdr6Ou12mzAMAWi328xms1IbnWUZrXab/thFAA86W+zs7DAYDNjb20MIwerqGkopfvCDH2CtZXV1lXv3nL/bm47ZLOHLL79iOpvxb//w33J0dIRSisPDQ8Cyvr5Osr7F8cYW0hjee/g1SZH+zWdg8RpSn0rv//0f/8d8eecOFsvGxgZBEFzAVadBgwavO55KAPRmQymff4CGXxgWNYOVGdM+1WRVN4POfTYW6yNNF+67eM3rgkWT7+tU90VUWWPebk3aawNrscaU2X+GwyGttovq1dqUzALr6+tMJhOOjo6w1nCdMQCD9gaz2YzZbFamNtM6Zzgc8sknn2CtJQxDHj56+Fa4MwgBj/d2GI/H7O/vE0URJycntFttkiRld3eX6WxWpoXb/vTnDAYDtNZsbGyglOL999/nG9/4BltbW4zHY7TWTCYTWnGLTruDkqpR+zVo8BbgqQRAL0BIcTV+cE+6z+Jn/7c7zDNvVutBEU7ws4UguCTDgz/vNROk3oTAjzJd4HMedw2uFm7MWaRw+WevbV+j0+mglIv2FUI4k3CrxQcffMCtd26xMj0AYNS7xtbWFj/84Q/p9XpsbGxw/fp1vvnNb5bC48HBAffufc3bMCSMMQxOBtz96ivG4zGHh4dMp1N2dh4xnU7pdrooKXnw4bcAuH33C9ZWV2m1WuUxm804Pj7m4cOHLjWcMYRhiNaaD97/AHBze4MGDd5sNG95gwYNni+EKMyO1qUbm07QWhOEAUop1tfXsdYyHo/5/PPPefTJXxKZDCMkduMWWmvu37/Pzs4ON2/dot1q88UXX9DpdPjud79LEATs7x+QvyHBH2dBComxlizP+NXnv8JoZ9b1mtH19XXanTZaa/Y//BY6COgcH9F6eN9lS+n1yrLW19fpdDpkWVaahZM04Z133iGKIt4KabpBg7ccF88FXEwIdZ4oi3VZc4XF1A4tDEYYLLrwIjYgiiTqwiCsRmKRGCzGafEoHOetwFonl0oLVRxH8cfCvOSdpE0ZYCLwFMFWiHoBp+B1YfXcuu7fwoXfFkTXdv78y6BiUfB1qizW59dskXvB1H57uVimjb3AVe5YGlK4/Hp76tfnlwHE+vpd+gbLF0rnuF8+9bkmPp2h0p4Z0f6iRoTjbZ5vyxOfvRAocGZgIdhc32A6mpAnKZ0gQgVBGbkbBIEz6R7cA2AQr/F4f4+9vQOEkGysb/Hnf/bnDPIcESge7+/RWekzmE0YzCZowdV2hj2LwOc8nqurRZX52geCwDRPebCzw/7uLrEKWeuvkGhDrjWzNCFJErZu3WD3/W9w84tfcuurL/j83fdJc83KygpSCkajMShJ1GrR6nRoddokwzE6z+mFLY5Gw6q1jSzYoMEbiQsLgJ76xFqLlLLw4dHkWLQ05CpHo8ltjrYabQsTqdDFYUAaMBopNIExYDXCakxtScyNwlqXZQRtsMYJgxaDJl9aNyNk+YsVYOYWKKd5WIQFt2AU0LI6zd2/uG/hE1i76tITf8aCuVhWpZ0tDejqR1ETTF6Ryfip/K2E4bLij6FagOzT3vcisBTj8LIShDqjOEsuqvGqnzEiXWAJWB7kkNsXt0hbMy/4PFEAtJZACISUxCqg3+4gtUEVe4GtzU2Oj49ZWVlx+YDbbVYf+AjgVZLEaQuzNMPqgJX+BpPpCe9940PavS5pJPhs9wG7yYj8qgVAqnlgoVFwxrO4Siw2J6/19f54wOHuHr/zw99ACEG/0+Xk5ISZ1YgoYqYFO9/9ETe/+CXv3PmMX/zBf4Wou8IondFpt7ChpN3vMc1Tjo+OWFlbob+2yihP2Ija7FvXwowr7tIGDRq8Mng+JuBy4/wkbY/bSS+eLor/Fj+ffbPiX2+2EO7zhSaumvbPVcRfV9/9XwGVr1hyXPSaK6rCK4NlffGkti32wyvRH2c1RMydcpUL6CvV/EsiDEICFZAmKWmaIgQcHh4ihCAMQ9I0JQgCbgUJAPn2u4RRxNbWFpubW7Q7HVqtNv1+nzzPCcIQC/zVT3/qNpzPTSv38nq9vONCFbQxfPLpJzx48IDHjx9jrSWOY4IgYDgcsru7y/2PXCDI9a8+J7aWw8NDgiAE4dLL5Tp3+YLTFGMtxlpWVla5cf36aW3z6zbYGjRo8ES8kj6A5wUrPE0Qw1UFP5SG2XOCQ17nIItXE87V4HXt19e13k+D+jtQb7d3H2nFLbrdLlEcE8cx62vrKKWIoqgMRDDGsDJyGsBBe8MJeoVpWErJLJkhpWQymfDBBx+wvb3N11/fRec5Ui3Xxr6pODo+Ynd3lziOOTw8ZDKZsLGxwQcffIBSijsqYtRfJcgzNj7/FBBonZMkiaN6AY6OjoiiiMPDQ4aDAcZoNjc233K2ygYN3g68cgLgmXQldj5rxkUX1iulP7Hz5c391GTXeC54nfv1ZWR5eZk4axPksuZIfvjDH3Lt+jWM0SSzGePJuDwnTVOm0ynJaMC6HgGgt2+ztbXFcDgsI4mxsLq6ShRFfPzXf82dO3fY399HSoXO3+wgkDoslsFkyKNHj3j48CEnJycATKdTl0pPKd69fZsHRVaQdz77BUky4+joiDRNS5eera0tptMpg+GQMIqwFlZXVlHqwt5BDRo0eE3xygmA5xrMioXlMoLc26N/eYPxGj5EW/v/2wyBe2077Q6/+7u/y0p/ha2tbW7depfV1VWCIGBlZYU0TWm322yZMRJLoiKSqIvWRVpIQRkscnJ8QhzHXLt+nc8//4I0TdE6f6vIiwUummx3f5fV1VXa7TZBEVCT5zmyoNbZKbKCvPP5J4RhyNraGkEQOEolKRkMBmxubhIU+YCn0wlRFDkuwAYNGrzReAUFQJiPgr1oxF09anYxgvZZ6nCZ8pbU+43G29beRSxr//Pth+d/l6tuk0DnOQL48KMPCcOQLMuYzWbkWcbGxgbtdps8z1FKsZU7Tdaou8V0NuPg4JBc5wwGA7a3r9Hv91lZXeHo6AitNb/61a/ItUYI+VZRl1gsAsHX975mZ2cHIQRZljEcDhkMBnR7PQaDAY8++g5WCDYOdokO9jk4OCgEaUOn0+H27dtIKel0Orz33ntsbGxy8+ZNRwXToEGDNxoXFgCNTbHFgcjLw1q9NGsG2FPmu7NMseVv1mCplU2GsWl51FGSMRuDsRpjc4zJMSYr/s0xNmeORKReH2vOMC3aIlo1d4fIa5+riOLT2TU0LnJXY9FFPdzxNIvnq591RJ9zvE0wvIh+sJSj8NRx9RHA5pzjYijHrzXIVkTQbrF1/Rrj0YhkPKEbxkQyIIoiptOpC0ZIElbGzv9v1LuGFBEqaNFb3SDo9DicJiRS8Zu/9dv0ej06nQ4HBweYgpHgbdqAWGAqNbuzI3721SekQc6N966jYkFvpVPmWxZr6xzc/giAm5/9nDyzYAOyzNLtrAIho1HC3v4x4yxBKUlooB9EKMBIi1EXDKhr0KDBa4ULC4DW5k6wsfUFzvP4LfH9saedws/yESq/N36R8YJU7u5r80LAOn2NsQWPoNXuKAQw9/dpAXBZ/U6jqIco6iJqn5fV2xb0IaI4fL8Ux9PglZ9wPbfjqeM0V+ObjWcXli4KK5YfwBX3uRvDp46neLbWWrTR5NYRPxtricOQlW6PzSI1mbWWIAgIw5D15AiAQXsLnRtyAxrJTBsORyNsEPCzn/0Mow2ff/45X965U+UAfouyV1gBOoSEnE/vfsbB8IDD0RFBrEiyBJ3nZFmGlJKjH/0mAB/c/YK1tQ3SVBNHbVZXN5AiYHNjm3a3xzTPWFtdI0SyEnURiGJ8vfKzUYMGDZ4CTzFjvoDJ4GnpT8QZn5+2Dg0avGp4Ggqdq7rPU5Uj0NqgteHjj39W5gW2Rd5tH41qraXT6bA63QfgKFrj+PiY6WTC4OSEPNe0Wi3CMKTb7fC3/vbfIkkSHu/vOg2+dzZ8a2DJ0xRrDcPhkDt3viRLM4bDAUkyI88dB+Xx8TF33/8mANc++wUmTcugmslkQrfbpdVqEQQBj3d2yPOcXs8J50KAkPIt69cGDd4ePHUuYJ8J5DK/LZ7n/xVlFo8L3veK6VaeV7kv815XfY/T9W4WhQYXg1SS8XjMH/3RHzGeTBiPx0wmE2bJjPFozHA4ZDQaMd1/RCefAHDSWmc8dlHCQkrW1tbodXsoqej1eoRhyOeff44xTitvjXnLBBWBVAqpFGma8MtffsqXX33J1tb23Ds/nU75en2bpNUhmk7YuPdlKUj7/gXH0aiCgMFgwGA4JI5jhCzIYN4i38oGDd4mPJUAWPqmLfinzfvFnT0ZL/q3SSnLcs8TWJ4nHchc2c9ZMHtRvn1X3V9VvU0j/zW4GIoNXrvd4h/8/X/AzZs3ARBSEEcxYEnTlE6nU/r/TdobHI4mSCn54Y9+xMrKCpPJmCRNODo+4uuvv+anf/lT/uqnf1VQM7mQiLdNUDHWunnWCvb2950QPZ3S7/cJw5AkScjznCTPefzt7wNw7dOPCcOQvb090jQtzedRHKGkZDab0et2+eCDD1jrrxXa1berXxs0eFvw9jjNNGjQ4MWjoCNJkpQf//jHDE5OCIKAKIyIoggpFZPJBCEE163TSA2729y8eZMwDPnZxx9zdHTEysoqW5tbvP/++/T7K8StmMFw8JIb9+ogTRMODg4YDE7Y3z8gSRI++ugjVldXATj4gfMDfOezX5CmLqAuDEOCICAIAqSUpFlGlmX0ej2++c1vsbKyUlhnmt1egwZvIp4z2+e8dnBOWzh32LksrMs1Y4tmx+dLtcGCZvNUPUTt8wuFN52/4Ns+Beb6TdTcyM6q+wtWNFxMA2sX/r0YGp1JBWcNMNy/e5fvvfsBLakI44i1jXVGkzFRFKG1pjt4BMBJd5uj4xMskizXdLp9jM4ZDQcEaUR7NebzO1+wt7dfPpW3ub+tgFwbvrp7l299+BGxiBhPJwyGA4ajETdu3GC3+yMA1u99SS9LeOcHP6TdarHzaAdhDRLL7s59VKaJw5C9w/2SqFtIBfpti+5v0ODNx4U1gLqgWlg0Jxpjyu/nfrMWrU35uydxBRffm2LIFZhQMtUZqdXk0v12pv+a0JTULP4zOVdOubGEusYd/p5FFLSYj4h+EbCA0a9Phgnj/7MaYS2B5dShXqIwa2r/nXeWe8ZLSVjOvEoV2Ra8i8PbCYs0FpnnrBExfrAHmUa2Ir58dJ9Pf/lLTk5OEEKwkboI4B3bZTDJmGlJb2WdPM+JAgn5jIO9R2SRZGQzjmYjcvvqvwPPDQKsBC0hF5b7jx+RYkitgUAxyVIyDF89uMdXWnNy812EtWx/9jF3vviU8eAQlSfc6PfYiEK2uysYaxhkCd/7nd/g2s2bxDKENH/ZLW3QoMFzwJWtTBfN3TunTxGi0gIKnhg4ckrz9pwiIOf0fae0fyyvw9usgrggzgpgfT267rJSqmuZeNv9p6xLBRfKkI3VNTZW14jDkPFkwmA4JApDOp0OWTJjbeYEwN1gxfEeak2aZRhjCYOAwckx796+xcraKv/mD/8QqZSjKRGv0zi6QtQabKxlOpvyeHeX9Y11JrMp0yIlnJCSLM/Z+a7LCnL9lz9jOBzw5Z0vGJ4cc7i/z8nhIde2trhx4yYffeMjfvhrv8bt2++hpESIJjNwgwZvIl6oasJaW7Nd2lMBJPXP9Sjh88676vKeZBW0nBZqn4yLReRWms9LFl+v21PV70n1eQ3szRfEm9imq4YfQ1cSsV5IDlII4jim1W6xurpGHEdkWYbFCYjvtASBzcmFYtJac/yBWhOGIVEUMRqNUEoxGo34yU9+wieffILRBvkWcf+dB1toQr/++muSJMVaS7fbxVpLnucMh0O+/PBbANz81af0ez3W19aIW62KVPvwkJ2dHfb39zk4PERK6cik3/ZNTIMGbygu7ANYN4V6c67WGm0ExorS1OuPXBuMEafMqECVOYQqKtYvMPXz/G9Q+AkaU2oJ565xFNVlXXXNX6VuJp2/BozVpV5nPirXLlUnWOvqfjltkIvUMxegxTmlbbzsvFsQY1+VX+JVCEqy3GPYwsz6cgUvy5Mpit5mWOs18VfVRwKlVFlckiQEQcA0yx0HnVBIKenvfQLAcWsDpCrnESklSjnqF6UUq9ub/Bf/9q9c/l+jG0G+Bq0Nd7++y2BwwrWVa3z/hz/gzudfcHJywsHBAYff/B55GNE+OWZ15yHJex/SDWOEEHS6XcIwINCKKIr44IMPSLPECYFZIwQ2aPAm4hKZQOa1AXUqk6Xf1wSqOQ1c+Tenf3uidu7s8rDLrzmnQUvr8OSF7/ILztNoDJ8Gjp7v1VkQhRBzR4PXBFc5hgpNXhRGSCEJg5DV1VVWVlZYWVmh3W6jlOJa7iJ6j1ubtFotkiRhNBohpaTb7ZKmKVJKBoMBX3zxBWmaYs+hmnr7IDBGMxqPGI1c8Maf/vGf8PDhQ4bDIWtrawzTlN1vOC3gjc8+pd1qE8Ux1joqns2NTbQ2PH78mE8/+QQpJD5LS4MGDd48vJL2k7qWZlFj83RxwHWNxlmfL1fiWXe4XGlPd9Xpa5/nQrh4n4sIyGdfc9nSng9ejVqcxll99zzq94LuIwTWWra3tul2O+R5RhSFbG1tIQvftDzPywwg+db79Ho9VldX6XQ6DIdDJpMJW1tbCCE4PjnhF598wkXI5q8Or+JYmYeUEgTkec7Dhw8RQjCdTpnNZoxGI9I0ZTAY8PA7jg9w6xcfl9pYTwp9dHxEkiTEccxPf/pTVldXCYIAo/Wr2uwGDRo8Ay5uAtYCowVagzECa2Wh8bMFIenZGsH6Z/AaN+PMTQtUIUbUzLzCFCbNIlpTFPOQwH32GrxzqarqEcI1E2SRs/fUZ3zdLpH4nsJEXatE3SR9/txZ1E8Ac/c8Q/spatcsQthLT9TnmXldH5zVD+fdyJRnzFP/PG1m5KfHoqBg0Syv+6uyuJ9RD3GVdTtH233FtG8CUMDmxiaqFTPVOV8/uM/t+AO21zcYyQFa61IA3AtWGI1GDEYjgihEJ5bD42Ou3biBCBQPHj1kOBy8II3y5cf+snfJa8Cv3FxdTVkuIhjIjGFnf5dE5+Q6Jw4jsjRjPBgigB/3Vvkd4MbXXxILy+HJMWvdPiJQqCCg2+0QhRFHJ4cc7x0QaItCkL8S70aDBg2uEhcWAPPcUUFpXQh/Boy2GKPJTT5HE1MehYA3Jxzi/fysm7QWo4SFxRQCjhYabd1ngyWv6Ss1lPPwuZYgUVvwRU0AtIXQ53OIinlfwcuuggZLXpqlnVB8kascnUxxO3G2eDRn/pZn1c8+VSjk2byLnu7mUqVR71dbEwY1XoB9sbBzAo+ndHlFIc6nlrk6+CeyrA5XaQKGXtTmnZs3CTotUCGtfo9eqwOAUorB/i6d6REAH+9P+fqXf4GRglTnhGHI+vo60zzl2vVrfPov/wV5rrECnntsqjhvA3Q2rLWlwFenALpqIbD+mKzPhgJ8tfOAYTqlHcZEcUQ2nSGMJZSKvZVVTlZWWR2c0P75T9m9/RH9fp9Wt8Ng7Ezujx894uZ775J8+E3+9N/9EalQ5LahgmnQ4E3DU5qAn/fEW7vFIr+DqC2Pz4374Rkm6cV6X7iOl7jnmVwqz8OEd/VFvsZcMC8GL5Iv5wWMoUxnBEFAkiTMkhmj0ZDBYECe51hriY8fIICxjDHtFT76xjdYW1tjdXUVYwyHh4ccHh5yfHLC559/fuX1ex5w6e/atNvt5xJ5fuqx1cZHmqTs7e2jtWY4HKGCgF7XRfqurq2x+8NfA+D9u3e4/d5tl1FFwGA4dBlB0pSHDx7y7q1bbG1to80rvGFq0KDBU+PSAuBZ9BBnaZHOom2xLLne2QzPmSxPl7cYGDJfL1uamU8d/r8l15/V3jO/rwWhXAbedPwsC8OrRmvyrG061a/PWkaDJ+Kq+6tenhSCtZU1rl+/jpBOAzYcjtjZ2WH38S7T6ZTN9BiAk/Y26+vrCKDdbrOxscGtW7fodDpMp1OOjo5cGrPXYLMgCtqbKIrK717UeJylMx4+eoCUAiklG+vrrKz0aRX1+eK9DwBY+csf83jnMQf7B9y/f7+g3ImZTCYcHh4ymUx47/Z7ZTq45n1q0ODNwoVNwFmWzZl5S8oXa9BWz9HDeBhj5871E4g2uvTtW6R60abKyrCY6UJrU07+i1QvyyOEbZG9w546z9iCMkXM12ER8wuj9xms7lvW4Wk0NLZunnw6vHKCjrWcn1XjyTA1X8inLelZ6/C24Uq1UzVTZ9yK+fXf+HUXTJBphFLkec5gMOD4+JhrN65z3QwBGHS3abfbzJKE69ev8/DxDsYYWq0WYRgyGo0Yjyelz9urCt/+weDl5Cq21vL5F5/zB7/3N9laWyeWAQGCVhiRR4bH3/4ORkpW9nZJ7nzOvgyJpMJimY5nrK2tMdM5j3d3+e53vsPP7vyKB4f7zg+oQYMGbwwupQE8j6blwmWc+nB5XPi+L0guepni1ysl/F0x3tyWvdkQwnH/KaXodrv8t/7xf8iNGzdotVvO5BvHdDodOp0Os9mM9slDAMYrNzk+Pub46Ij9/X3SNKXb7dJut9Fac3x8TJonL7l152MZ5ZH/+0VRISmpODo+4vHjHaSQGK0RQmAKn0S5usbOrdsAfPPhfW7fvs2169dYW1tHCEnuN+zGkUl/9NFHAA0dTIMGbxguzgNY/2zP1lud5tSbu7L298WEyafRcJ0SVOfu+iTBYln2A2/YpEpZJ6pf5s45VVd7znG6zqdM1Ev+e1Y8q8nvSbU7v6XLyqudd6pfnxZn1eLqxMpzn+wroaE6+2mUfz2HDYS1Fm002mjGozH/r3/5Lzk6OmIynpBrp0VSSiGFQGdZaQL+aiq4fu0avV6PMAy5desW7733Ht/+zndYWV3hF5/8gln6NALgxd7BNwXGGLIs4xeffsJ0NqXb7zGdTQnCACGE8/Er6GBufvYJUkquX79B3HJBI/1+jzzPyLOMlf4Kv/97v0+n3Z6zeLzZPdigwduBC2/pZjojLyb1PM8xWpNrTWY1uvC3M1hyDFoXGUN0IRII66JxvdkXjTZZGSmqTWFasLhzvBAwZx6+OKqJymJkdW1JI+PLO6NQa3OWEpYIS1bLHlIXnx0Vzlm1PCOyc4G2Zc6kLDVWVIs1clGQfnpchdk4J1+e3eScCNLz7lh/NnWKn6eD4cw+v+IlSwteSmTzxXFGxHgtQvt5wKdo0177ZNx7PZvNyPKMMAxpxyFrIqFlEgyC1q0PaYeCa+sr3N/bZzgw7Dx+zPq1Le4/fsQf/fTHTOvv36WgeXqHgtcLpiCh+uknf8l7H97it3/zt0hFwnAyIZaKrY0NHn33e/Cf/X+58dXnjNIJQdpianKiTpuT/TEm17z/4QcIA9dWN3jv5js8vnsXkhlQbITlucQFDRo0eMVxYQFQFwKe8/mrHabwthKOCsURJ7jz/ALjLB+mJhxU9AoFMYz7bM/WcF1UYFmm+fMLtF0UAM9YuCs9VgFRfW9qf8/f99xaLReMztJ6FppEI8wrGyFrMKWA+qyoa83KeJpnbvOLWZns6yAALq2fvWpZ+BSEECAEcRQX2YGsI3huOwqY69ev0frqJwCMW2sYBLs7jzg4OOR4PGWUZagwIMXw//gX/09Gk0lBXGO5/AAx525O3iRIIQDDNJnwn/6b/5Sv7n3F7/3e73Hj+g2UcGn25K//JrNuj9Z4xPrdL0nW1rh5+132Hj9mZ/cx/X6f92+/x3Q6RUeKb3zwEftff13d5BWdlxo0aHBxvJKZQBo0aPCmwJYZKforfeIoKlPBtVtt4uNHABy31jHGEEURW9tb3Lp1i+vXr2OB/+z/9695+PAhugwOaiSP82CswViLkoqT4xN+/ouf89Of/pSTwQl5niOlZDydcu+jbwLw/p3P0Nbw3R/8gHa7TRRFRFHEL37xCwaDAZ1ul83NzTfa37hBg7cRFxYA6xHAi/5qxroIYB8FXEb7+kjhWuSwWVJGHYvE0Wdh8byzzJr1LCX142VNZssodC57fb0dbwYs1r7c53LVuOg4flPh2yyERAjhTL9Zxng8JkkSptMpjx8/ZqPw/3ukW+zs7HD//n1msxlhFLG+vs4vf/lLfv6Ln5NrN7coqWg8z56MUAVY6wipZ8mMn/zlT/jLv/gLpJSsra0hhODBt78LwO0vPmN4MuDnf/VXZFnGe++9R7fbRQjBZDrhYH/f8Tbqhg+wQYM3CZcWABezffiFzguA9QWvfo0XDl2E2fkC4EUEpDkB9DyBconA+jKpU67i/i+7Dc8Dxrx5bXoTn9NFsBhAtfN4h9FoxPbWNgjBvXv3SNOUfr/PyvQAAHXrm7TbbbrdLuPxmNl0ymQy4au7X5bzhd9ENhrAJ8AKsoJk21qLUoo8y/nZL37O450dDg4O0Foz+K3fAWD9/te83++yfe0af+sP/oBvfPObdDodF7EdxTx4+BApBIFSL7lhDRo0uEo0JuAGDRo8RwhuvfMOH334EcPhEKUU/X4fay3HB/t0J04APIrXiKKI7e1t5zNY8OgdHh818t4zIgxCcp3TabdZ39ggSRLW1tbovP8BR+/eRljL+l//FYPBgP29PR49fMh0OmVjY4PV1VV+8IMfsLm5SafTedlNadCgwRXiwkEgxrogEFt8niNT8MTLi6bh2m+muA7ACjsXjFHPm2tFldu3/psp7nZamVIPLKE458mmimWax4ucd/b3tWAWd0L1k7DPGtb6HLGczMHHH9uFbxa/feq7vnL98eykFq9em14ePB+eNYZslpDNZiAUSkhW+n02tzbZSA6Q1pDJkKy3Sc8YRsMR65sb7A5GZFqTmrx8s3yA2eWDOd6y51IPpsIyy1KCKEJbmEymSC3Y29snCEIefecHrN+/x8Zf/xW7v/sHmDxnZ2eHJE2RgWLz2jY2Cuj3+si6vqDhgGnQ4LXHhQXApODvshKSLMca6yJ/a9k+vFnYm3rTGlFITi3DBxZdi8zNtCknk1xUv2lcZpDizNrnhcVWaCiSlVssxmbFZ87VHpQUM7aeCaSY2WpUNFVmEeapX+Z88HKgEjznKDZe6YnSspQyRTg6FlPj5TO61kfPqJWpp4t7dbqnyhrzNHgbzb1nIQgDsGDynFvrW3x4/R3GoxGHh0eMRyNyrelN9gA4aW8wSmfkWrNzsMv6xgayHfH+tW/w7nsf8PEvf06Gz7ZjETZHvEKj5lWDBfLa+2msIbCCo9GALAcRh4yGKcPhAb13v8H3gVuf/YpfJRlHxwN6nS5xu0VvdZXv/8av0V9fY+/wgD/e2oajfaCQwd8UF+QGDd5SXNwEvBj2f9EE9cU5njy5TsWyWF5JAlMvt7y+wtJF9jkms38y7CtQh2eBXZJdvoaFZ3HVJrnXrbcanA8hBAKB1ppev89qb4WVXp9ABUghWF9fJ45j5O5XAKSb7/LhRx+xuroKwmmndK754P33+eijD5FCLrxaS8brRY63AQtttrjnkeU5YRHdO5slaK0Jw5Djb32PLIppDU9Ye/yIVqtV5jDu9rqsbmywdv063/7Od9jY2Hi5bWvQoMGV4uKZQBaza1zQwf3sc87OBPJ02T/O/OWFaGZsUYfL3OdFBQksC365qqwiV4HXd232bgmN5q8Oay1plmKxXNu+xve//31WVlerwI+VFVZXV9lIDgHn/7e/v0+32yWOY7TWxEHIl7/8nPHRgHYYoQBlHR/66zteXh6MMcRxTBCGfPvb30YpRafTIer12PnGdwBY/9lflWb7PMtJ04zJaMRgb48sTYnC6CW3okGDBleJC5uA8yIRuLV2jurFR+BKKcvvpJTkSYIWdilJrrW25PTy5Xk4Q3FV9jIIIQjDcM70LCVVxoHaecYYlmasuGrYp8us8KKoXOpCiim8ORUKIcVLcU8UVDlTLdQ43l4vGGNfzPh6jaCUIm7FLu+slNy5cwedZkgpabfbtFot2u02veEuAAexoyV59OgRaZqilGQ2mtDpB7x38xZ/lKYoKsVWQ0ZyOeR5jlKKzc1N2q0Wx8fHJEniAm7imIcffMTtT/6aD/703xBf7/D5r/8dvtwbsbu7y4///M9pdzvsHewTReHLbkqDBg2uEJfWAJ6l7RBC0Ol02NraKgXCxevnvzj7HhdBPem6EKJcbPwBYIxuNDMF6v0gEEhcPzletRcP//xctoiXUoUrQjO+FmGsy/rRarX4J/+jf0J/ZYUsTZlOp6RpyvEnP2ftf/2/oDU7AWA2KzYC1hJFEaura4RSomcJWytr9KK20wDS0BZcFn6O1Fq7/MtSMpvNUEoxmUyQkxE/2vs5APHhEe//+b/mD/7F/5FeIMmylL39fWazGScnA0aj8ctsSoMGDa4YVzqfaq2ZzWbOjPAsgpetRc3WP58BKQRKKYIgIIoier0e7XabIAhrjkOLBwuf4cKLed0M9RpGwwkESrr+Uhfh9lrsrtesvQ1eDPzQEAi00bRb7VLjdP/+fcIwhP1d/tE/+99x87O/cNdMMv7O/+F/T/7oIVtbW44ixhiEhW7cZnN9HWNyJLY4GjwNoihibW2VtbU1siwr58r3//oPaedjiBTCghwmtI8e88Evf8zJyYCHDx9w584dTk6O2d7aetnNaNCgwRXiwiZgb0712r3FzBoAWZaVqYaCIEBbXeXhnYu0PV22/1fpHFEYeaSlpHwwVpDVpn9rBbZMxCqQUhEECiEEURQ5R+YsJR8eoWs0NaXwIiyBkoRBiFKKNEtJkxQVOF3DdDbBGksQBGW9BaBs5Tkn8NorgUZizlBlnScM+7bPfYdweYCvEPX7lBQdT9C4Cju/Q5C1MvIrrd0VQ4BAUhJhvKVaYDfcz95ALRt7l0U95skHeBlrwFjSyZSP/+KveLe/iYpglqX81sc/pjWZIG+sueuPp0TjMd//yx/z1a3b9OI25AaBpNdv8fAgRQSQp5ocMG9JPt+rhFKKKIr4m3/zb9HqdTHqgLjTJghD+rORE9qvdd046YRYIWiNT2ALuq0Oo5MhrU6bb33jo7LMAIH0c+EVjKMGDRq8eFxKAAS3mNYFiHrmD1Fo4gDCMCTXlAKg9yFcBm+ytdaipEXOBSoU5wBGSUrdW/GPKXzYnLnXlZOmKVEUEQQB7XZMkqVkWYYQIGQV0iqlBOHyZoahIs8hCCTr6+uMRiOOj4/nzNnWWqTxdaqHFjq9h+W0OfNJwt+yRdi6kq4MZ93HPbszhHL/71zIdtXeZyNMeb4QxX94IfeVrekLgD07auKqBcDyPsaNG5Pl9NodtHVekkEQcqvTAingwQBmGWQGKwTs7zM+GXJ8cIhSiiTP0Gt9rMjJycnQZMXrL5pAkAvDb4hnsxn//J//c/r/vf8h1965STqZ0m63Sa/dRnz6Z7BZkTwLYzhZ3SKKIpfBKcuYDjWdmrVAWEMgFfrtfrsaNHitcWEB8EWh7js4l+9WgFLzfA5uARPkWpDnGcYYgiAgyzKyLCOMQlQQENiqnPqiZ60ly7LSPwacGfvw8LDUZPrz6lhcfN7UCfCsRfZNbW+Dq4NLQSYZjyd0eyGieJf237nNN7V2kRwPhoDz7Uu+8S3SNAUoLQiz6ZRHj3bI8uwlteL1h7WW2WxGHMfs7+8zGg65tXUTEcVEYcjDX/97XP/sJ6w9uoOVEmEMOzc/4uN3vo3NXOBOr9tjfWOdlbiKAi41f2+pdr1BgzcBlxYAjTGlEOUDLupawLlgA1mJED5KWABWCoQ9rS0ThTlVCB/EUYswBqQ0UCSXB4swIJVAz2wtGliSF3kwhRRokZe74HrdfARxEAQI4TjL6mZuIYQzYxcRz+eZS8WCVnK+TadNrYvaVCllGQldCagvMjjC9SdAEASu/7BVuxq8vhDVs32h97SWLMs5PDxgs9MjjiK01nz8nR/x7u/+Ae/++b8rT3/427/P5z/8TXSS0Gq1AAiikI8//pj/5P/znzjS+QZPDSEEaZpydHzE11/fY627RmAFNsuJ1jf56X/nf87qX/8h3dkRo9UN7n74Q6L9A2QYsr+/z62b79Dv93l496uyTIXE6MLFpzEBN2jwWuLCAqCUEqVUKQD6I45jjDHOVKB1RRcDWFRpAg7DsBSCMq1Rudvtey2cR65toelzviue7mWWzjAByMDVI8syBIJWu8fRYY7W+RwtjK+rihQ1ObQUsHw9fVn1YIi6QOsFXK11KSQu7R/kmevsmenksEghiaKI6Ww6d70UAqQqDSx1/8nFCOv6fS7t7yZACucv5yO5x+Mxuc7Ltjd4fbGo8b4I6uPrWTgO0yQhyzJW+iv0ul0mkwmtTodP/2f/S+7+xZ9xfTRg0Oly/N0fsNXt8vXXX7Ozs8P169fptFrs7+9jc9OMwWdA/dkls4Q//bM/5bsffRuhQoIgYDqdQhiy8/2/yViPQUC322X01V3a7Ta3bt1ib3eX45Nj9LSKAm4Mvw0avP64sAAYx3EZOeY1ZXmez2nQvBAILofv1GRLp4lMa8JCAIR5gSvXLYLAac3SNC1/i9sxNrIgXD7gJHHX9Xpt8iwny3Th5yfmzLlR2CIIg7Ke/j5aazf54SLk6ovMokazLuAuX4wswli0rvwc69yGdeGyvqB6DZvWGimkc55fwGJE9Xl+W0+zUJelFeUmSeLaaF8cR2GD54NKmXz5qPy6lvqycFH5AXnBCnDt+nVW+32stRwfH7O3v0/8ze8xjiNiDDpJysCtdrvNysoKK2urrK2vMUmmaNMw/z0tFueLk5Nj8jzn9s13mA5GtPshx0fHdLtt1tfXmUwnrKys8Df+xt/g008/5d69e2QFgffg+Hix9BfWjgYNGlw9LiwAaqOJZEgYukukDBGihcWZX4WPzyiUBxJoIUsBcI6TT4IWXiiqTyKWXBtU4ExISkkQONLYlS47hw8xWNIkodUOmc1maJOgAkEUBeTamS6lUk5Y0xqd61IAjKJoTjh0gmtemT1L5pnTAqAXcLNsSd5cQCJQVuEzQ2S1oJezSLSdFhVynbsAlcKjfm5avQT1iuAcl5zz5mofqY1llsyqqM7CRH0mzqvTG7M2vJ2ajmWC32W+y/MMJdx79uDePR4VZsjt7W22NjaJWzF5mhAKS6fb5fHubpkGcpYkjB/v8HhvF2MtUiiwjRB4FTgZnHBweMB/+x//Y3765z/hxs0bREJxeLgPFqIwZDKekOeO0cFow/raOtYabt+6vVDaYvRPgwYNXidcWAAcj48J1AoqCJyLDwUNS2AhtAhjkFojskLwsRayDCkEaWHiVcpJh0oa4qA0bmJ0TdMkMxdjKkCG2pEWK8lv/PZv8Md/fsj3vv9dfvpXP2U8mRC3YqaTMSoWhD2DtDmjwRQpQ3cEMYPhkBXpfNuklLRaLaIoQuuMIDQkSUKapuTJlDiOQTg6mygIsECW5QRKoEJBaALsNC3NwUEQFOZTUCgC4xY8Yw1hUDlMz4RESFHS5HhYq8nyDCkVSkmCsDK9SSswZRoVgTljjnXaSsrzzvLHMYLlZQhBuxMzTSakaerMvvrJmr8IiS1ChK2pIm0NZ9znJcI/I+Apo4ItiOVR7KENuOwCmGPRr7BgeZ7md1Hgm/vbuI1fpBShEHTjNtdWN+jGLTbW1jk8PKQXt1nr9VFKYcOQ8eiIySQhR5OYnM7qCvuDI9qrfcL1FRJhSa3GiCdscBpcCHmecPfrz/nxT/6E4fExnZZCZAaBRdkQFShm4wkbm5v8xo9+jdHRMb24hZ4m6N3jspxYBRxb9068Yq97gwYNLoiLawB1xnQ2KU01ABYXdCGU05oZNDKohJagyDZh0UUwR8H3h5wLMJjzuTMK7/Md4pjrx+MRP/nzn6CU4LPPfsUPfvh97ty5w2AwQOucVGva3dilFDOQzHTBR+Z8DweDAUEQ0Gq1yrRIUgqCUIGIkEpgMTVtV1AktIcwDOa0gZEOyXOBlM7UPB/QwSniah8NKQp/ukWTrqNh8dHNlcbNcfAVZWPLz4uYZ/k445zip2WyoVKS9997nyBQfPb5Z4xH4yfSg/inWLF+2IXfXh0stuXp/NkKFexi0JKt+uIyJYlXWPh7VggAYwomT0uv2+Xo4BCdZoRhSKAUx0dHALQ7MWme0+12OD4+ZprMWFtbY5LMEIHi1u13iVstpqMmCviqIKVgZ/cRx0eHoDVaZ3TiFlK4jXaW55g85/7duzx69Ij33r1Nr9Plwedfktb2BdZahFLYwl2kQYMGrx8uLAAaYxiPx86vrvADBMjyDGudn5tSas7cWU/LVg+gkFIQyGoZnPcBNAXLXIW1tbXKj8nChx++T5YlfP21pd1uczwcOkVk0AJCDrMTtC5yYAa29CfM87wMXmm1ojIThvdvnM1mrlMKKhlwAqT35/OpqlRhYoZ5X6m631/5WYCSCqnUHIn2q4I816RpygcffIudnR3GTbqnBs8IC7TiFr/+o1+n1Wqxvb3N4OSEMAxJ05TxeMzm5iZBINE6Z3d313HSpSlffvklq8X7fuPGDeJWjB0PX3aT3hC4OfbG9RtkWYZJM44Oj1BrG6ytrjIrgnb8c4rjmLt37zIeDLm5vkWvW3EFKqmwJis2u6/atq9BgwYXwSU0gLrklFpZWXGaLGNL37nF/L8+UMRTs9SFPGMsCDlH2VK/ztPA1IUqay0iEoRhyMcf/5xOp0On0ynzWoIAq+h0YBRPyVKLtQatXURwkiSMRiNWV1ex1jKdTgki67QShSBojCm5yLyv4GIUpW9nnSLGtcmUlC5BEJTnCgRWVlHNURSV/UmRwcSXXafYOcU9eEY0p6fXWYYLabsE7OzscHh4wMnJyYWucXK4KTWdr1NE4JxG0L5edX9WLGpDn9dGxFrL+++9zz/9p/+Uz37+KevtHnu7u2xubpYbSGMMySxjc3OT2WzG0dERcRwThiErq6s82N+FKEBK5Uz4ZxCWN7gMBK1WzFaR0s1aNw+Nx2Mm4zEra6sEQUCapgyHTuj2G+Ysywhr4+W8DDMNGjR4PXApHkAf7Xt0dMTGxgZSSdAuYMP7xS0SOXvUhTmDxZiK/25uIRIaijRo9fRxVlhC5Shh9vb26Pf7pUYvDILSINluK/r9FUbDKWk6xe96tdaMRqNS4EMYbKbn6re6uspgMGA2m82RQC8unF7oWraA+uvKbClULHteOPSCntYWz5Nz0XRxywTDpdlELkjfIYDpZMJ0WrX1QkLga5oU2Jn2K9P625Im7mpM4ReFZTgYsPd4zwl+H67wzjvvEAQBa2tr9Pt9AOI44uTkiNu3bzOZTBiPx1hrGQ4HtNstEqNdRLFUzrejwTPjRz/8NX70ox+xvbqOsoLB4RE3btyg1+lwNDhBCMF0OqXf7zOZTNA1pocwDMtyWlGMzC1Wm0YQbNDgNcWFc6uXwajWMhwOOTg4KPjzZPW7dRpBf9Sv8wKiE/icMJgkSSkYPukwRRRunju+v6OjI5IkKe/hfdKUUnS7XeI4Rsla6jgcxcnJyQmTyaSkdkmShPF4zGw2c3QzcVxo/+oCzrywU5/uzj5ruXjk+RSVcmbhp4V9wnFeHS5+j+X/nVeHF4sntf5J18Clar7kNk96DmfX7Fnqffm7vVgI9o8O+Ff/6l+xfe0aaZpwdHSEtZaVlRXCMKTb7aKNodPpcHh4WFoLPA2REIIojhBSkunGB/CqIIRAScnO48fcu3+PLMvY2dlhNps5vtXZrJyb/TPyDAjeRQZgfX2jJORv0KDB64mL+wDitF7WWKxQ7B8eY4RiZXOFzGuxjCXPa/57Na1Dkll07rV5gtxojDaooJZPWIARqdMC4kzFpuAAswJMOiUIg9JHRUqJFBmTcUJunUbNaMgyDRha7QgVhIzHo9K0m2UZg8GALI8JwkoDVCeETvIp0gpynaNk5e9ngTSv4je1mTcBa6+txJYE2HXBueoWp/2U9rT8/STNjGU+yvbs8+0zReN6DV8ZiIytNvrCclZyhosuB3OBME+1iJwt6JTayaXFmvIaW/5dL3MZavxGC2cbLl9/89RC2uXOL92zLkjf8swQ/p4uAlgC71y/wZe//AxTCHuDwaDUJg0HJ2ysrxAECpNZdGogh0jF6FbEx599yv7RCSqIXTo467dzpnE7e0oMxyMOjo4Q2nDr+g2mgxG91T7HgxOiKCIMQ7IsI01TZrOZS6upcxIMUe1d0XmOxGX0ax5FgwavJy4sAOYootglFc90jozaHA7HzJAY4c2slQ8dWIyu4kN1npdEx9bYUlgCKiEPyMQILaosIaaUNOavkbKIJBaQaUNmKkFsOp1iLbTbLTY3t0iSmfP5K3zzxuMxk8kQGcyXp7Vma2uTf/Af/G3++A//mN0Hj8t8w75+s1xgl0x5Z5mEfZ3qBNTVeQs6oQuZXsHUko4Ya862wDzjzGyExchKqJ2jh3nGsuvm16c37uUuFPdU4XYpqXahi77kHb0kv/xVyTFzUdBPRlEHcVleO1/3S0BQRJm/IAjXs0pJNtbW+eF3vkcsFVIIVldXSx9Y757QbXd5Z/sWyWyGmcHudJcgiIhEi70046effcEo025826DkyXyWEfO24/Mv73A8GrCxukZvbZVr166x+3CHdhgRhCGdToc0TRFCcO3aNfI856uH92mHghvvvleWMxoXwWJNEEiDBq8tLu4DKATTIuBCaE2r3WYymTAYDMiLRUZrXRMA57NheNPtIrzZGJxQkIkJWpw2+ThawXTu7/KzBFMLnkiSpEgjF7C7u0tSZBqYDyrJ0WlSNM1pJKbTKXme87O//IQHDx4xGk1c0IcvG0FKEXCygLqQt4h6P8y16bILenldVYX65yuHqP37Vs/zZzX8KbVoYuHfq6jKMrxo36yaRXBrc4tut8vu493ShyxJkjLbRxAE6DRnZ+cxo+Gw0AxKNjY2QAhGoxG/+tWvsNZZAiofxsbk+NQoOE6PT074/d/9PR4/eEQvbqGU4tr162TTGUmSIKUsrSxaa+I4ZjqbsbPzuCzKGO0oYBo0aPDa4sIC4M2bN9nYWOev//pjwjCk1+uRpilZmpSL0mJAwuJn/7fPrLEI97v3v3OaQic02oLvrnJCNkaXpjdjDHpBo1IXRH2wSF3YBIMtuPG80CilJEkS/vzf/yVSgRQhudalgGsBK5cTCZ+nAVwMyCg/i0povKiTfmlatk8+98I4i2HX4sieX8E1t6xS44D+ykFrw+Pdx5ycnBCtrpPnOWEYopSi0+nQ7/eRUnJycES/16PTbtNqtbh79y6PHz9mZXWVvb09hsMhFntupHuDyyEIFLPZjEcPHzI6GRCsCGLlloEsy5hOp2V/n5ycMJlOaXc6xHHE+OBkrixPy/X2bgwbNHi9cWEBcG9vjyRJyly7o9GI8XiMEZCZiqplMfK3/nkxvdoiXJYGgaWgRsECzt7prq/7oDgCZSElKrRIZUoNnzGm1Dp4h/LTuXw1SE8KLSvaFiEwxpJnpiaQVv5fWudL5aEnmYDr5/lo6SgOMFZgtCYMw1PnLU2zhSO49oLgeZrHi8HOmSMXn9mrxFk4BwvG5R152TVpsAABrK+uEUVRGWS1sbHhhL6Tk3JM6TwvA0Amkwndbte5b+DcNLxPrs9N3eAZUdB4/dm//zO++81vgTFMJxPWrl0nTVOUUvR6vXL+nE6n9LpdxnlKGEYE7VZZVKCCgq6ref8aNHhdcYlUcGOOj4+JoojJZDIX/LBsF3huyiiYo4upQwO2mFRkEYDhfPpMyZkHEMcdgiAgCEIIcjLrzBeewHnxPotEzcs2rsLluMMaiUCdWnSc7i9fuuN9Gi6+VtxiY2OF+w/vI818dpTzUC/x2TUj1S7+1DOj2eE3uDws0O/36Xa7pKMJvV6PJElK1wyllCNezzUG994FQUC73SaOYzKtGQ6HzJKZeyca4e/KIKUiDAoqLAztThuA4+Nj0smUjz76iL29PSYTl/Xp5OQEIWFvb5d3onZZTq6Xu/Q0aNDg9cGFBcAsywiLHb2nBXA5dTXIun/Ok2drr0VcBmVl6djf6/aQUnLEoSvZmFIekYVmYDZLyeyE1EzRC5OSj7b15t+zs3DUHerc33YpQ86TJrwlbS/9ok7/po0hy3LisEitd0oAu9AdLvzrZVELAD59p6cUCs+77AwRevk1T8lAUY//vRzOrvlT9/qyC5cFtTzhovMueR5E1wvOCqc+Biqg31/h5OTECYDdDlEUY4yjF4nCkMl0SjJLsJnLJORdNuI4hiwt39uLboqW1ue54ip3RS+ozsK5zmxvb/Fbv/Xb/PTHP8FozWw2Y62/QiSdebgVx0wmE6QQxHFMmk65+c47zO49KouKoxiRTl5s/c/FsufxpHo1O9sGbzcuEQVsyZMZCIGRAqskmTXkJgNrSk47bfIiWk9gjChdtOo+bl7489xfXitgjAGfUUMpAgv5LCEWChlIcpGX3IFZlqCN4whEGQKlsNaUZktfrr9PlmXzfodWEMhWYaZKHSmzb6vVGJYEolBQ1iylHtFYe0ZQR01YsRjn+4dgMhkyndrS3FsXAI2o070smNaXWsScidzY/NQvT0ZFjTJXbQvLxHQLpPLyQqAqmuDiqsWcY39+Rq/Kc828l1t4LKCFxcgzRGuhl5dpFZwR/KOfyjq5vCxXh4Qz2yuWfy+tOxZhcO19slB5QSwtqvoiMIIIATpHCsGtW7c42t9HoGm1ArLZDJ1MmRx36Xd7bG5vkGQJw+GI0fiILJ/S6XQAw6AgJTZ6MX+2AIIzOl2f2UdXC1GMicsKEPqcZ/H8hSgLoCRf3vmSvbv36eSCbhwSqAAZBpjJjGw8JdeadDx16eBkwGGaYYGD48OyrLjdxo6PEArO5IR6Ybj8tvJi1zZo8Gbj4jyApf+ecRQbQrh/pQVMLQOGKYQZsLbyn7PWljQQXhPnI4PDMCxz7uZ5XpIlB0FArhTCWpCgFGT5zKWPwyCEJQgEIlBo4cwS/j6+TB/ZW0/b5uD8+6yRGA3WCAoLMAaDWRKhu0iEXP/FSbrnLD6lrOPqvSjULZTmjppi0iyesMwMjQWrr2ROE7Vj2W++GpeBFdX6NxcgBGCXa/osFrFsUbcX0zafLq8SrE8pEYU59V2ZM8QuflcPRFmukFz+nah8WxfLKjrHZ8lerIddkLj9X2cJgODeG8QVLnLndLlCEOCCCkyuuXv3LkrAxkafg8M9+q0O+Szl8YN7ZP1VtDXIlqMfabVcerj19RWSLGM0GiKFxAgzTz/kWnxGDV6QSdJ63+DL4umi/p8KZzxyoSSj0YgvPv0l1zqr2CTDRAHGGtpxiziMOD58zOHefskJaLVhOpnywfsfluX47EvG2isdXk+FM6lo7JUH8Ddo8CbhEjQwxb92/m8lFUI6k67fqadpSp5ppFSnJod6IIiHF/aAudzA3jcoCAKEgrgdOUJY5qlVRGDLcryg5zWK3mTty6vf1wug89qFJSt9WfmLddWV4rIULFc8GZ9lWFkucDyhrHOuObO8K6agEQiUkeVf8897uSbUByItxVONiTMaY3Ekj5fR2NXk4FM+reC0M1eoATzd3tNlr66scv36defzB9z7+phACUySYdOcRE442T/knXffZXhwAEKwtrbGZDJhd3eXqBWXgVlKKgIV1KL6z1vRZaGZe954Gh6f8+z0gkskZXoyxNnp2bIsI2r32djYoB92CALFKEnYimOOHz9if3eP0WhEt9st585ep8PBwSFbaxtlOb1OFyUktsgJ/8riFa5agwYvGxfXAJ7hO1fX5Hh6FxfN66NqK188T9K8SBeTZVlptvUZPqAS2Ky1CESpGYT5IBJTaL18RK8vz6eOW8zC4eEDQ+r1WfQ5WoyEXfQxLMtbou6pC5z1fy8UuOHLFpe45rw1ZiHzxkV8wywGW9O+ncresbQIgbKyktnqOwB79p2XL9sCawWmtjb6sfXkui8/T1p1ppAgpKi0b9aesYbOl1t/7KL2/0Xvu/nfFsur/6lgSYaY03sRW7vvfGbm+hiWtnpOV+EPeNonzwKz6rMQrK9vEBXao8F4xNpalygIUAhavRY3trb5/JNfkiQJk6kz++7v79PpdGi323R6Pb77ve/yJx//lPFk7HIB1+94LvP5ixQAL9OfXk287Bpx5ph8KlgBou4KUj2zOIr5nd/+Hfr9PgcPHmOMZWQz1q9tc//+fZRwGkKtNdvb26RZxq+++oKt7W1UTQMttcXmurD+LuuP5TnKwT+/856hL+u8HfjizvCMfl2qlz+rzFcLZ/VfgwZXhYv7ABb8eV4r5wen1nktx68p8/M6H0CDEBIpZZmFw5t587yiYKkLap62RQhRluvvZa0lDMOSXsILY7nNsco6k3FRjuf8O4uX8DyKkzmBsBBel6HSQoqlGRfq2T/qZZ8VAFNH5jNM2KrPnwxZzm++/ct4BpVUVXT0OaZoKRVCldJDlckFiGCpD6A1pkz5JwApqjrl1pSuBHUIqDE8zkMLyFV1Tb2uWuulQo04Z/GRKOSZno0SvyCYOUFzXjOoTy1Ei+Us++0iJuvCv+1UecsX2Ko+lV+qoAqcWN6vpVEbe0mTqWWx7aeFSqUU3/72t1lZXWW9v4LOMj788BYr3S53v7jDdODoo65du8Z4PEIGktXV1dJPN4oiDg8PmUwm7v2lCuCCJ22GnsYv72lxWXPuefWSnP0GXAzzz0HXBM1iPBS3j6IWP/rRDxmPxk7w3tsnFZqfffwxUa6JWm1WV1fpdrt0Oh2Gjx8TqYDx8YCHyUF5h43+CpFQaCvLUTQv2J3lQHK+ACiFKi+TQpb7R21MmTEKKChoau09UwA8a96c9y1+1XgmG+GvwYvAhQXAVqvigJobnEIhjCtmnuBZgA1QKjgV9OEFGi+E+Yg/X7bXBM4JadIJF2mWOr8UW2UHkEIilEDISlDy5Z+3C10u+FhCpTCF5usUfUxtoojjuHadxtgqcKR+Td3MfBFtngUiFWBlJfhWBNbnQVB3gq/X7yxORjibkgcx71R/FsfjKURVfaSoFp/MmlMCRFFraln50LqWOk/lZDWBeVFzvKwe5wntASFqaVo3izYJlT21rusyIHO8L6AnJz9VgrWlgCtY0FKbsxPGXSgFoLVzC2BdK5bZKh2ddSdX59XK8G4aVXmXEwANkC48waAWkRSokCBqEUcR29vbTIcj0Jp7X9/j8c4jPnj3tssCIhWdfswsTRFxwGg0Ynt7m5OTE7a3t9HWoHNdBIPNm9/P3Qydo929WlinYbuUad2b4pf5swZgIp5FeDW27sqiQPjNjECpahxOJ1N+9dlnvPsbv8fW1habm1t8vb/D/cePuNbqF8wKM6Io4sGDB6ytrXENy/b6BuwfleXkSYIwBiWjKlNSnevVfbG0rk7TvqQbwG2kiy4yUlQ+w6jamBdIWV+Dcpb7f0rOSuFYT8f4SvOdNmjwHHFhAXB1ow+4lzRJkjINUCxbCOkEDaMNcVqu/ggRFhG+QWEONW5xNwarDdkpcmaw1rP+uzzAwvuXSBCB00pFgShMpO6n1KRo4czNSgGiRaCU8/E7k5/PoE0lVM1ZKkProtsAISVxHCGE01ROpxP8ilQvWiqBUnWBLSt23qLgSqxNjHP1WDCrFnD70+qaOSFhQbFU7rmFRlBlQKkLIFmWzfeF71YpiDvtqkgv+AiBMTmm7COL1qY06afTxLVJLJgFBVVnWuZ4GVNOC4BCCDAGmVdagTyr8kbn0pCpvCq61qZFcnFXH3fvOVO914BYLwCqWhm+qgaDKttUbUoEQug5k9rcsygFPqd1FLJqu8WUlj8hRa286nJjrEtxWHuOyyRFryHzF+s8p0hjgxK27NdlZOxe6FNBUC7Wxlr0BfIEz2mQpSQKg6LJbrR2sTDcBaDX7pCqAK0N3W6P7Y0tDvf3mE2O2VxfYzadcHP7Ouv9FfZ2HjPLZmSZ4dq1awShIstTjMnJshSjNarQBjnhxtdVoE4J90Wf2qAUAEthYonRuqr9adTPtmL5WQLrNkZL81CfowMWppwT5q+pR5kv1vb0d1UdTfGTcJmRClcZF8xU9ZGsacuUMrSCmLW1DY6PhwRBwGAw5NY777KiIqbjCbODfcJkRpImyEBx8+ZN8iRlZX2tLGe1t0o7bGFNgC0EM2/RcW1ym6Hy/aznca9tRNxmzbdHzLnN1Dd72isX3IMmzbJiqhHld+62C2ZoGZW9WNekG7La/G3n5hVREy6XZTvx73Hd6uPfifq9q/Pn/66wEFxWO0VrPa9MrbuWnLHvEAvnLm4slxnEl5YjnIZ1UVN7Vl3nbzN/zaLV7SKazfNcturf1f+ul73svMXfnoRl973IuctwlpJisW6L516k7Retw1m4sAC4/dGKv1XBz+XghBtTfq5rqlptRRCqgqLFVVIFilgEhDgBTeeaXOcoqVCBqkyG1msMi10alQkSC5PJGO1NwFgyYci1xtRNxjhT6rLHZ0xCmp/gB2v9GhmHRC0n1KZZVqaqOhmcoPW0LGN+eQgRNvRdhJIuEjkIgrk3by4lHpbUC1gWcl2ZbkyeY/OKN6Vev7SmCTI1DYwkJWDGste8rv0xxpR9hxTIViW0h0GIVBIpJFrnrk5UGqggCAmDAJOkKCRBoNyE7vtESDRVHGuSpEgpUMqZ5/19jdEInDk8TdOa68DiomlLLaQFzBma1fLZS4lgXgNbr5+xbmNRll9oeLXR6ND5mCqpmCUzcp0jkeQ6J0lm5fmz6ax89vk0QRqLCpymW5STryZNRvhRonNdaqiVVKdeWK8dr2Nxc+TdJ7TWZaS71ppZzR1CG10GSlncu+GFWakqsvEUQ2Krd6u+QDvBsJSMiz4USKEwVmGKFI0CiGoL51pnnSyKePRwl529A7733e+xLiUH94YYK5lOJwynQ46HRyRJwiyfYmzGcBbR6gV847sfsHu4g4wtG90VtlprGKNRKiDzQSBeu+vnCGsQ1gveTiD22lYZyEJ8EqVFodakGqrv87TmM6wsdiE6SUmJlAFZVhNoahYMY2vPURTWibJfFdLX31abFAtY6YLmAiXn62dibGFhce4a2rlwKIkxM6Ry72AYRljrLCdoQyxr72RtsVhf7fHh1ntYEzAxsL22wdos5fqN68SRYDwacZxOGIxGrG2uc5RMuLG+wsrGGtnJcVnO8WRIW4bEQY8waCME1ZxCpYXXRmOsRolqqQmDYM7Pu27eT9KE3Lj3UdpqvCY6JzF58f5I2soJuaq4R7lAFmPElafJzLR8vkrUtMMipD7G62Mjqwn+otgM1q1YywIZ3Zj0m7/6HDNvQapDSFkK53UrjxDQbgW1+XrRn7mSDH3/qZrvu9uIyrl59Cwrlj/XtUEQxy3SGtdveW6hcKnmtzMEFXLq7hGL/vNee79okfKWQO8u5t2/6gJSfax49hBP7+bXN18OzKeArf+26BZWzo+1ceiDzpalaK0/f98+H3R6lrC4+H09vWVdqDv1bBZ+8+2sy1nP4i5wYQGwHXkfFUEQVAuYM9dV5tKSskFYlHI7VFH48KjiRQqVIpYBUaTKjg/8AlprjB8Qy1Cf0HMMqTGOnb5GGWGB1OaVj0r95SXDsInbUdmCX7A4L5Cl+VUI4fzajGF7Y4Ncz0ohLQiCcrdojcQYVV4ThWFZh0UOxNIHUAh3r0JB5dpaaGuMRRTaI2vt3OQqW1EpJdUHtCRDlQ7586hrIesvvQXy2oSnlJqbjOovRfkyW0tgKx1DvQ4aZ5Kswz/fRdQHeN0vsh65vahcPFv76Rb/MuVf7UVqxXH1DBZffiWLTYgmRSOVRBW5Uf0kCBD651mbHABsmpda6iAIyvOceJ/h1QdxHJVR8X7jUzbKOgoj7wvr4dwniv43lizPyLPcbZhqAqCf9MEtwnkhABoss9ydr3NdTuKube598W3SuhL6tKm0D1mWkevcccWpAKzjjVOB0/aEWQr/l/8NAL/9ve8wky6d2IPPPuPg63tEoWItFkgMR4cH7Hx1j9lsyixJwGq2rm3QVoojrZkOTsjSjNksoW0sP/rgAzfuLSRpUta1rlXWxk3+URjRabdRQUCapKR5Spqk7hnqHJ1VQoI2ek5ImwuaWXFCh8UWVH/V3FafnJM0qcqrC+pWYM6Ys4wpNsFeWPCaaGvJ8hwlBeWltlA7SaoAYStAKWQxLlTQoT6sBQrR6aCEJBTylOYKYKUd05WSo50d1tfX+OCdmxw93uHo0UOEdJvnXhSQWM3o8MAtbGsrTLOEva/vluVE7RadbhcZrRC32njfZv/ulM9H5+S5Zjablv1ljHGBezhNU6WgFLS7LTc2C6qv8v22ktAE5SQgRFCuD6eEslJoCeioStBbXPhFzcVGm8p1ScRhOU94NxO/PpV8tZwWbnwdFtets9x3Fs9bFIrqda1/P7e5XRCk6/P0WVmw5pQQNbctL4CHYUSvF8+1yZ/vnosljir3ojlhRBqkrN6ZOsIwnBOm622qP78kSQiCoKSMq5/n2zibzRBCEIYhcRzT6XRIkoTRaMRkMinXEH+OUqoUFv296hrrZW5Rnp2kziG8aGE55eZzAQHQC6K+rEWZZ9l1/pn58xfv+7S4sADYC6oHnuVZbRu9EBNZ801S0ml68tztAq00zk1H5CTW7QZ8Q+opos4KwFgcOP68SEWEwmmBbK1DLWDDyqlY57p80YUwqKBfK6z2MQzK/MZCCNIsxXFeGSwpvvFGV873Rlt07mZcHyHr21MnjxZCVj4zEoz3zxEUpq2iTVIRFDvWeY0YJHae09BlUAaBRJzxSKUwc8JSGSggQNV3l7n7PZZuAjBeC2kB44T/IAgIlcIaQ+59tWSlAbFmfrfpB279eft/dZEHuf5s47rA5s6ufa4L0zXNi6mizq2dfynmXrC5EkQxOTqhMcMig0pAL3efQsw9g7qmtqVChHUmWTMn+Fq08AIgxULl7hUGrblxXW2AgtKU59q0OKE4adEY184gqAuA82VR3DkveACdJqPaYAUWQlOVW+//+cnKB3i5++a5KTUmxhjEdFKe+3/+w39Bg9cAX/xk7s/feooi/tE//m/wX4vbiLBL1Oogi3fcD8RS6+k1ObVr1YLPd31zFgaB08YvbFiyPC8I+/NiYZdEUUgYRqWLDkAQKFQx/gWggurNPbX4F3Ovkoq4Va1vohWV7fBzuBMWRKGF8YLb/Dvs6+qEqvpccFbAUH02mhdA8lyfWvxhUVMoyvXFbx7rAsJZpsX6PKy1LjdUaZry5ZdflsLX9evXaxvaqh6Lm2BfvhCnNYD1371QFkVRee86/FipB3LW5YEsy8rD+6lmWUaapty5c4fj42O01qXwFscx7Xab69evE0XRQowCZXpKf7/6HOiFvjzPSdO0XE/SNGU8Hpf90el0yv6vB6bW4dlP/DPwAu4iTd2icLm4salbgC5qyn4SLi4AylqUWm1QzExeCkve1QqKZc+AtQGBdH5OJjcYYdAYEFlJ+RLHcdmJs1mlwVoUAKtdpjo1uL05USlZ8xESCBVWL4+qv2ReQ1P8Ve94Dbag4hBSEAYRQno/m4jS9yc4gxbaWNI0ccKeFIiw7o9T074JsF7Dg99JFdokKwhq6vd6PwRRQNnTtharajOMVcXOUs07StdQ513UWjM4GRDHMXEco3VemuutNaUpQ+BMqWHBy4isKHrqA9UIg5FVXg+/26lzNPoAHZ+AfnFXWNeoGCPASqIoQimXqsqXlcwKn7hCUCt39dZNoH4SnEzHxFHsNEVSEQhvRrAYrYnDFkErILGm1MbOmR8KAVBKiZCSiColocgNgZQY6cwRru8FudFEcascKy5jDYhYlvVVKigWFTfO3CJqCQJVmIRrgU7SjZssTQutnMZoJ8jrPK1piA1SOPOIDCTD0aRokyDLs9LUa6YJZpK4NiEYj8dkeTZnUvEafd+vfvFxC6JkNnXuBj+6/j7vPq60Qw3ebAy+/0N+5+//fTcekW7TX2iyPdy8GZZzxKIGpL7J8iY/Y0yp9fGbRr9oSyEIpCrLKt93IZjNZuUCvqjV0dqWrjj+Xl6oqTRl1QbIAgl2bv5avJ9vSxgG5SY0y3L8RmpRi7OoDayE0Pl5vVrsvfnytIbPbeJKlTBZVglL9TmrNA0rNx9Np5XGum51o3Ar8H3+O7/z22jtXK+m00nlWqLn56O61s/PF0EQIJVBysrtxGvtpJRMJpNSQPMCU124k1KSJAl7e3sMh0NGo1EpnHqzZ6vVYnNzk/X1dcIwpNfr0ev1+If/8B+WdfB94dc4T2vkhadFzbEfa0mSkPr5tRCml7nm+OuFEBXtXbGe+XO9kOoFci+c+nr4Z1BPiFH/bVHoHA6HpaJkOp3OCf2Lvv5nuRwsg7AXFCV/9r/6ZOn3MzQZfrG2pf+RW4Qtnisky7PSPGtkjpWp01ydY79eZqsvc4T6XZEFjC3NzItqdafOX15+nQZDyUpTaKTA+h2qFEhR8NoJiwoq529r6lFvBlukYTPWVj5LRb/U21EKpMKZm8sb1+optSkDcL32xp9jlFzaptxoMpuVkddnda2n5nGVsEhDaaKvi7Sy7qPitZqFhszWfJVEzTZrpEbLKprWm0z8JFGfIM8yjUyn02pXZAJ0LueFQmuZTiZOI6Vz0iQhSVJn0rROCzmdTJhMpkynE7QxWGMZj0ekwwnCazWxSCEJQrcoiDh0moY0nfOJi6Ko3Okt+uVlkxmT0agox5mbpJTkVhOvt8pnFoaR004UpMYl36V1Du1SilI7HIYh2hhUsTmK4pgwCLBF38xmU0ajcel6ofPK5O0ntTiKCOMIwqCk69E6r0h7i3cmUJVpyw0Ht2CGUYSt+eW4yTEhy2fl83aUTCFxFNGLBe1uTLfn6ENarVaZjUcKQaAUYaGx9MEnoZKIwGlUtra2AcuXX96h0+kyHmfle+O1FHXznizGUX3H780q9Y2GR33TWH+GWrv8xG5425KeKgwDtDAIBVEcubYEYbkQTWsmTW/a8v3n351ABbTb7bIOp97HmhastBQsmNz8RsFtvELCMPBDl8X9e6kNF4D3zZUunZ6rj8JmCZ1W7DbMgSo3y0KAisAaN5YEzj1CBQqjTdnPAKlSZd21kuVc6eaLyvpylhYeS9mOXGuX8cWaYmxacq3JsxwElZXBmJKHUEpBFEXkudcWmbk5ptSAG+HWIE77UqVpOufmUhcAdVDNr4vmUi+khmGIC2r0vmqn1yiP+oK8qIUMguq9q/pLFKwbtqZ9922lppWzxXvBKbh72FIJUT8nTVOCIKDT6RTzc0XjVjfhp2ml0UqShNksKftkXjFTE3hshrVVcKfvW++y4jVqfo73JtxWq1UKVLPZjE6nQ6/Xo9/vl/3ty8myjPF4TKvVYjqdnuIW9vPOonC5OEcsE+LyPC99KZfBB+P59tXPqwv3dYHQv9v1TGe+HF9GXXvr+zvLsnJzNJ1OS6HYj6coikqB12tC/bP5Z//sny2t/yIurAFc7/SWfp8oS1aMBa11qRq2FtJZFbFkdMUBZ2SGDipH+rpPTx31neOixBwEYSkDBVKibOGPmOdFKHAhe0pR+vPV4YmlPaT3mSl+LdOFFQ7mflpL08qsF8dxrQxdpW6zEITVPRcHRmkplGDq5Ko13ispJV7EmnvhBBillgqAQkqMcRO9VGouYGLuPCERhXlZCohVUC6s9aASkxe5nil29GGINAohFcaIsr1Zlpf3skpjw0r4LdX5xh0XUWGPRqNycp5ONYPjhPF4QpLMCqHAlrsrLxxkWV6+jG7RrJtnAlQgiOMOHRET2cqROMuyQssHs2lGmuXkWV5oQl0d00nGZDB1C421c8Kh0TmdsOV2kLMZrbCNsBKjM44OB+WQqvsQhWGIkII8c30Wxx3CKCQKQzd2/IRRBEBIESKl47/stHuAmFtUut0OUeGjK4QszGIxURwR9TrlYh0GqopwDAJUHKEKE0tWTFam2BFHYYixljzzwRey3ABJ6Uz+WptiAo+IJKgF528/KWtbEcKHYau22GrSdIKxhnBlBSEgHo6xUtHpuKATX1bdHDMcDivNknCblEA6k08Yhgjr0sclvu7Wcjw4KceoW0wqZ3Rdi0JtdxwHXr/fd9R80paLlH8uXotUFwzqKN9XIeaDa2q8lXWuyrplY1F4yPNsbiyW49oKjAnK+dUHbrl+dXOyLDavQRgSKBdkl45HTIym1W4xmSUuzSbFBjZJCQoBajJxpn0VOP9KFajCt9ZgdcUMkKJdoBbzG8Z5q4WtfF6ptH6ufW5BlMoFyHh/06AdOt9rDUKC1M4v2l1vmExdBHMch0gVuo1fsTZo7eesgE6nO7fhrWtZfB3q86vFWbVMbd2pC2ztdrvUKPrF22sf69aQSnMvS5PnMiwqOTy8cCALrWfdl61+XhjOu0z5hcFpNWWNUmv+PqbwmfeWM9+W+lp8ljnSGDNnAh4MToiikJWVFVQAQphSy+uFzCAI5vg+/Zj3fea/l1KWlHN1c7bf7Hmu0E6ng7WWbrcLQKfTKZ9hfaNe94f0z9Lfsx444jeCy8zuaZqW860/z5fbbreXKrG8IOuDVbzQ6YU5KWUZxDKbzci1RufevcFptf04878Nh0MGg0HZHrexgW63W/ZNmqZnMp8sw4U1gHv/24fl53pAQa4qYcl6qdY6f6FZXvmduQFU/CENVnlH0XlNkBXOOGtNZWZz/l1uAGpjih1tNdiDYvJwmh6DrPPGhTBP11DzPamd5xYlJwhkuubPJURtV2sdYZ1YMolLi6xFDNa9xryjutcelGZV6fIYe/NJqb6XAiWq9Ghu8ayZjYNFAbDQlFhLZiv1+9zCVJbtI13lqd+kEMz1lLCliaO+o5NSOt+y+t39eLAZk3To+BrTjKTYbXY7HTrdLkGdiw43Zoy16Nozqk9yTlgVbteV6yL4QFULa6GNyvOs8o1BFCZ7M/ei51mOzfJyIaGmRbYUgkDNj6ec8DAYaYnCiDAMamPZEhgwWe6i7oo6B0FAbjUDkvn+lN43B6zV5eTscmSHRFGMkiFSBriIzgxjTRE5KggCUWiinUbRzzt1DZRvszPHKKdhntNmFO0TAlNOrk5D4BfHuN0lCCInSBSCoZASJSAKDCpw6dnyPEMUmi5hNdbklUAiKy19HAalhdAJCK6fLQaEqe3CIY6jon+LtlJN0KUJzhRBGsWO3W/mRCCRgcAH28xNbQX9VH1hdwKSQkkfsOU2FraIunTcov4VseUuPQgLwaN45l5wK4U3C3VfTo9E56c0tV4LrNNKS1EXLgPXkGL8u7mjtEqo0L8ApcYHQCgJoY/WLiIshdPoSaXRRRBRlmW04ppwYrzZyz0LbyaUQmGtwBTaqMrqYUl1Wgm1tXfAGkfzVVTOBe0Vt/GaZYoaR2FYCYB5FQwVx3G5WfABcX4cefcEYwy2MBFL6ZglxpMxaZISBCHt9grtVptWu1U8Gzs3X4FnBjClBjOX1ebWrUOFgCskYRShlPfn1eX7UzfLOl9zU0SMC6yuPZs5gc+vf3rBbGeZJklBfu1Mk84ca8u+Ku+l3X1UoLC62pzWrTLOKqHKujrBykWPIwSGYrMgK0uAKPrIl+Cv94WL2ga7bv1XgSAIZOla4vtVCEGaZWW/e1/8eqAd1mm1wigs2UHSqbMmuSC0yvpXznd+DCxYrsrfagwlxphybXAWL1Uod7wvuMYHhdXL8/OBLzPP87LRsyQp3626wsorw6y3sgQKa2zJ11mnOvP1dMFPjj7OU9i5jbbGmspU7dcMz4mstWMGCIKAyXRClmb8j/+jf8JFcPEoYOLyIeW1HWlUG7fWWrJc4OlDZE2TZkyN30kHSF1FVWZpVnWwFGhryoAOP/Fq7TR8WJDIQrgpbOQBEHgeLVm93MIWfn7zvGiA0wzWTKlSzjvBV6ac+u7QkqtsaQYMWdsxuOvmg1fi2Dne2lzU6idKh+XyOqWK3boTwE7V20KeMS8A+gXeWOScNqLWpsI0GagAYUSl5QO09IK3Cyzw7VDK0opdpHOapOULbLQlsBXNgI8ULe+jI+xUk88yMAFWSqwMQAaVaUo4c7PONRiNVbUGhUHpJ6nNlFyPiu4SBDICcqc9rQlpEoMS9cEISjhlsChs6VEIJhBzk3/J3VjspvyzDWqvRmpTUp1gZE4qJUEUOLNxECIzjZ4aprNxoYKXdOMuUbtFO2rNZUmoBAKnCfI9aKzGSEilYTYdlxpiS7EIS4VUIGUtQtjKsrIiPW3iInPPIrGVA37doVpaUH5dEvXxKpimPRBeO1J1qbCGwGS+twjCAM+xmGEwQpTRwa5v3Vju5G5KBa/l9i+dKB3xXd1BJ26zlAuLLoSarBDulXIUOlrXdh/S95TTmJpC0zSvYYN2EINwBNNZnhWTqAu8kbWgJYJ6dJ0uTeYCAYGbs3IsRmcup4wKQGuE8Sam6t1CzDt1Z4rSXKoNWOsWy0Aq2u2KaD/XzodWWBc5rIybn1QrKLUCQkoSk89tMKp6O+2OLOgFBZVFxAagWu7vuNNCBdWCr3RYaim8MBoEiiyxJNOMLHfvm9doCQS9Vqd6FHUB0M6T1/sN0+J5i+ZEd417Fx3bhFtYM6PJjC43BXUNv0kyqG2q48Is6E2L3iRYeLEshRCVb1dqaprauTYVdEhSkqV5KTyAs6RoY5glCVpaF6yXp+hUVxtOiqhi79pQ01RlWc5wOCBNUpIsJTXOVcIYQxxF5WZhZWWFlfZK2dbtjc2SWioUFTODE5QKH0Dh/Lkt1UbPW0mMmGdtqMexdaIQJSphqvQzFlXwot/El/0o59fBqmCQUVz2vx8bQWHN8u5hFkf1FcWKtpTIFbuUbtM9ryX3gULQ8tpiFphBaoJ/4QPu5Zkwap0qCyCZJeX6hnCbQv+eKOXM2skCbYzbQFVMIM4VAcC508ymM0r6scJf0SmIqrkoTXOkqPxTjbUgHFftbJaRphlpls75FDqlwtJmLMWFBcCg5htwltLQS6Z5rlBaY3VNsKOeF7eQNorvsryKqrSyGKxakxW+Un7nvRgQUr9vaYK0ztfJQ5tKrWtq9A9WiML/7nSb5jVQtcW1EACXTSOLu8p6ef7hBsXOvjRVUmj2yvWwMgUFoUAqW5ZV+sThONzKHZeS1e7T2nPUvzUBsO4TInB+YsWfc5rVEOJWgBSyjJha1j5vQvX100qAlcRBmyzN0FlOMk7xiVJEsX0yRbtyrclr2tMoimtCd4pd4DYsqy4qPaurg1g4oVjYVDAnyFTyt0BXcg95zQtASVkF61gJuSTJqwCJbqdD2I3AKKbDlOFoAhbnOxdLRBSgcjk3IVf3zzBF2jm/E7fWYrUlCINKsCt2loEKQBjyfFbWVdbzBZtqYvMCme8GKaspr36NtE6w8Cc6LUVROAZbCNPeIRrcIteqzRh5klNqoJTTZkvtXC6UkkgrEMb5LZaLf+19UkVQUQUByiACg1XilOO2MI6gPTgj24eklAPc5qLUqggykWC1Qevc9blSTrA2qjQ1e18aj7pmdV5QsRjreDCVUKUGwWa2YGrx40ZURPa4DYn/K5QRKgycX6gQCF1JJ7Y21yorym2cta6BQgsXeZ7qpSynQkgi5TV7C1lxlCm5DZWUZdsxFp2lLsGJBhX4pInFXBhI0E5oqW+OanF0LoV1OfxESbhurTOj+RfPSDvnKzi/nthCEyXd4m0L/zQhUEFYli9qu/AoqDgPg0DRa3XJetqNZG85SoybK5f4zljrfImtccQzqTXlO1RPJqBzpz21OC5QbyaHusuDJC+CqZQKiMOQ9V4fX2AYVmTsUjirgvft/ODd94v53G2oKLRyWeGW4pUDZX+JeWE6ROB7wgti3ndbiACti+AI4eqghAQliEo6HMgLZgthIZ/m5HPWknKokNocKypf0YofsKKTEqIibXdCULUZ9Rota90zi8JqTBnlNNfWWJeMasmaZoxlmkzLDWz9NRC1OAC3EalSBeq8Eu61NuiCJivPNfkZrmj1tjv/wwme3sj7DAeFZarso8JSiajJJF7pYh0bhPOrnJWm/rqFBlzf5RSuUzqniiyvNPu2cNnK87xIrJFzlny2DBcWABdpIs67iZTSCYBpTXWKWZDKnaBhEUjjI8X8jwah3YE2CF04rEtZRkvWO1sLShOitZZMu5nWWovJhZ9lsLra4VjpNFl+8HizB0A+58uka1K5JasJgHPTVi1iFuz8rqMQdIIyGq1S8VTmFEpTjVIKoQygi0nQDVRbmC/qQSCqIEZ1/a7mntM8bLmr9y8eOHMS0UKgTNEMbTOkNKUJNq8J1rZm0veTDK7G5NgyWMRPDHErLoXbOIpBUIbgZ3nOOJ3VhNDKF9LaHFNkN6l2/oWAWtNolYEM4tR84ExqeM1LtQlwE1SlHdG1eJwgKEx8gBUGpC6fsdY5s3DGMB4TKMV0MgGEo6/JJMlQk02nTPK08o8SshBQnauAkM68IpUkiuLK/0NWfWaswfvoOB+vokFiXmNdd8mYM48A0ywptZCyprEQ1gX/+CLLDYYAqyZVEFTt/whLIpYH7iCo3AuKgA83KQqMTZ0GsBD+BF6bq4iDsIyMj6MIlEGGYCRkaVI4ec+c2VI6DjxBVDNuVZs0LXL0XP2qkRBYAaai56icwgMkjvfNCQzzG8FyITFVKj6LxdikLMM/A53nWCmQYY24uVgEBIJKx1H0Vc0dIzDVC+jNd0KIKuqeYj4zptDqSDe+fN/XX2Ah5gSsuY2p1EjlFt92u02/3y/mSU0ynBSckXnZfqUUyBCDwpvdZ0OXBcj/Xd1WnKpHOXrq1hE/DmquB+WcaitzXRgElcuHdPO8f6Tet1gAZNppiYQL5rPWmfLTLHMuJ4WJ1tR8MOdgvTbOWa4yKjqbeZ87QRi65yG0ROpKGJNWFBrvgG6nWwYbYCwmodSAj0bjUgPoN3dK5cyE4MQOSpLvmSnMkUKUvJzeZ26Oh9TakrYlRJZuQ249ccE0dfL40sRbRD4bKeayCJWRxIBN80ox49dmXJBRvsw6VXw2RQYeKZanbbTWzQVSVvNv3QRe+XJqpLbFJtpJUqUyxxqmk4WkDLVHWz5nO1+/NEur16F4n+rRub7vRO1fr4DydUuztAzOUVKWGW/qMoTXLPr2JklFHxcGIWEUluZk71OZ5XmpTbXFfXXh/5drXcoNthD+/JzlfRK9Am4xCv08XFwArD3IpSpe6hOUG6ihUuVDqXeitQJjCgHQWkIqMt6pTqudiZBEQpErN4izNCWMY6IwKv05oBAAvRLMWKSwhWrV0JKOWFQbjSQoqZuMsGS1zpZG4Stb1VVgDNjcC2kWXXN6rvMAnu6M6qOUhZYhA52Z2uK1kLbLOOFJ4yZpp/WpwtFLbVmNuNn7bgSBj3DyD8n7W/lnY+Z+qy+gec33bR5VCjSXsqzaIc1tCGq+VUZYMltlZfGTjiomqTiKiIuIr2Q2I0lSkjRhMJss3VS4wIvaol6rZxRGVQon64RZr+Gsa38c8e7p1vkIYN8PWe1p1ndjSkJtgzpXTxlUEZHz2mIwc/esJighDVJVfi3VWLHObOeF0Foebfflk19svzkC50s7rWnho7AiVnXuArbiBPPzpQAtdRUEVS9cAKp6FrLWr6rGbymEoBW3iOLIad1FVmSsCMrxrlRAiCCiiHQuItqyLKXb6xF22kxmU05OThiPxzU/IYG1tU6y1UKak5LatKysqAkgNs1Roohq9T6FrscRNig3Up542rfPC2PaVAuEENYJUeVTq/n+SMqgEuezrEvBwOqKYHtuDAHhKU65QkhSspjo3TtaBdBYpoUPpovmrfvW1saacBuTUqOVTmnHLqCov7JCdCtgPBwxm07RScpsOiu1Es63LEDLAF34brpIaacJ01rPzYF1VoRFmMKc78/zPG1CCrI0K1fc01trt8jlWIxkyfvtNNmBqPVDYYY31hT0LAsuC3iLQfkAa0TQQBw6VwUpkLKaR6QQ5LXxX0rfFBsAm5GR14Qg3LxUm/PzmiYIXJBNK3aBUaVgJ+atEXVXF0cIX9GF+fU2UAEmy7DZPKecH4Oz2YwojIjiaG5jroXbbPlrsqzi+FXWlppWW0Zpu41lVssRn8ySBQ5Uyj6es9oV71GdisW3oRS+rHUWA6+gqLFhzGXZKfvfYZbMqrml5q9ofHac4twsq1kl6z6C2pRk83Pli+JdrQuXtTFef57zMo6dExonk0l533q2M98fFBufOg2eq0L13OuZ0Mr74F3oiv5a3IQ9ARcWAM+KVjrrPEf9UPEFKqqIOGucAzq4jlJRC62cpGsSQ17sfkIV0ArCUtWaFAtiIH3alaKMUypghbagrcBo6VZi43cexTmGucy0dUdPaWy1ehvKge/8tlQpAGLmnUWX9gfO3FOPGKw/yHJ5FyDDahKXhQYwF9XuRFuNsjlW56XpR1qJRCJ1gLQVjUsZ5VxTXdcnEV8HS+EDWNTDDSyvpTA1P0Rbd6UkT2vM8uX/3AdrAQ1WQy50oVHNmInZnBljliTlrnuml/sz1V+k05h/YesO+XMLhPTaN8/DWGiFa/4VThCuvdi1l1liUKUQOl+f1FajaHHCq2POPwqNlKaoa7XxAJBK451egnomECsw+izt7nJYnH+PLz0IVCUwa1OlsKvV2Y2HzGk9WZh4JIiam4yPRAMIhSDwe3UhaBX0NcYaRKugDwrchtBrzmKp6IgiijiMMNY4bkEBVknSPHP0MzVaBoFAipj6wuvHdS5y8sImaaxxVCIFYmQZCet2/P4dFLjsJlXkfLW5mvdpq567BZGVz8lRSLnzUmFIljktUfjylZsAUW7QJG6z618hT8GT57kTJkv/2GretYVw74JwvF9tJdx4/yiBICqJ1S3SZHRb7tmsr6+jUsHh0SGT0RilLaPhkOFw6DItFGXnQUhCZZJP08SR+2t9Ks3iee5BHnVqEB8A5Oo9T8c133kKGRU+m1LOMRxIYwnkcn7Y+jOr+5qr2uZqcY6xoSoEfl3NF5z/fi8Kwv6dEdb52opChnDUKvPzhd+81918tKD0R/ZBkK6uLGmT+2zT3DmNltfYcmw4DWCV5aistyg0q0UZ/r0QuPFaysg1C5exlpmp6N7qQlUd3jxc/8Zr25zQY6v+r62J9UxEQpvSr8MUWtrT/QBZllYUVwvjpz4kz1pLzltn6mvB4hhY1FxWgvlZz2ze2ugsn8vPOwv1ui66rdTrehFcWABMa/5f9Um33tf1igkg0NXvZkEDaE3lGyCMLY9gLjVQIQELi5CKsNWuzLT1yabIjOBu7NTK2kJuBbl2Aok3W/sX0e0KZW3yMqXkLG1NIrKV75QXAP3uxC0e9V6a37n6LYTJbE0+Eigv9tW0Nb4ddu5q4wI2NJi04moSVCYegXA5M61wB9WC5V7Momb1qFZjqhy/uEnAn5fnlaColHN6Lqo6B11bKNWccON8+4wuDuspPHQZZee6rxZNKEBnyylrXITe0p/m/fpwPmpCWfDmfX+epFyspZKIOqdi7TxUFf1HLkuB1xiN8fWzds5UkAlTM7HK0m+TYq9RFmerCFljM0CX2rfqmVukymuCRc0P0QiyrF7ictjaLtkJc5WmY44bMneuFfWMCL7iWqaYQgB0jsreb9aiay913Q0jRDozK4XWqRUTR04ApGWRgSzb6wSAgAhB20riyNG3TGfTwtlcM8t9Fm+vifUaP0d/4uHS6rlNhBYZRlYakCoqHAJEISRU6RvdiRJhw9LUWDdHzlOyVPtqi0Xbml+qrd7bBMPUVg7jqljYtdZEQpY+Wt4Fw5cc1uYiUWgehZCYIlWkP7u+85/lKXIJfYyP3i2fRRyXzymWgn67RRTF7N17zIMv7jEcjUhnCSGOfmI2m5UkvUoF6DBgZm3JTaZzU44LR4XlhfFqMVuEq1u1Uz9bUFz6NTmGHFMJgF5j9/9v71yW4whhKHoFPU45//+j3iROD0hZCIFoPyvLzD2LKbs87gd0o4uQhLlQqUOsvjGiu4HKF/SuuDcDfvVzjCf7jhr/JAABPGGJ5NhKDbgkRGU+8QBembUIW8dNPG4zGib+L5YGR3Ntx9sF4M5hcyjahIrC49DnvSev+XYb1/aSlTyRw1a2TpIR2hO/Nk1JIOskkUmb7duH5/2ArV1TvOKVqC6yzr2u57MJz3tCzACvrPDuapdtExsf/YZtGxnc12ObAS0J8Gud2q/4tgD0QO51oYGorcjrywyij1ggn03d13q7AZpOHVvLmXkQbsilKFBpw7qb2jLQRZaYU8NVJRQT3KSgyR1WOmwY4HCJN1XE4zOXYsdtnNahscxbDFp1/14M3Ml5o/bx4JdfjoKyDJCIL/lqHyUWlrfG0GC2Cht384yhlkpJAP5gdO2Qu0CHszMG5zkjAhBldKL/thlvtofZ3d3fCr9gn2nrFBf3fqKV1bJ9pOiXw5N7ejKW+di3kkvQxKMPNDV0eV8c5ut28eWJFYaO2xw1BDWX57E+nxUBlkcMgP7piKswVciIhWnacO/nelrGg2fi3ysiI5vSgEvtxe2eYqwZotY93fv3a5HZMD0yoVtDN4N+If7W+dbn1rc1bayuBunmW/1dBkytOgWgz7+S9/9eEP0jdYkiFUGbXtaCUwE9G2otOKxCjgqp7oGUUlCOUU9SKu5WoU2g7cnfg15R9Qk1vGLRHmPJUXNNuS6wXmBd4V2rs17dbljECzvL+UbYAW0z8pnsrcoeI2id722u87kJQABRt9LM8HssuYd3L3usl6fFx70QyZJKkqxHyD9bfx17V1/3UAeOWxkenwr5cSIErJrBbq94fv6Jl5eXUYDZx4qIkTPz7PyI6bSj4ITvvHOeHe30LQFrvUGP3QsfMbLeHpv6QuwwcbfTV1/GeBxhLK11+FaWbvZa6540USqanjBEKSAXx7Nr7YBgLautt27vy9y1XS4hGsmon31lIufltJq83H6vuwiK56O1tryG5sLC67n5eB7XIWOZ+Y14kj3Rb1vSh6EiJRSM8RVmeMV1+W/E1sOX0OfqkKwxxsxmYXY//hLpAswBZLMfwJYRPJc0gS0M4CrEpiC93LCZ7bYKhhqJQPl4SN5YNVhbnkJP7or3qUJlTRA+SoycSRRxdLn+zX/OiZbbBNv2+FVVm+rQ4E0cO3JN7aOKnrt8e0QNto0dq19yqaGr/dZU6UGKbGXwvuLbdQAJIYQQQsj/wffTRQghhBBCyH8BBSAhhBBCyINBAUgIIYQQ8mBQABJCCCGEPBgUgIQQQgghDwYFICGEEELIg0EBSAghhBDyYFAAEkIIIYQ8GBSAhBBCCCEPxl/oj/YDAioXuQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#############################################\n", + "# Unpack and plot predictions\n", + "plot_skeleton = True\n", + "plot_pose_markers = True\n", + "plot_bounding_boxes = True\n", + "marker_size = 12\n", + "\n", + "for image_path, image_predictions in zip(image_paths, predictions, strict=False):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + "\n", + " pose = image_predictions[\"bodyparts\"]\n", + " bboxes = image_predictions[\"bboxes\"]\n", + " num_individuals, num_bodyparts = pose.shape[:2]\n", + "\n", + " fig, ax = plt.subplots(figsize=(8, 8))\n", + " ax.imshow(image)\n", + " ax.set_xlim(0, image.width)\n", + " ax.set_ylim(image.height, 0)\n", + " ax.axis(\"off\")\n", + " for idv_pose in pose:\n", + " if plot_skeleton:\n", + " bones = []\n", + " for bpt_1, bpt_2 in skeleton:\n", + " bones.append([idv_pose[bpt_1 - 1, :2], idv_pose[bpt_2 - 1, :2]])\n", + "\n", + " bone_colors = cmap_skeleton\n", + " if not isinstance(cmap_skeleton, str):\n", + " bone_colors = cmap_skeleton(np.linspace(0, 1, len(skeleton)))\n", + "\n", + " ax.add_collection(collections.LineCollection(bones, colors=bone_colors))\n", + "\n", + " if plot_pose_markers:\n", + " ax.scatter(\n", + " idv_pose[:, 0],\n", + " idv_pose[:, 1],\n", + " c=list(range(num_bodyparts)),\n", + " cmap=\"rainbow\",\n", + " s=marker_size,\n", + " )\n", + "\n", + " if plot_bounding_boxes:\n", + " for x, y, w, h in bboxes:\n", + " ax.plot(\n", + " [x, x + w, x + w, x, x],\n", + " [y, y, y + h, y + h, y],\n", + " c=\"r\",\n", + " )\n", + "\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wO18A_3m5Spk" + }, + "source": [ + "## Running Inference on a Video\n", + "\n", + "Running pose inference on a video is very similar! First, upload a video to Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 92 + }, + "id": "d9a7gSe15bCa", + "outputId": "698b180c-cd8f-4d17-9c71-f8e58f93631b" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor-dancing.mov to taylor-dancing.mov\n", + "User uploaded file 'taylor-dancing.mov' with length 1415324 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "\n", + "video_path = [Path(filepath).resolve() for filepath in uploaded.keys()][0]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left and define\n", + "# `video_path` yourself with right `click` > `copy path` on the video:\n", + "#\n", + "# video_path = Path(\"/path/to/my/video.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "I885B01359qu", + "outputId": "0affdeda-a10b-4849-b3cd-edf1cb202b52" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:02<00:00, 25.37it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:01<00:00, 53.25it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] + } + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of individuals to detect in an image\n", + "max_detections = 30\n", + "\n", + "\n", + "#############################################\n", + "# Create a video iterator\n", + "video = dlc_torch.VideoIterator(video_path)\n", + "\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights,\n", + " box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted for each frame\n", + "# in the video.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for frame in tqdm(video):\n", + " batch = [preprocess(Image.fromarray(frame)).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels, strict=False) if label == 1]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the top N bounding boxes\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "# Set the context for the video\n", + "video.set_context(context)\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images (in this case a single image)\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(video))\n", + "\n", + "\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={idx: img_predictions for idx, img_predictions in enumerate(predictions)},\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)],\n", + " ),\n", + ")\n", + "df.to_csv(\"video_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "altka3NGB_su" + }, + "source": [ + "Finally, we can plot the predictions on the video! The labeled video output is saved in the `\"video_predictions.mp4\"` file, and can be downloaded to be viewed." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xRWxH0gO6oPg", + "outputId": "c2cc9025-7741-4403-d5cc-c62470a4ba74" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:146: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration of video [s]: 1.57, recorded with 51.7 fps!\n", + "Overall # of frames: 81 with cropped frame dimensions: 828 768\n", + "Generating frames and creating video.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 66/66 [00:01<00:00, 35.27it/s]\n" + ] + } + ], + "source": [ + "from deeplabcut.utils.make_labeled_video import CreateVideo\n", + "from deeplabcut.utils.video_processor import VideoProcessorCV\n", + "\n", + "video_output_path = \"video_predictions.mp4\"\n", + "\n", + "clip = VideoProcessorCV(str(video_path), sname=video_output_path, codec=\"mp4v\")\n", + "CreateVideo(\n", + " clip,\n", + " df,\n", + " pcutoff=0.4,\n", + " dotsize=3,\n", + " colormap=\"rainbow\",\n", + " bodyparts2plot=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " trailpoints=0,\n", + " cropping=False,\n", + " x1=0,\n", + " x2=clip.w,\n", + " y1=0,\n", + " y2=clip.h,\n", + " bodyparts2connect=bodyparts2connect,\n", + " skeleton_color=\"w\",\n", + " draw_skeleton=True,\n", + " displaycropped=True,\n", + " color_by=\"bodypart\",\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "include_colab_link": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb new file mode 100644 index 0000000000..7e77e835d2 --- /dev/null +++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb @@ -0,0 +1,2228 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5SSZpZUu0Z4S" + }, + "source": [ + "# DeepLabCut Model Zoo: SuperAnimal models\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1616492373700-PGOAC72IOB6AUE47VTJX/ke17ZwdGBToddI8pDm48kB8JrdUaZR-OSkKLqWQPp_YUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYwL8IeDg6_3B-BRuF4nNrNcQkVuAT7tdErd0wQFEGFSnBqyW03PFN2MN6T6ry5cmXqqA9xITfsbVGDrg_goIDasRCalqV8R3606BuxERAtDaQ/modelzoo.png?format=1000w)\n", + "\n", + "# 🦄 SuperAnimal in DeepLabCut PyTorch! 🔥\n", + "\n", + "This notebook demos how to use our SuperAnimal models within DeepLabCut 3.0! Please read more in [Ye et al. Nature Communications 2024](https://www.nature.com/articles/s41467-024-48792-2) about the available SuperAnimal models, and follow along below!\n", + "\n", + "### **Let's get going: install the latest version of DeepLabCut into COLAB:**\n", + "\n", + "*Also, be sure you are connected to a GPU: go to menu, click Runtime > Change Runtime Type > select \"GPU\"*\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "AjET5cJE5UYM", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "290a589f-a063-4933-d315-e13052ec1024" + }, + "outputs": [], + "source": [ + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5h0vq6E50Z4W" + }, + "source": [ + "**PLEASE, click \"restart runtime\" from the output above before proceeding!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LvnlIvQm0Z4X", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "ef4fd2ed-4569-41d4-b78a-8bf5ae9a0e6b" + }, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from PIL import Image\n", + "\n", + "import deeplabcut\n", + "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions\n", + "from deeplabcut.modelzoo import build_weight_init\n", + "from deeplabcut.modelzoo.utils import (\n", + " create_conversion_table,\n", + " read_conversion_table_from_csv,\n", + ")\n", + "from deeplabcut.modelzoo.video_inference import video_inference_superanimal\n", + "from deeplabcut.pose_estimation_pytorch.apis import (\n", + " superanimal_analyze_images,\n", + ")\n", + "from deeplabcut.utils.pseudo_label import keypoint_matching" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UeXjmtu40Z4X" + }, + "source": [ + "## Zero-shot Image & Video Inference\n", + "SuperAnimal models are foundation animal pose models. They can be used for zero-shot predictions without further training on the data.\n", + "In this section, we show how to use SuperAnimal models to predict pose from images (given an image folder) and output the predicted images (with pose) into another destination folder." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FvFzntDMxPoL" + }, + "source": [ + "### Zero-shot image inference\n", + "\n", + "If you have a single Image you want to test, upload it here!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NbDsZQfsxPoL" + }, + "source": [ + "#### Upload the images you want to predict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c4yfTj7r0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "image_path = os.path.abspath(filepath)\n", + "image_name = os.path.splitext(image_path)[0]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left\n", + "# and define `image_path` yourself with right click > copy path on the image:\n", + "#\n", + "# image_path = \"/path/to/my/image.png\"\n", + "# image_name = os.path.splitext(image_path)[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Jashzdjb0Z4Y" + }, + "source": [ + "#### Select a SuperAnimal name and corresponding model architecture\n", + "\n", + "Check Our Docs on [SuperAnimals](https://github.com/DeepLabCut/DeepLabCut/blob/main/docs/ModelZoo.md) to learn more!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uH9LXig90Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "# @markdown ---\n", + "# @markdown SuperAnimal Configurations\n", + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")\n", + "\n", + "# @markdown ---\n", + "# @markdown What is the maximum number of animals you expect to have in an image\n", + "max_individuals = 3 # @param {type:\"slider\", min:1, max:30, step:1}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OmJtVmHq0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "# Note you need to enter max_individuals correctly to get the correct number of predictions in the image.\n", + "_ = superanimal_analyze_images(\n", + " superanimal_name,\n", + " model_name,\n", + " detector_name,\n", + " image_path,\n", + " max_individuals,\n", + " out_folder=\"/content/\",\n", + " close_figure_after_save=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6VEjHu-00Z4Y" + }, + "source": [ + "### Zero-shot Video Inference\n", + "\n", + "This can be done with or without video adaptation (faster, but not self-supervised fine-tuned on your data!)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qGoAhxZOxPoM" + }, + "source": [ + "#### Upload a video you want to predict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PK3efA0I0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "video_path = os.path.abspath(filepath)\n", + "video_name = os.path.splitext(video_path)[0]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left\n", + "# and define `video_path` yourself with right click > copy path on the video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JoA-RATSICj_" + }, + "source": [ + "#### Choose the superanimal and the model name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OiRAP9XD0Z4Z", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "# @markdown ---\n", + "# @markdown SuperAnimal Configurations\n", + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")\n", + "\n", + "# @markdown ---\n", + "# @markdown What is the maximum number of animals you expect to have in an image\n", + "max_individuals = 3 # @param {type:\"slider\", min:1, max:30, step:1}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Zv3v0QgSJNOg" + }, + "source": [ + "#### Zero-shot Video Inference without video adaptation\n", + "\n", + "The labeled video (and pose predictions for the video) are saved in `\"/content/\"`, with the labeled video name being `{your_video_name}_superanimal_{superanimal_name}_hrnetw32_labeled.mp4`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "poqynL0UJTBp", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "_ = video_inference_superanimal(\n", + " videos=video_path,\n", + " superanimal_name=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " video_adapt=False,\n", + " max_individuals=max_individuals,\n", + " dest_folder=\"/content/\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Z8Z5GSti0Z4Z" + }, + "source": [ + "#### Zero-shot Video Inference with video adaptation (unsupervised)\n", + "\n", + "The labeled video (and pose predictions for the video) are saved in `\"/content/\"`, with the labeled video name being `{your_video_name}_superanimal_{superanimal_name}_hrnetw32_labeled_after_adapt.mp4`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5mhOmtzw0Z4Z", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "_ = video_inference_superanimal(\n", + " videos=[video_path],\n", + " superanimal_name=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " video_adapt=True,\n", + " max_individuals=max_individuals,\n", + " pseudo_threshold=0.1,\n", + " bbox_threshold=0.9,\n", + " detector_epochs=1,\n", + " pose_epochs=1,\n", + " dest_folder=\"/content/\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "br3pwGf40Z4a" + }, + "source": [ + "## Training with SuperAnimal\n", + "\n", + "In this section, we compare different ways to train models in DeepLabCut 3.0, with or without using SuperAnimal-pretrained models.\n", + "You can compare the evaluation results and get a sense of each baseline. We have following baselines:\n", + "\n", + "- ImageNet transfer learning (training without superanimal)\n", + "- SuperAnimal transfer learning (baseline 1)\n", + "- SuperAnimal naive fine-tuning (baseline 2)\n", + "- SuperAnimal memory-replay fine-tuning (baseline3)\n", + "\n", + "This is done on one of your DeepLabCut projects! If you don't have a DeepLabCut project that you can use SuperAnimal models with, you can always using the example openfield dataset [available in the DeepLabCut repository](https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/openfield-Pranav-2018-10-30) or the Tri-Mouse dataset available on [Zenodo](https://zenodo.org/records/5851157)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yPy5VgDDhD6o" + }, + "source": [ + "### Preparing the DeepLabCut Project\n", + "\n", + "First, place your DeepLabCut project folder into you google drive! \"i.e. move the folder named \"Project-YourName-TheDate\" into Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "SXzBBV8ehDR9", + "outputId": "90d61c19-400b-4e5d-8ac9-63680d72cdb5" + }, + "outputs": [], + "source": [ + "# Now, let's link to your GoogleDrive. Run this cell and follow the\n", + "# authorization instructions:\n", + "\n", + "from google.colab import drive\n", + "\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-QmTftBMo4h6" + }, + "source": [ + "You will need to edit the project path in the config.yaml file to be set to your Google Drive link!\n", + "\n", + "Typically, this will be in the format: `/content/drive/MyDrive/yourProjectFolderName`. You can obtain this path by going to the file navigator in the left pane, finding your DeepLabCut project folder, clicking on the vertical `...` next to the folder name and selecting \"Copy path\".\n", + "\n", + "If the `drive` folder is not immediately visible after mounting the drive, refresh the available files!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_iFFEYAB7Uum" + }, + "outputs": [], + "source": [ + "# TODO: Update the `project_path` to be the path of your DeepLabCut project!\n", + "project_path = Path(\"/content/drive/MyDrive/my-project-2024-07-17\")\n", + "config_path = str(project_path / \"config.yaml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HZTG3Eo475w0" + }, + "source": [ + "Then, use the panel below to select the appropriate SuperAnimal model for your project (don't forget to run the cell)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "t8NtCy1Jo0bu" + }, + "outputs": [], + "source": [ + "# @markdown ---\n", + "# @markdown SuperAnimal Configurations\n", + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BPvoL9uZ0Z4a" + }, + "source": [ + "### Comparison between different training baselines\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eVmpaLdB0Z4a" + }, + "source": [ + "Definition of data split: the unique combination of training images and testing images.\n", + "We create a data split named split 0. All baselines will share the data split to make fair comparisons.\n", + "- split 0 -> shared by all baselines\n", + "- shuffle 0 (split0) -> imagenet transfer learning\n", + "- shuffle 1 (split0) -> superanimal transfer learning\n", + "- shuffle 2 (split0) -> superanimal naive fine-tuning\n", + "- shuffle 3 (split0) -> superanimal memory-replay fine-tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WofR2jytxPoR" + }, + "source": [ + "### What is the difference between baselines?\n", + "\n", + "**Transfer learning** For canonical task-agnostic transfer learning,\n", + "the encoder learns universal visual features from a large pre-training dataset, and a randomly\n", + "initialized decoder is used to learn the pose from the downstream dataset.\n", + "\n", + "**Fine-tuning** For task aware\n", + "fine-tuning, both encoder and decoder learn task-related visual-pose features\n", + "in the pre-training datasets, and the decoder is fine-tuned to update pose\n", + "priors in downstream datasets. Crucially, the network has pose-estimation-specific\n", + "weights\n", + "\n", + "**ImageNet transfer-learning** The encoder was pre-trained from ImageNet. The decoder is trained from scratch in the downstream tasks\n", + "\n", + "**SuperAnimal transfer-learning** The encoder was pre-trained first from ImageNet, then in pose datasets we colleceted. Then decoder is trained from scratch in downstream tasks.\n", + "\n", + "**SuperAnimal naive fine-tuning** Both the encoder and the decoder were pre-trained in pose datasets we collected. In downstream datasets, we only finetune convolutional channels that correspond to the annotated keypoints in the downstream datasets. This introduces catastrophic forgetting in keypoints that are not annotated in the downstream datasets.\n", + "\n", + "**SuperAnimal memory-replay fine-tuning** If we apply fine-tuning with SuperAnimal without further cares, the models will forget about keypoints that are not annotated in the downstream datasets. To mitigate this, we mix the annotations and zero-shot predictions of SuperAnimal models to create a dataset that 'replays' the memory of the SuperAnimal keypoints.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AgIsUu6v0Z4a", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "imagenet_transfer_learning_shuffle = 0\n", + "superanimal_transfer_learning_shuffle = 1\n", + "superanimal_naive_finetune_shuffle = 2\n", + "superanimal_memory_replay_shuffle = 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "kuKcxM8F0Z4a", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "c7df2943-1e2c-4b85-c20d-8b94a8aabd75" + }, + "outputs": [], + "source": [ + "deeplabcut.create_training_dataset(\n", + " config_path,\n", + " Shuffles=[imagenet_transfer_learning_shuffle],\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_6RncQbr0Z4a" + }, + "source": [ + "### ImageNet transfer learning\n", + "\n", + "Historically, the transfer learning using ImageNet weights strategies assumed no “animal pose task priors” in the pretrained\n", + "model, a paradigm adopted from previous task-agnostic transfer learning.\n", + "\n", + "You can change the number of epochs you want to train for. How long training will take depends on many parameters, including the number of images in your dataset, the resolution of the images, and the number of epochs you train for." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "7ed11ae2a4be462da84ff716e0725af0", + "0f0ed94a863f49b9b85d0a18fa8ce2a5", + "343f2670d37c4bf18859238c3d81d419", + "d104ae21091e4f10a7de18e191b9f04d", + "5dcbd8f3fb6148cca6cfc72b20ce49bd", + "e1675e53ca9a4da8acf6c16fba7a2578", + "3d2996e10f96404baf24d2c4215b75a1", + "b988f87e676840ee98daa3d996c9ddbc", + "1779b84e748b4989a8ed53434c30016f", + "d37cf6fe7c444bc2a2568c3407389ea8", + "2cef5e028d2e40a6bba7400be922d0c2" + ] + }, + "id": "H2z8kM340Z4a", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "75cc2c95-2ac7-4354-9134-4847937e15ce" + }, + "outputs": [], + "source": [ + "# Note we skip the detector training to save time.\n", + "# For Top-Down models, the evaluation is by default using ground-truth bounding\n", + "# boxes. But to train a model that can be used to inference videos and images,\n", + "# you have to set detector_epochs > 0.\n", + "\n", + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=imagenet_transfer_learning_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J-udMck7nDbG" + }, + "source": [ + "Now let's evaluate the performance of our trained models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "TDHMdKz4m_16", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "1d38fb84-7f4c-45d1-dbcd-fd7117ca4dad" + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[imagenet_transfer_learning_shuffle])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0GIFWU-MxPoR" + }, + "source": [ + "### Transfer learning with SuperAnimal weights\n", + "\n", + "First, we prepare training shuffle for transfer-learning with SuperAnimal weights. As we've already create a shuffle with a train/test split that we want to reuse, we use `deeplabcut.create_training_dataset_from_existing_split` to keep the same train/test indices as in the ImageNet transfer learning shuffle.\n", + "\n", + "We specify that we want to initialize the model weights with the selected SuperAnimal model, but without keeping the decoding layers (this is called transfer learning)!\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "wOSdZQtOp8qa", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "ea721606-ea9f-444b-cdae-f62cf0ad30be" + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=False,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_transfer_learning_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3qFxlRHixPoR" + }, + "source": [ + "Then, we launch the training for transfer-learning with SuperAnimal weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "9a996c8dc3b34bc5b8805b3687e22b27", + "d012b421c189412dabeac84cba4164a7", + "1abff22a7c9a416d9166e6b150612171", + "7271412c1f0141649a7300dbce2b003c", + "3c011813d7cb48588a8d236785d9c24f", + "3ea385fe815f4e50a0b81ec299040314", + "fe59f6c5ed7b4e2cb87bb60224acdaba", + "04370d8302c04c5ca6a351383126193f", + "d67c4871543e405fbb576a55f8c9048a", + "a6cb25fa67ef4733a720960b3fc8213c", + "b73b1b64620d492dbc4eaf4bd83ca23a", + "dccbe277cc084ed6aa0b329067b5c69c", + "c8b57833d3f946abae69b84075345a54", + "bee292213d8645618536fcdf6a491d83", + "fbbc8c5b20c7423fb21b74296e0eeb28", + "ff0c737c49624b1ea27588611951fc84", + "42874cdab4be4dc38b0c33775b27d98c", + "e3a185abf8a04edabf32d58bdee10dd1", + "7cdcbbf9cb694dbf949e8b7eea8e7836", + "2ec06260b237411cabd3de7c37e03b1b", + "9f8009429aa34b40a65c998230f20c99", + "2a3abfe7867641db9fbfe3ee76854bf4" + ] + }, + "id": "W60UgRQWqghn", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "18b931b8-98f4-4539-bf82-1910ff5b7f70" + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_transfer_learning_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XzOWKiOixPoR" + }, + "source": [ + "Finally, we evaluate the model obtained by transfer-learning with SuperAnimal weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "jpO3aIAIsWbz", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "30415e5b-8011-4651-af77-a781ea2b5af7" + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[superanimal_transfer_learning_shuffle])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_Es6RR-_0Z4b" + }, + "source": [ + "### Fine-tuning with SuperAnimal (without keeping full SuperAnimal keypoints)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6oo9oJ8XyZrn" + }, + "source": [ + "#### Setup the weight init and dataset\n", + "\n", + "First we do keypoint matching. This steps make it possible to understand the correspondence between the existing annotations and SuperAnimal annotations. This step produces 3 outputs\n", + "- The confusion matrix\n", + "- The conversion table\n", + "- Pseudo predictions over the whole dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fRm62Ji_xPoS" + }, + "source": [ + "#### What is keypoint matching?\n", + "\n", + "Because SuperAnimal models have their pre-defined keypoints that are potentially different from your annotations, we proposed this algorithm to minimize the gap between the model and the dataset. We use our model to perform zero-shot inference on the whole dataset. This gives pairs of predictions and ground truth for every image. Then, we cast the matching between models’ predictions (2D coordinates)\n", + "and ground truth as bipartitematching using the Euclidean distance as the cost between paired of keypoints. We then solve the matching using the Hungarian algorithm. Thus for every image, we end up getting a matching matrix where 1 counts formatch and 0 counts for non-matching. Because the models’ predictions can be noisy from image to image, we average the aforementioned matching matrix across all the images and perform another bipartite matching, resulting in the final keypoint conversion table between the model and the dataset. Note that the quality of thematching will impact the performance\n", + "of the model, especially for zero-shot. In the case where, e.g., the annotation nose is mistakenly converted to keypoint tail and vice versa, the model will have to unlearn the channel that corresponds to nose and tail (see also case study in Mathis et al.)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "vEHeuKSKyjA6", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "5863a81e-e0b9-48c7-f2f9-de14d38e805e" + }, + "outputs": [], + "source": [ + "keypoint_matching(\n", + " config_path,\n", + " superanimal_name,\n", + " model_name,\n", + " detector_name,\n", + " copy_images=True,\n", + ")\n", + "\n", + "conversion_table_path = project_path / \"memory_replay\" / \"conversion_table.csv\"\n", + "confusion_matrix_path = project_path / \"memory_replay\" / \"confusion_matrix.png\"\n", + "\n", + "# You can visualize the pseudo predictions, or do pose embedding clustering etc.\n", + "pseudo_prediction_path = project_path / \"memory_replay\" / \"pseudo_predictions.json\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sA8yyLgs0zoO" + }, + "source": [ + "#### Display the confusion matrix\n", + "\n", + "The x axis lists the keypoints in the existing annotations. The y axis lists the keypoints in SuperAnimal keypoint space. Darker color encodes stronger correspondence between the human annotation and SuperAnimal annotations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "luDxpD9H0zYZ", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "confusion_matrix_image = Image.open(confusion_matrix_path)\n", + "\n", + "plt.imshow(confusion_matrix_image)\n", + "plt.axis(\"off\") # Hide the axes for better view\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i0QWikYmy_Mj" + }, + "source": [ + "#### Display the conversion table\n", + "The gt columns represents the keypoint names in the existing dataset. The MasterName represents the corresponding keypoints in SuperAnimal keypoint space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CeA-NzDMynYV", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "df = pd.read_csv(conversion_table_path)\n", + "df = df.dropna()\n", + "\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Adding the Conversion Table to your project's `config.yaml` file\n", + "\n", + "Once you've run keypoint matching, you can add the conversion table to your project's `config.yaml` file, and edit it if there are some matches you think are wrong. As an example, for a top-view mouse dataset with 4 bodyparts labeled (`'snout', 'leftear', 'rightear', 'tailbase'`), the conversion table mapping project bodyparts to SuperAnimal bodyparts would be added as:\n", + "\n", + "```yaml\n", + "# Conversion tables to fine-tune SuperAnimal weights\n", + "SuperAnimalConversionTables:\n", + " superanimal_topviewmouse:\n", + " snout: nose\n", + " leftear: left_ear\n", + " rightear: right_ear\n", + " tailbase: tail_base\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_conversion_table(\n", + " config=config_path,\n", + " super_animal=superanimal_name,\n", + " project_to_super_animal=read_conversion_table_from_csv(conversion_table_path),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GkfIo8zTxPoS" + }, + "source": [ + "#### Prepare the training shuffle and weight initialization for (naive) fine-tuning with SuperAnimal weights\n", + "\n", + "Then, when you call `build_weight_init` with `with_decoder=True`, the conversion table in your project's `config.yaml` is used to get predictions for the correct bodyparts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xEeM_hrOu6k8", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=True,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_naive_finetune_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gZx6nr-ExPoS" + }, + "source": [ + "#### Launch the training for (naive) fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c3XAr6uRyXOD", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_naive_finetune_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oXuRshzhxPoS" + }, + "source": [ + "#### Evaluate the model obtained by (naive) fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VXfdKS-H2yqw", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(\n", + " config_path,\n", + " Shuffles=[superanimal_naive_finetune_shuffle],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_nUAMlbZ0Z4b" + }, + "source": [ + "### Memory-replay fine-tuning with SuperAnimal (keeping full SuperAnimal keypoints)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n6HPu6RaxPoS" + }, + "source": [ + "**Catastrophic forgetting** describes a\n", + "classic problemin continual learning. Indeed, amodel gradually loses\n", + "its ability to solve previous tasks after it learns to solve new ones.\n", + "Fine-tuning a SuperAnimal models falls into the category of continual\n", + "learning: the downstream dataset defines potentially different\n", + "keypoints than those learned by the models. Thus, the models might\n", + "forget the keypoints they learned and only pick up those defined in the\n", + "target dataset. Here, retraining with the original dataset and the new\n", + "one, is not a feasible option as datasets cannot be easily shared and\n", + "more computational resources would be required.\n", + "To counter that, we treat zero-shot inference of the model as a\n", + "memory buffer that stores knowledge from the original model. When\n", + "we fine-tune a SuperAnimal model, we replace the model predicted\n", + "keypoints with the ground-truth annotations, resulting in hybrid\n", + "learning of old and new knowledge. The quality of the zero-shot predictions\n", + "can vary and we use the confidence of prediction (0.7) as a\n", + "threshold to filter out low-confidence predictions. With the threshold\n", + "set to 1, memory replay fine-tuning becomes naive-fine-tuning." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CSLmjlCIxPoS" + }, + "source": [ + "#### Prepare training shuffle and weight initialization for memory-replay finetuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BKEF76AI0Z4c", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=True,\n", + " memory_replay=True,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_memory_replay_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MKwJiIyKxPoT" + }, + "source": [ + "#### Launch the training for memory-replay fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ru8tIFmD2Mkv", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_memory_replay_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i-2MBRDjxPoT" + }, + "source": [ + "#### Evaluate the model obtained by memory-replay finetuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sfMcK3gq8WxZ", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[superanimal_memory_replay_shuffle])" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [ + "UeXjmtu40Z4X", + "FvFzntDMxPoL", + "6VEjHu-00Z4Y" + ], + "gpuType": "T4", + "provenance": [] + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-10", + "last_metadata_updated": "2026-03-06" + }, + "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.13" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "04370d8302c04c5ca6a351383126193f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0f0ed94a863f49b9b85d0a18fa8ce2a5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e1675e53ca9a4da8acf6c16fba7a2578", + "placeholder": "​", + "style": "IPY_MODEL_3d2996e10f96404baf24d2c4215b75a1", + "value": "model.safetensors: 100%" + } + }, + "1779b84e748b4989a8ed53434c30016f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "1abff22a7c9a416d9166e6b150612171": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_04370d8302c04c5ca6a351383126193f", + "max": 159594859, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_d67c4871543e405fbb576a55f8c9048a", + "value": 159594859 + } + }, + "2a3abfe7867641db9fbfe3ee76854bf4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2cef5e028d2e40a6bba7400be922d0c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2ec06260b237411cabd3de7c37e03b1b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "343f2670d37c4bf18859238c3d81d419": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b988f87e676840ee98daa3d996c9ddbc", + "max": 165432914, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_1779b84e748b4989a8ed53434c30016f", + "value": 165432914 + } + }, + "3c011813d7cb48588a8d236785d9c24f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3d2996e10f96404baf24d2c4215b75a1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "3ea385fe815f4e50a0b81ec299040314": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "42874cdab4be4dc38b0c33775b27d98c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5dcbd8f3fb6148cca6cfc72b20ce49bd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7271412c1f0141649a7300dbce2b003c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a6cb25fa67ef4733a720960b3fc8213c", + "placeholder": "​", + "style": "IPY_MODEL_b73b1b64620d492dbc4eaf4bd83ca23a", + "value": " 160M/160M [00:00<00:00, 201MB/s]" + } + }, + "7cdcbbf9cb694dbf949e8b7eea8e7836": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7ed11ae2a4be462da84ff716e0725af0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_0f0ed94a863f49b9b85d0a18fa8ce2a5", + "IPY_MODEL_343f2670d37c4bf18859238c3d81d419", + "IPY_MODEL_d104ae21091e4f10a7de18e191b9f04d" + ], + "layout": "IPY_MODEL_5dcbd8f3fb6148cca6cfc72b20ce49bd" + } + }, + "9a996c8dc3b34bc5b8805b3687e22b27": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_d012b421c189412dabeac84cba4164a7", + "IPY_MODEL_1abff22a7c9a416d9166e6b150612171", + "IPY_MODEL_7271412c1f0141649a7300dbce2b003c" + ], + "layout": "IPY_MODEL_3c011813d7cb48588a8d236785d9c24f" + } + }, + "9f8009429aa34b40a65c998230f20c99": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a6cb25fa67ef4733a720960b3fc8213c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b73b1b64620d492dbc4eaf4bd83ca23a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b988f87e676840ee98daa3d996c9ddbc": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bee292213d8645618536fcdf6a491d83": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7cdcbbf9cb694dbf949e8b7eea8e7836", + "max": 517816013, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_2ec06260b237411cabd3de7c37e03b1b", + "value": 517816013 + } + }, + "c8b57833d3f946abae69b84075345a54": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_42874cdab4be4dc38b0c33775b27d98c", + "placeholder": "​", + "style": "IPY_MODEL_e3a185abf8a04edabf32d58bdee10dd1", + "value": "detector.pt: 100%" + } + }, + "d012b421c189412dabeac84cba4164a7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_3ea385fe815f4e50a0b81ec299040314", + "placeholder": "​", + "style": "IPY_MODEL_fe59f6c5ed7b4e2cb87bb60224acdaba", + "value": "pose_model.pth: 100%" + } + }, + "d104ae21091e4f10a7de18e191b9f04d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d37cf6fe7c444bc2a2568c3407389ea8", + "placeholder": "​", + "style": "IPY_MODEL_2cef5e028d2e40a6bba7400be922d0c2", + "value": " 165M/165M [00:04<00:00, 41.1MB/s]" + } + }, + "d37cf6fe7c444bc2a2568c3407389ea8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d67c4871543e405fbb576a55f8c9048a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "dccbe277cc084ed6aa0b329067b5c69c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c8b57833d3f946abae69b84075345a54", + "IPY_MODEL_bee292213d8645618536fcdf6a491d83", + "IPY_MODEL_fbbc8c5b20c7423fb21b74296e0eeb28" + ], + "layout": "IPY_MODEL_ff0c737c49624b1ea27588611951fc84" + } + }, + "e1675e53ca9a4da8acf6c16fba7a2578": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3a185abf8a04edabf32d58bdee10dd1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fbbc8c5b20c7423fb21b74296e0eeb28": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_9f8009429aa34b40a65c998230f20c99", + "placeholder": "​", + "style": "IPY_MODEL_2a3abfe7867641db9fbfe3ee76854bf4", + "value": " 518M/518M [00:05<00:00, 101MB/s]" + } + }, + "fe59f6c5ed7b4e2cb87bb60224acdaba": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ff0c737c49624b1ea27588611951fc84": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb index 4c4af5e240..00049863f8 100644 --- a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb @@ -17,8 +17,13 @@ "id": "RK255E7YoEIt" }, "source": [ - "# DeepLabCut Toolbox - Colab for standard (single animal) projects!\n", - "https://github.com/DeepLabCut/DeepLabCut\n", + "# DeepLabCut for your standard (single animal) projects!\n", + "\n", + "Some useful links:\n", + "\n", + "- [DeepLabCut's GitHub: github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", + "\n", "\n", "This notebook illustrates how to use the cloud to:\n", "- create a training set\n", @@ -48,22 +53,19 @@ "id": "txoddlM8hLKm" }, "source": [ - "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" + "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n", + "\n", + "As the COLAB environments were updated to CUDA 12.X and Python 3.11, we need to install DeepLabCut and TensorFlow in a distinct way to get TensorFlow to connect to the GPU." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "q23BzhA6CXxu" - }, + "metadata": {}, "outputs": [], "source": [ - "#(this will take a few minutes to install all the dependences!)\n", - "!apt update && apt install cuda-11-8\n", - "!pip install \"deeplabcut[tf]\"" + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" ] }, { @@ -73,7 +75,30 @@ "id": "25wSj6TlVclR" }, "source": [ - "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !)**" + "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !)** You will see this button at the output of the cells above ^." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oTwAcbq2-FZz", + "outputId": "9cfd8dcf-a0a8-4801-ed1d-fbcd5ec056af" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" + ] + } + ], + "source": [ + "import deeplabcut" ] }, { @@ -98,11 +123,12 @@ }, "outputs": [], "source": [ - "#Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", - "#(We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", + "# Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", + "# (We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", "\n", "from google.colab import drive\n", - "drive.mount('/content/drive')" + "\n", + "drive.mount(\"/content/drive\")" ] }, { @@ -114,7 +140,7 @@ "source": [ "YOU WILL NEED TO EDIT THE PROJECT PATH **in the config.yaml file** TO BE SET TO YOUR GOOGLE DRIVE LINK!\n", "\n", - "Typically, this will be: /content/drive/My Drive/yourProjectFolderName\n" + "Typically, this will be: `/content/drive/My Drive/yourProjectFolderName`\n" ] }, { @@ -127,57 +153,25 @@ }, "outputs": [], "source": [ - "#Setup your project variables:\n", - "# PLEASE EDIT THESE:\n", - " \n", - "ProjectFolderName = 'myproject-teamDLC-2020-03-29'\n", - "VideoType = 'mp4' \n", + "# PLEASE EDIT THIS:\n", + "project_folder_name = \"MontBlanc-Daniel-2019-12-16\"\n", + "video_type = \"mp4\" # , mp4, MOV, or avi, whatever you uploaded!\n", "\n", - "#don't edit these:\n", - "videofile_path = ['/content/drive/My Drive/'+ProjectFolderName+'/videos/'] #Enter the list of videos or folder to analyze.\n", - "videofile_path" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "3K9Ndy1beyfG" - }, - "outputs": [], - "source": [ - "import deeplabcut" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "o4orkg9QTHKK" - }, - "outputs": [], - "source": [ - "deeplabcut.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Z7ZlDr3wV4D1" - }, - "outputs": [], - "source": [ - "#This creates a path variable that links to your google drive copy\n", - "#No need to edit this, as you set it up before: \n", - "path_config_file = '/content/drive/My Drive/'+ProjectFolderName+'/config.yaml'\n", - "path_config_file" + "# No need to edit this, we are going to assume you put videos you want to analyze\n", + "# in the \"videos\" folder, but if this is NOT true, edit below:\n", + "videofile_path = [f\"/content/drive/My Drive/{project_folder_name}/videos/\"]\n", + "print(videofile_path)\n", + "\n", + "# The prediction files and labeled videos will be saved in this `labeled-videos` folder\n", + "# in your project folder; if you want them elsewhere, you can edit this;\n", + "# if you want the output files in the same folder as the videos, set this to an empty string.\n", + "destfolder = f\"/content/drive/My Drive/{project_folder_name}/labeled-videos\"\n", + "\n", + "# No need to edit this, as you set it when you passed the ProjectFolderName (above):\n", + "path_config_file = f\"/content/drive/My Drive/{project_folder_name}/config.yaml\"\n", + "print(path_config_file)\n", + "\n", + "# This creates a path variable that links to your Google Drive project" ] }, { @@ -188,10 +182,12 @@ }, "source": [ "## Create a training dataset:\n", - "### You must do this step inside of Colab:\n", + "\n", + "### You must do this step inside of Colab\n", + "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**.\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pytorch_config.yaml**.\n", "\n", "Now it is the time to start training the network!" ] @@ -207,10 +203,10 @@ }, "outputs": [], "source": [ - "# Note: if you are using the demo data (i.e. examples/Reaching-Mackenzie-2018-08-30/), first delete the folder called dlc-models! \n", - "#Then, run this cell. There are many more functions you can set here, including which netowkr to use!\n", - "#check the docstring for full options you can do!\n", - "deeplabcut.create_training_dataset(path_config_file, net_type='resnet_50', augmenter_type='imgaug')" + "# There are many more functions you can set here, including which network to use!\n", + "# Check the docstring for `create_training_dataset` for all options you can use!\n", + "\n", + "deeplabcut.create_training_dataset(path_config_file, net_type=\"resnet_50\", engine=deeplabcut.Engine.PYTORCH)" ] }, { @@ -234,14 +230,26 @@ }, "outputs": [], "source": [ - "#let's also change the display and save_iters just in case Colab takes away the GPU... \n", - "#if that happens, you can reload from a saved point. Typically, you want to train to 200,000 + iterations.\n", - "#more info and there are more things you can set: https://github.com/DeepLabCut/DeepLabCut/wiki/DOCSTRINGS#train_network\n", + "# Let's also change the display and save_epochs just in case Colab takes away\n", + "# the GPU... If that happens, you can reload from a saved point using the\n", + "# `snapshot_path` argument to `deeplabcut.train_network`:\n", + "# deeplabcut.train_network(..., snapshot_path=\"/content/.../snapshot-050.pt\")\n", "\n", - "deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10,saveiters=500)\n", + "# Typically, you want to train to ~200 epochs. We set the batch size to 8 to\n", + "# utilize the GPU's capabilities.\n", "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 1.03M iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry...." + "# More info and there are more things you can set:\n", + "# https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html#g-train-the-network\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=5,\n", + " epochs=200,\n", + " batch_size=8,\n", + ")\n", + "\n", + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end." ] }, { @@ -251,7 +259,7 @@ "id": "RiDwIVf5-3H_" }, "source": [ - "**When you hit \"STOP\" you will get a KeyInterrupt \"error\"! No worries! :)**" + "Note, that **when you hit \"STOP\" you will get a `KeyboardInterrupt` \"error\"! No worries! :)**" ] }, { @@ -263,7 +271,7 @@ "source": [ "## Start evaluating:\n", "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { @@ -276,11 +284,10 @@ }, "outputs": [], "source": [ - "%matplotlib notebook\n", - "deeplabcut.evaluate_network(path_config_file,plotting=True)\n", + "deeplabcut.evaluate_network(path_config_file, plotting=True)\n", "\n", - "# Here you want to see a low pixel error! Of course, it can only be as good as the labeler, \n", - "#so be sure your labels are good! (And you have trained enough ;)" + "# Here you want to see a low pixel error! Of course, it can only be as\n", + "# good as the labeler, so be sure your labels are good!" ] }, { @@ -319,7 +326,12 @@ }, "outputs": [], "source": [ - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype=VideoType)" + "deeplabcut.analyze_videos(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -343,7 +355,12 @@ }, "outputs": [], "source": [ - "deeplabcut.plot_trajectories(path_config_file,videofile_path, videotype=VideoType)" + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -364,7 +381,7 @@ }, "source": [ "## Create labeled video:\n", - "This function is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " ] }, { @@ -377,7 +394,12 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path, videotype=VideoType)" + "deeplabcut.create_labeled_video(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" ] } ], @@ -390,6 +412,11 @@ "provenance": [], "toc_visible": true }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3.8.12 ('dlc')", "language": "python", diff --git a/examples/COLAB/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb similarity index 52% rename from examples/COLAB/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb rename to examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb index 16a373586c..08633839fb 100644 --- a/examples/COLAB/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb @@ -7,7 +7,7 @@ "id": "view-in-github" }, "source": [ - "\"Open" + "\"Open" ] }, { @@ -16,10 +16,15 @@ "id": "RK255E7YoEIt" }, "source": [ - "# DeepLabCut 2.2+ Toolbox - COLAB\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628180434489-T0RIWEJJU0FJVOT6FNVD/maDLC.png?format=800w)\n", + "# DeepLabCut for your multi-animal projects!\n", + "\n", + "Some useful links:\n", + "\n", + "- [DeepLabCut's GitHub: github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Multi-Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html)\n", "\n", - "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628180434489-T0RIWEJJU0FJVOT6FNVD/maDLC.png?format=800w)\n", "\n", "This notebook illustrates how to, for multi-animal projects, use the cloud-based GPU to:\n", "- create a multi-animal training set\n", @@ -46,46 +51,26 @@ "id": "txoddlM8hLKm" }, "source": [ - "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "q23BzhA6CXxu" - }, - "outputs": [], - "source": [ - "#(this will take a few minutes to install all the dependences!)\n", - "!apt update && apt install cuda-11-8\n", - "!pip install \"deeplabcut[tf]\"\n", - "%reload_ext numpy\n", - "%reload_ext scipy\n", - "%reload_ext matplotlib\n", - "%reload_ext mpl_toolkits" + "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n", + "\n", + "As the COLAB environments were updated to CUDA 12.X and Python 3.11, we need to install DeepLabCut and TensorFlow in a distinct way to get TensorFlow to connect to the GPU." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "id": "-MVvZ13_FMvP" - }, + "metadata": {}, "outputs": [], "source": [ - "#a few colab specific things needed:\n", - "!pip install --upgrade scikit-image\n", - "!pip3 install pickle5" + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" ] }, { "cell_type": "markdown", - "metadata": { - "id": "bqUEb8TBdpWb" - }, + "metadata": {}, "source": [ - "After the package is installed, please click \"restart runtime\" if it appears for DLC changes to take effect in your COLAB environment. You will see this button at the output of the cells above ^." + "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !)** You will see this button at the output of the cells above ^." ] }, { @@ -98,18 +83,9 @@ "id": "oTwAcbq2-FZz", "outputId": "9cfd8dcf-a0a8-4801-ed1d-fbcd5ec056af" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" - ] - } - ], + "outputs": [], "source": [ - "import deeplabcut\n", - "import pickle5 as pickle" + "import deeplabcut" ] }, { @@ -120,10 +96,8 @@ "source": [ "## Link your Google Drive (with your labeled data):\n", "\n", - "- This code assumes you locally installed DeepLabCut, created a project, extracted and labeled frames. Be sure to \"check Labels\" to confirm you are happy with your data. As, these frames are the only thing that is used to train your network. 💪 You can find all the docs to do this here: https://deeplabcut.github.io/DeepLabCut\n", - "\n", + "- This code assumes you locally installed DeepLabCut, created a project, extracted and labeled frames. Be sure to \"check Labels\" to confirm you are happy with your data. As, these frames are the only thing that is used to train your network. 💪 You can find all the docs to do this here: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", "- Next, place your DLC project folder into you Google Drive- i.e., copy the folder named \"Project-YourName-TheDate\" into Google Drive.\n", - "\n", "- Then, click run on the cell below to link this notebook to your Google Drive:" ] }, @@ -135,11 +109,12 @@ }, "outputs": [], "source": [ - "#Now, let's link to your Google Drive. Run this cell and follow the authorization instructions:\n", - "#(We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", + "# Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", + "# (We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", "\n", "from google.colab import drive\n", - "drive.mount('/content/drive')" + "\n", + "drive.mount(\"/content/drive\")" ] }, { @@ -148,7 +123,9 @@ "id": "Frnj1RVDyEqs" }, "source": [ - "## Next, edit the few items below, and click run:\n" + "## Next, edit the few items below, and click run:\n", + "\n", + "YOU WILL NEED TO EDIT THE PROJECT PATH **in the `config.yaml` file** TO BE SET TO YOUR GOOGLE DRIVE LINK! Typically, this will be: `/content/drive/My Drive/yourProjectFolderName`\n" ] }, { @@ -160,18 +137,24 @@ "outputs": [], "source": [ "# PLEASE EDIT THIS:\n", - "ProjectFolderName = 'MontBlanc-Daniel-2019-12-16'\n", - "VideoType = 'mp4' #, mp4, MOV, or avi, whatever you uploaded!\n", + "project_folder_name = \"MontBlanc-Daniel-2019-12-16\"\n", + "video_type = \"mp4\" #, mp4, MOV, or avi, whatever you uploaded!\n", "\n", + "# No need to edit this, we are going to assume you put videos you want to analyze\n", + "# in the \"videos\" folder, but if this is NOT true, edit below:\n", + "videofile_path = [f\"/content/drive/My Drive/{project_folder_name}/videos/\"]\n", + "print(videofile_path)\n", "\n", - "# No need to edit this, we are going to assume you put videos you want to analyze in the \"videos\" folder, but if this is NOT true, edit below:\n", - "videofile_path = ['/content/drive/My Drive/'+ProjectFolderName+'/videos/'] #Enter the list of videos or folder to analyze.\n", - "videofile_path\n", + "# The prediction files and labeled videos will be saved in this `labeled-videos` folder\n", + "# in your project folder; if you want them elsewhere, you can edit this;\n", + "# if you want the output files in the same folder as the videos, set this to an empty string.\n", + "destfolder = f\"/content/drive/My Drive/{project_folder_name}/labeled-videos\"\n", "\n", - "#No need to edit this, as you set it when you passed the ProjectFolderName (above): \n", - "path_config_file = '/content/drive/My Drive/'+ProjectFolderName+'/config.yaml'\n", - "path_config_file\n", - "#This creates a path variable that links to your google drive project" + "#No need to edit this, as you set it when you passed the ProjectFolderName (above):\n", + "path_config_file = f\"/content/drive/My Drive/{project_folder_name}/config.yaml\"\n", + "print(path_config_file)\n", + "\n", + "# This creates a path variable that links to your Google Drive project" ] }, { @@ -182,8 +165,7 @@ "source": [ "## Create a multi-animal training dataset:\n", "\n", - "- more info: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#create-training-dataset\n", - "\n", + "- more info can be [found in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#create-training-dataset)\n", "- please check the text below, edit if needed, and then click run (this can take some time):" ] }, @@ -195,7 +177,7 @@ }, "outputs": [], "source": [ - "#OPTIONAL LEARNING: did you know you can check what each function does by running with a ?\n", + "# OPTIONAL LEARNING: did you know you can check what each function does by running with a ?\n", "deeplabcut.create_multianimaltraining_dataset?" ] }, @@ -209,11 +191,15 @@ "outputs": [], "source": [ "# ATTENTION:\n", - "#which shuffle do you want to create and train?\n", - "shuffle = 1 #edit if needed; 1 is the default.\n", + "# Which shuffle do you want to create and train?\n", + "shuffle = 1 # Edit if needed; 1 is the default.\n", "\n", - "#if you labeled on Windows, please set the windows2linux=True:\n", - "deeplabcut.create_multianimaltraining_dataset(path_config_file, Shuffles=[shuffle], net_type=\"dlcrnet_ms5\",windows2linux=False)" + "deeplabcut.create_multianimaltraining_dataset(\n", + " path_config_file,\n", + " Shuffles=[shuffle],\n", + " net_type=\"dlcrnet_ms5\",\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + ")" ] }, { @@ -223,8 +209,7 @@ }, "source": [ "## Start training:\n", - "This function trains the network for a specific shuffle of the training dataset. \n", - " - more info: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#train-the-network" + "This function trains the network for a specific shuffle of the training dataset. More info can be found [in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#train-the-network)." ] }, { @@ -235,14 +220,26 @@ }, "outputs": [], "source": [ - "#let's also change the display and save_iters just in case Colab takes away the GPU... \n", - "#Typically, you want to train to 50,000 - 200K iterations.\n", - "#more info and there are more things you can set: https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/functionDetails.md#g-train-the-network\n", + "# Let's also change the display and save_epochs just in case Colab takes away\n", + "# the GPU... If that happens, you can reload from a saved point using the\n", + "# `snapshot_path` argument to `deeplabcut.train_network`:\n", + "# deeplabcut.train_network(..., snapshot_path=\"/content/.../snapshot-050.pt\")\n", + "\n", + "# Typically, you want to train to ~200 epochs. We set the batch size to 8 to\n", + "# utilize the GPU's capabilities.\n", + "\n", + "# More info and there are more things you can set:\n", + "# https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html#g-train-the-network\n", "\n", - "deeplabcut.train_network(path_config_file, shuffle=shuffle, displayiters=100,saveiters=1000, maxiters=75000, allow_growth=True)\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=shuffle,\n", + " save_epochs=5,\n", + " epochs=200,\n", + " batch_size=8,\n", + ")\n", "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 50K iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry...." + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end." ] }, { @@ -251,7 +248,7 @@ "id": "RiDwIVf5-3H_" }, "source": [ - "**When you hit \"STOP\" you will get a KeyInterrupt \"error\"! No worries! :)**" + "Note, that **when you hit \"STOP\" you will get a `KeyboardInterrupt` \"error\"! No worries! :)**" ] }, { @@ -262,13 +259,10 @@ "source": [ "## Start evaluating: \n", "\n", - " - First, we evaluate the pose estimation performance.\n", - "\n", - "- This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images) and stores the results as .5 and .csv file in a subdirectory under **evaluation-results**\n", - "\n", + "- First, we evaluate the pose estimation performance.\n", + "- This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images) and stores the results as .5 and .csv file in a subdirectory under **evaluation-results-pytorch**\n", "- If the scoremaps do not look accurate, don't proceed to tracklet assembly; please consider (1) adding more data, (2) adding more bodyparts!\n", - "\n", - "- more info: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#evaluate-the-trained-network\n", + "- More info can be [found in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#evaluate-the-trained-network)\n", "\n", "Here is an example of what you'd aim to see before proceeding:\n", "\n", @@ -284,10 +278,11 @@ }, "outputs": [], "source": [ - "#let's evaluate first:\n", - "deeplabcut.evaluate_network(path_config_file,Shuffles=[shuffle], plotting=True)\n", - "#plot a few scoremaps:\n", - "deeplabcut.extract_save_all_maps(path_config_file, shuffle=shuffle, Indices=[0])" + "# Let's evaluate first:\n", + "deeplabcut.evaluate_network(path_config_file, Shuffles=[shuffle], plotting=True)\n", + "\n", + "# plot a few scoremaps:\n", + "deeplabcut.extract_save_all_maps(path_config_file, shuffle=shuffle, Indices=[0, 1, 2, 3])" ] }, { @@ -323,7 +318,14 @@ "#EDIT OPTION: which video(s) do you want to analyze? You can pass a path or a folder:\n", "# currently, if you run \"as is\" it assumes you have a video in the DLC project video folder!\n", "\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, shuffle=shuffle, videotype=VideoType)" + "deeplabcut.analyze_videos(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " videotype=video_type,\n", + " auto_track=False,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -332,7 +334,7 @@ "id": "91xBLOcBzGxo" }, "source": [ - "Optional: Now you have the option to check the raw dections before animals are assembled. To do so, pass a video path:" + "Optional: Now you have the option to check the raw detections before animals are tracked. To do so, pass a video path:" ] }, { @@ -347,11 +349,13 @@ "## look at the output video; if the pose estimation (i.e. key points)\n", "## don't look good, don't proceed with tracking - add more data to your training set and re-train!\n", "\n", - "#EDIT: let's check a specific video (PLEASE EDIT VIDEO PATH):\n", - "Specific_videofile = '/content/drive/MyDrive/DeepLabCut_maDLC_DemoData/MontBlanc-Daniel-2019-12-16/videos/short.mov'\n", + "# EDIT: let's check a specific video (PLEASE EDIT VIDEO PATH):\n", + "specific_videofile = \"/content/drive/MyDrive/DeepLabCut_maDLC_DemoData/MontBlanc-Daniel-2019-12-16/videos/short.mov\"\n", "\n", - "#don't edit:\n", - "deeplabcut.create_video_with_all_detections(path_config_file, [Specific_videofile], shuffle=shuffle)" + "# Don't edit:\n", + "deeplabcut.create_video_with_all_detections(\n", + " path_config_file, [specific_videofile], shuffle=shuffle, destfolder=destfolder,\n", + ")" ] }, { @@ -360,7 +364,7 @@ "id": "3-OgTJ0Lz20e" }, "source": [ - "If the resutling video (ends in full.mp4) is not good, we highly recommend adding more data and training again. See here: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#decision-break-point" + "If the resulting video (ends in full.mp4) is not good, we highly recommend adding more data and training again. See [here, in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#decision-break-point)." ] }, { @@ -369,14 +373,15 @@ "id": "PxRLS2_-r55K" }, "source": [ - "# Next, we will assemble animals using our data-driven optimal graph method:\n", + "## Next, we will assemble animals using our data-driven optimal graph method:\n", "\n", - "- Here, we will find the optimal graph, which matches the \"data-driven\" method from our paper (Figure adapted from Lauer et al. 2021):\n", + "During video analysis, animals are assembled using the optimal graph, which matches the \"data-driven\" method from our paper (Figure adapted from Lauer et al. 2021)\n", "\n", "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1626266017809-XO6NX84QB4FBAZGOTCEY/fig3.jpg?format=400w)\n", "\n", + "The optimal graph is computed when `evaluate_network` - so make sure you don't skip that step!\n", "\n", - "- note, you can set the number of animals you expect to see, so check, edit, then click run:" + "**Note**: you can set the number of animals you expect to see, so check, edit, then click run:" ] }, { @@ -388,23 +393,37 @@ "outputs": [], "source": [ "#Check and edit:\n", - "numAnimals = 4 #how many animals do you expect to find?\n", - "tracktype= 'box' #box, skeleton, ellipse:\n", - "#-- ellipse is recommended, unless you have a single-point ma project, then use BOX!\n", - "\n", - "#Optional: \n", - "#imagine you tracked a point that is not useful for assembly, \n", - "#like a tail tip that is far from the body, consider dropping it for this step (it's still used later)!\n", - "#To drop it, uncomment the next line TWO lines and add your parts(s):\n", - "\n", - "#bodypart= 'Tail_end'\n", - "#deeplabcut.convert_detections2tracklets(path_config_file, videofile_path, videotype=VideoType, shuffle=shuffle, overwrite=True, ignore_bodyparts=[bodypart])\n", - "\n", - "#OR don't drop, just click RUN:\n", - "deeplabcut.convert_detections2tracklets(path_config_file, videofile_path, videotype=VideoType, \n", - " shuffle=shuffle, overwrite=True)\n", - "\n", - "deeplabcut.stitch_tracklets(path_config_file, videofile_path, shuffle=shuffle, track_method=tracktype, n_tracks=numAnimals)" + "num_animals = 4 # How many animals do you expect to find?\n", + "track_type= \"box\" # box, skeleton, ellipse\n", + "#-- ellipse is recommended, unless you have a single-point MA project, then use BOX!\n", + "\n", + "# Optional:\n", + "# imagine you tracked a point that is not useful for assembly,\n", + "# like a tail tip that is far from the body, consider dropping it for this step (it's still used later)!\n", + "# To drop it, uncomment the next line TWO lines and add your parts(s):\n", + "\n", + "# bodypart= 'Tail_end'\n", + "# deeplabcut.convert_detections2tracklets(path_config_file, videofile_path, videotype=VideoType, shuffle=shuffle, overwrite=True, ignore_bodyparts=[bodypart])\n", + "\n", + "# OR don't drop, just click RUN:\n", + "deeplabcut.convert_detections2tracklets(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + " overwrite=True,\n", + ")\n", + "\n", + "deeplabcut.stitch_tracklets(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " n_tracks=num_animals,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -424,11 +443,14 @@ }, "outputs": [], "source": [ - "deeplabcut.filterpredictions(path_config_file, \n", - " videofile_path, \n", - " shuffle=shuffle,\n", - " videotype=VideoType, \n", - " track_method = tracktype)" + "deeplabcut.filterpredictions(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " videotype=video_type,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -448,7 +470,14 @@ }, "outputs": [], "source": [ - "deeplabcut.plot_trajectories(path_config_file, videofile_path, videotype=VideoType, shuffle=shuffle, track_method=tracktype)" + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" ] }, { @@ -467,7 +496,7 @@ }, "source": [ "## Create labeled video:\n", - "This function is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " ] }, { @@ -478,13 +507,17 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,\n", - " videofile_path, \n", - " shuffle=shuffle, \n", - " color_by=\"individual\",\n", - " videotype=VideoType, \n", - " save_frames=False,\n", - " filtered=True)" + "deeplabcut.create_labeled_video(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " color_by=\"individual\",\n", + " videotype=video_type,\n", + " save_frames=False,\n", + " filtered=True,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" ] } ], @@ -496,6 +529,11 @@ "name": "COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-10", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3", "name": "python3" diff --git a/examples/COLAB/COLAB_transformer_reID.ipynb b/examples/COLAB/COLAB_transformer_reID.ipynb index 1e15a01683..12eb4f89ea 100644 --- a/examples/COLAB/COLAB_transformer_reID.ipynb +++ b/examples/COLAB/COLAB_transformer_reID.ipynb @@ -7,7 +7,7 @@ "id": "view-in-github" }, "source": [ - "\"Open" + "\"Open" ] }, { @@ -16,12 +16,12 @@ "id": "TGChzLdc-lUJ" }, "source": [ - "# DeepLabCut 2.2 Toolbox Demo on how to use our Pose Transformer for unsupervised identity tracking of animals\n", + "# Demo: How to use our Pose Transformer for unsupervised identity tracking of animals\n", "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628250004229-KVYD7JJVHYEFDJ32L9VJ/DLClogo2021.jpg?format=1000w)\n", "\n", "https://github.com/DeepLabCut/DeepLabCut\n", "\n", - "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo 3 mouse project:\n", + "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo tri-mouse project:\n", "- load our mini-demo data that includes a pretrained model and unlabeled video.\n", "- analyze a novel video.\n", "- use the transformer to do unsupervised ID tracking.\n", @@ -29,31 +29,56 @@ "\n", "### To create a full maDLC pipeline please see our full docs: https://deeplabcut.github.io/DeepLabCut/README.html\n", "- Of interest is a full how-to for maDLC: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html\n", - "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/tutorial.html\n", - "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", + "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/quick-start/tutorial_maDLC.html\n", + "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", "\n", "### To get started, please go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" ] }, + { + "cell_type": "markdown", + "metadata": { + "id": "xOe2hvy85EVP" + }, + "source": [ + "‼️ **Attention: this demo is for maDLC, which is version 2.2**\n" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": { - "id": "HoNN2_0Z9rr_" + "id": "NXmLeZBX45Oe" }, "outputs": [], "source": [ - "# Install the latest DeepLabCut version:\n", - "!apt update && apt install cuda-11-8\n", + "# Install DLC version 2.2-2.3 (pre DLC3):\n", "!pip install \"deeplabcut[tf]\"" ] }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "TlhrVFKN8euh" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import deeplabcut" + ] + }, { "cell_type": "markdown", "metadata": { "id": "Wid0GTGMAEnZ" }, "source": [ + "## Important - Restart the Runtime for the updated packages to be imported!\n", + "\n", + "PLEASE, click \"restart runtime\" from the output above before proceeding!\n", + "\n", "No information needs edited in the cells below, you can simply click run on each:\n", "\n", "### Download our Demo Project from our server:" @@ -61,28 +86,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { - "id": "PusLdqbqJi60" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PusLdqbqJi60", + "outputId": "dbe30821-d3a7-443f-de74-6cb0bee49aac" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading demo-me-2021-07-14.zip...\n" + ] + } + ], "source": [ "# Download our demo project:\n", - "import requests\n", "from io import BytesIO\n", "from zipfile import ZipFile\n", "\n", - "url_record = 'https://zenodo.org/api/records/7883589'\n", + "import requests\n", + "\n", + "url_record = \"https://zenodo.org/api/records/7883589\"\n", "response = requests.get(url_record)\n", "if response.status_code == 200:\n", - " file = response.json()['files'][0]\n", - " title = file['key']\n", + " file = response.json()[\"files\"][0]\n", + " title = file[\"key\"]\n", " print(f\"Downloading {title}...\")\n", - " with requests.get(file['links']['self'], stream=True) as r:\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", " with ZipFile(BytesIO(r.content)) as zf:\n", - " zf.extractall(path='/content')\n", + " zf.extractall(path=\"/content\")\n", "else:\n", - " raise ValueError(f'The URL {url_record} could not be reached.')" + " raise ValueError(f\"The URL {url_record} could not be reached.\")" ] }, { @@ -91,27 +129,144 @@ "id": "8iXtySnQB0BE" }, "source": [ - "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data \n", + "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data\n", "\n", - "###in one step, since auto_track=True you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" + "In one step, since `auto_track=True` you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "odYrU3o8BSAr" }, "outputs": [], "source": [ - "import deeplabcut as dlc\n", - "import os\n", - "\n", "project_path = \"/content/demo-me-2021-07-14\"\n", "config_path = os.path.join(project_path, \"config.yaml\")\n", - "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")\n", - "\n", - "dlc.analyze_videos(config_path,[video], shuffle=0, videotype=\"mp4\",auto_track=True)" + "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 520 + }, + "id": "U_351Hkv81X-", + "outputId": "f7c30461-101f-47b6-c04f-15809aa5a4bb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture from the video(s) with batchsize: 8\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [00:39<00:00, 58.83it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Video Analyzed. Saving results in /content/demo-me-2021-07-14/videos...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/auxfun_multianimal.py:83: UserWarning: default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Analyzing /content/demo-me-2021-07-14/videos/videocompressed1DLC_dlcrnetms5_demoJul14shuffle0_20000.h5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [00:02<00:00, 1088.72it/s]\n", + "2330it [00:06, 342.29it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'.\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 4/4 [00:00<00:00, 1488.53it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The videos are analyzed. Time to assemble animals and track 'em... \n", + " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking.\n", + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n" + ] + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + }, + "text/plain": [ + "'DLC_dlcrnetms5_demoJul14shuffle0_20000'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "deeplabcut.analyze_videos(config_path, [video], shuffle=0, videotype=\"mp4\", auto_track=True)" ] }, { @@ -134,23 +289,69 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { - "id": "aTRbuUQ1FBO0" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "aTRbuUQ1FBO0", + "outputId": "0d182f64-512d-463d-a997-226c7199b724" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filtering with median model /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Saving filtered csv poses!\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/post_processing/filtering.py:298: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " data.to_hdf(outdataname, \"df_with_missing\", format=\"table\", mode=\"w\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|██████████| 2330/2330 [00:31<00:00, 73.04it/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "[True]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "#Filter the predictions to remove small jitter, if desired:\n", - "dlc.filterpredictions(config_path,\n", - " [video],\n", - " shuffle=0,\n", - " videotype='mp4',\n", - " )\n", - "\n", - "dlc.create_labeled_video(\n", + "# Filter the predictions to remove small jitter, if desired:\n", + "deeplabcut.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\")\n", + "deeplabcut.create_labeled_video(\n", " config_path,\n", " [video],\n", - " videotype='mp4',\n", + " videotype=\"mp4\",\n", " shuffle=0,\n", " color_by=\"individual\",\n", " keypoints_only=False,\n", @@ -182,13 +383,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { - "id": "7w9BDIA7BB_i" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "7w9BDIA7BB_i", + "outputId": "a163087d-cbcb-4e4d-f461-2e24ed19a80b" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] + } + ], "source": [ - "dlc.plot_trajectories(config_path, [video], shuffle=0,videotype='mp4')" + "deeplabcut.plot_trajectories(config_path, [video], shuffle=0, videotype=\"mp4\")" ] }, { @@ -199,21 +413,108 @@ "source": [ "# Transformer for reID\n", "\n", - "while the tracking here is very good without using the transformer, we want to demo the workflow for you! " + "while the tracking here is very good without using the transformer, we want to demo the workflow for you!" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { - "id": "5xlO6TVYxQWc" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5xlO6TVYxQWc", + "outputId": "a433221f-0390-4028-fe68-be0b90adad48" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [01:18<00:00, 29.78it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n", + "Epoch 10, train acc: 0.61\n", + "Epoch 10, test acc 0.45\n", + "Epoch 20, train acc: 0.74\n", + "Epoch 20, test acc 0.65\n", + "Epoch 30, train acc: 0.78\n", + "Epoch 30, test acc 0.55\n", + "Epoch 40, train acc: 0.76\n", + "Epoch 40, test acc 0.50\n", + "Epoch 50, train acc: 0.85\n", + "Epoch 50, test acc 0.55\n", + "Epoch 60, train acc: 0.84\n", + "Epoch 60, test acc 0.60\n", + "Epoch 70, train acc: 0.85\n", + "Epoch 70, test acc 0.55\n", + "Epoch 80, train acc: 0.79\n", + "Epoch 80, test acc 0.55\n", + "Epoch 90, train acc: 0.88\n", + "Epoch 90, test acc 0.55\n", + "Epoch 100, train acc: 0.84\n", + "Epoch 100, test acc 0.55\n", + "loading params\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 4/4 [00:00<00:00, 483.21it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] + } + ], "source": [ - "dlc.transformer_reID(config_path, [video],\n", - " shuffle=0, videotype='mp4',\n", - " track_method='ellipse',n_triplets=100\n", - " )" + "deeplabcut.transformer_reID(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"ellipse\",\n", + " n_triplets=100,\n", + ")" ] }, { @@ -227,35 +528,86 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { - "id": "MBMbRFEMxmi4" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "MBMbRFEMxmi4", + "outputId": "5ca4357a-c8e1-46c6-ecad-141bfce48cc5" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] + } + ], "source": [ - "dlc.plot_trajectories(config_path, [video],\n", - " shuffle=0,videotype='mp4',\n", - " track_method=\"transformer\"\n", - " )" + "deeplabcut.plot_trajectories(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"transformer\",\n", + ")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { - "id": "vx3e-r1CoXaX" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vx3e-r1CoXaX", + "outputId": "46cdbd39-d1f6-4b78-abba-7e979740f2a2" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|██████████| 2330/2330 [00:31<00:00, 73.75it/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "[True]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "dlc.create_labeled_video(\n", + "deeplabcut.create_labeled_video(\n", " config_path,\n", " [video],\n", - " videotype='mp4',\n", + " videotype=\"mp4\",\n", " shuffle=0,\n", " color_by=\"individual\",\n", " keypoints_only=False,\n", " draw_skeleton=True,\n", - " track_method=\"transformer\"\n", + " track_method=\"transformer\",\n", ")" ] } @@ -263,8 +615,9 @@ "metadata": { "accelerator": "GPU", "colab": { - "collapsed_sections": [], + "gpuType": "A100", "include_colab_link": true, + "machine_shape": "hm", "name": "COLAB_transformer_reID.ipynb", "provenance": [] }, diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 92d7eb9ff9..578a69fe01 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -50,9 +50,9 @@ "metadata": {}, "outputs": [], "source": [ - "#Setup your project variables:\n", - "YourName = 'teamDLC'\n", - "YourExperimentName = 'testing'" + "# Setup your project variables:\n", + "YourName = \"teamDLC\"\n", + "YourExperimentName = \"testing\"" ] }, { @@ -75,7 +75,7 @@ } ], "source": [ - "config_path = deeplabcut.create_new_project_3d(YourExperimentName,YourName,num_cameras=2)" + "config_path = deeplabcut.create_new_project_3d(YourExperimentName, YourName, num_cameras=2)" ] }, { @@ -93,11 +93,11 @@ "metadata": {}, "outputs": [], "source": [ - "#If you're loading an already created project, just set the 3D Project config_path variable:\n", - "#import os\n", - "#from pathlib import Path\n", - "#config_path3d = os.path.join(os.getcwd(),'testing3D-DeepLabCutTeam-2019-06-05-3d/config.yaml')\n", - "#print(config_path3d)" + "# If you're loading an already created project, just set the 3D Project config_path variable:\n", + "# import os\n", + "# from pathlib import Path\n", + "# config_path3d = os.path.join(os.getcwd(),'testing3D-DeepLabCutTeam-2019-06-05-3d/config.yaml')\n", + "# print(config_path3d)" ] }, { @@ -151,7 +151,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow =9,cbcol =6,calibrate=False,alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path, cbrow=9, cbcol=6, calibrate=False, alpha=0.9)" ] }, { @@ -179,7 +179,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow = 9,cbcol = 6, calibrate=True, alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path, cbrow=9, cbcol=6, calibrate=True, alpha=0.9)" ] }, { @@ -197,10 +197,9 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib\n", "%matplotlib inline\n", "\n", - "deeplabcut.check_undistortion(config_path3d)" + "deeplabcut.check_undistortion(config_path)" ] }, { @@ -239,12 +238,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Of course, this does not work on the demo calibration images, \n", + "# Of course, this does not work on the demo calibration images,\n", "# but when you are ready for your own dataset, edit and then run the following!\n", "\n", - "video_path = '/home/yourname/videoFolder'\n", + "video_path = \"/home/yourname/videoFolder\"\n", "\n", - "deeplabcut.triangulate(config_path3d,video_path, videotype='mp4')" + "deeplabcut.triangulate(config_path, video_path, videotype=\"mp4\")" ] }, { @@ -269,15 +268,20 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.create_labeled_video_3d(config_path,['triangulated_file_folder'],start=50,end=250, trailpoints=3)" + "deeplabcut.create_labeled_video_3d(config_path, [\"triangulated_file_folder\"], start=50, end=250, trailpoints=3)" ] } ], "metadata": { + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DEEPLABCUT_newGUI] *", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DEEPLABCUT_newGUI-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -289,7 +293,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb index cc45d9a2a7..549ce4260a 100644 --- a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb @@ -8,7 +8,11 @@ }, "source": [ "# DeepLabCut Toolbox - DEMO (mouse reaching)\n", - "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", "\n", "#### The notebook accompanies the following user-guide:\n", "\n", @@ -35,7 +39,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Import the toolbox:" + "## Import the Toolbox and Required Libraries" ] }, { @@ -48,6 +52,8 @@ }, "outputs": [], "source": [ + "from pathlib import Path\n", + "\n", "import deeplabcut" ] }, @@ -64,12 +70,17 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "# Note that parameters of this project can be seen at: *Reaching-Mackenzie-2018-08-30/config.yaml*\n", - "from pathlib import Path\n", + "# Create a variable to set the config.yaml file path:\n", + "# If this path does not point to the project from the URL below,\n", + "# edit it to make sure it does:\n", + "# https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/Reaching-Mackenzie-2018-08-30\n", + "#\n", + "# Example - Linux/OSX\n", + "# path_config_file = \"/Users/john/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"\n", + "# Example - Windows\n", + "# path_config_file = r\"C:\\DeepLabCut\\examples\\Reaching-Mackenzie-2018-08-30\\config.yaml\"\n", "\n", - "#create a variable to set the config.yaml file path:\n", - "path_config_file = os.path.join(os.getcwd(),'Reaching-Mackenzie-2018-08-30/config.yaml')\n", + "path_config_file = str(Path.cwd() / \"Reaching-Mackenzie-2018-08-30\" / \"config.yaml\")\n", "print(path_config_file)" ] }, @@ -99,8 +110,8 @@ }, "outputs": [], "source": [ - "#let's load some demo data, and create a training set \n", - "#(note, this function is not used when you create your own project):\n", + "# Let's load some demo data, and create a training set\n", + "# (note, this function is not used when you create your own project):\n", "\n", "deeplabcut.load_demo_data(path_config_file)" ] @@ -115,7 +126,7 @@ }, "outputs": [], "source": [ - "#Perhaps plot the labels to see how the frames were annotated:\n", + "# Perhaps plot the labels to see how the frames were annotated:\n", "\n", "deeplabcut.check_labels(path_config_file)" ] @@ -128,11 +139,12 @@ }, "source": [ "## Start training of Feature Detectors\n", - "This function trains the network for a specific shuffle of the training dataset. **The user can set various parameters in /Reaching-Mackenzie-2018-08-30/dlc-models/ReachingAug30-trainset95shuffle1/iteration-0/train/pose_cfg.yaml.**\n", "\n", - "Training can be stopped at any time. Note that the weights are only stored every 'save_iters' steps. For this demo the it is advisable to store & display the progress very often (i.e. display every 20, save every 100). In practice this is inefficient (in reality, you will train until ~200K, so we save every 50K).\n", + "This function trains the network for a specific shuffle of the training dataset. **The user can set various parameters in `.../Reaching-Mackenzie-2018-08-30/dlc-models-pytorch/iteration-0/ReachingAug30-trainset95shuffle1/train/pytorch_config.yaml`**. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", "\n", - "**We recommend just training for 10-20 min, as you aren't running this demo to use DLC, just to work through the steps. In total, this demo should take you LESS THAN 1 HOUR!**" + "Training can be stopped at any time. Note that the weights are only stored every 'save_epochs' steps. For this demo the it is advisable to store & display the progress very often (i.e. display every 20, save every 2). In practice this is inefficient (in reality, you will train until ~200, so we save every 10).\n", + "\n", + "**We recommend just training for 15-20 min, as you aren't running this demo to use DLC, just to work through the steps. In total, this demo should take you LESS THAN 1 HOUR!**" ] }, { @@ -142,24 +154,26 @@ "colab": {}, "colab_type": "code", "id": "jg96O2acywnW", - "scrolled": false + "scrolled": true }, "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1, saveiters=300, displayiters=10)\n", - "#notice the variables \"saveiters\" and \"dsiplayiters\" that can be set in the function\n", + "# notice the variables \"save_epochs\" and \"displayiters\" that can be set in the function\n", + "deeplabcut.train_network(path_config_file, shuffle=1, save_epochs=2, displayiters=10)\n", + "\n", + "# you just need to run this until you get at least 1 snapshot, which is set by: \"save_epochs\"\n", + "# (so in this case you could stop after 2 epochs!) How do I stop? Click the STOP button!\n", "\n", - "#you just need to run this until you get at least 1 snapshot, which is set by: \"save_iters\" \n", - "#(so in this case you could stop after 500!) How do I stop? Click the STOP button!\n", - "# To train until ~2,000 iterations on a CPU should be ~30 min" + "# To train until ~50 epochs on a CPU should be ~15 min\n", + "# Every 10 epochs, your model will be evaluated. You can keep an eye on model performance\n", + "# while the model is being trained." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "*Note, that if it reaches the end (default 1M) or you stop it (by \"stop\" or by CTRL+C), \n", - "you will see an keyboard interrupt \"error\", but it is not a real error, i.e. you can ignore this.*" + "*Note, that if you stop it (by \"stop\" or by CTRL+C), you will see an keyboard interrupt \"error\", but it is not a real error, i.e. you can ignore this.*" ] }, { @@ -171,7 +185,7 @@ "source": [ "## Evaluate the trained network\n", "\n", - "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results**.\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**.\n", "\n", "You can change various parameters in the ```config.yaml``` file of this project. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated positions need to be so that they are shown in the plots." ] @@ -187,14 +201,14 @@ }, "outputs": [], "source": [ - "deeplabcut.evaluate_network(path_config_file,plotting=True)" + "deeplabcut.evaluate_network(path_config_file, plotting=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**NOTE: depending on your set up sometimes you get some \"matplotlib errors, but these are not important**\n", + "**NOTE: depending on your setup sometimes you get some \"matplotlib errors, but these are not important**\n", "\n", "Now you can go check out the images. Given the limited data input and it took ~20 mins to test this out, it is not meant to track well, so don't be alarmed. This is just to get you familiar with the workflow... " ] @@ -223,12 +237,12 @@ "outputs": [], "source": [ "# Set the video path:\n", - "#The video can be the one you trained with and new videos that look similar, i.e. same experiments, etc.\n", + "# The video can be the one you trained with and new videos that look similar, i.e. same experiments, etc.\n", "# You can add individual videos, OR just a folder - it will skip videos that are already analyzed once.\n", "\n", - "#i.e you can run 'reachingvideo1' and/or 'MovieS2_Perturbation_noLaser_compressed'\n", + "# i.e. you can run 'reachingvideo1' and/or 'MovieS2_Perturbation_noLaser_compressed'\n", "\n", - "videofile_path = os.path.join(os.getcwd(),'Reaching-Mackenzie-2018-08-30/videos/reachingvideo1.avi') " + "videofile_path = str(Path(path_config_file).parent / \"videos\" / \"reachingvideo1.avi\")" ] }, { @@ -243,8 +257,9 @@ "outputs": [], "source": [ "print(\"Start Analyzing the video!\")\n", - "deeplabcut.analyze_videos(path_config_file,[videofile_path])\n", - "# this video takes ~ 8 min to analyze with a CPU" + "\n", + "deeplabcut.analyze_videos(path_config_file, [videofile_path])\n", + "# this video takes ~ 1 min to analyze with a CPU" ] }, { @@ -279,7 +294,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,[videofile_path], draw_skeleton=True)" + "deeplabcut.create_labeled_video(path_config_file, [videofile_path], draw_skeleton=True)" ] }, { @@ -305,9 +320,9 @@ "outputs": [], "source": [ "%matplotlib notebook\n", - "deeplabcut.plot_trajectories(path_config_file,[videofile_path],showfigures=True)\n", + "deeplabcut.plot_trajectories(path_config_file, [videofile_path], showfigures=True)\n", "\n", - "#These plots can are interactive and can be customized (see https://matplotlib.org/)" + "# These plots are interactive and can be customized (see https://matplotlib.org/)" ] }, { @@ -339,11 +354,16 @@ "colab": {}, "colab_type": "code", "id": "RJGiDKuUywoC", - "scrolled": false + "scrolled": true }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,videofile_path,outlieralgorithm='uncertain',p_bound=.2)" + "deeplabcut.extract_outlier_frames(\n", + " path_config_file,\n", + " videofile_path,\n", + " outlieralgorithm=\"uncertain\",\n", + " p_bound=0.2,\n", + ")" ] }, { @@ -365,7 +385,9 @@ "source": [ "## Manually correct labels\n", "\n", - "This step allows the user to correct the labels in the extracted frames. Navigate to the folder with the videos and use the GUI as described in the protocol to update the labels." + "This step allows the user to correct the labels in the extracted frames. Navigate to the folder with the videos and use the GUI as described in the protocol to update the labels.\n", + "\n", + "For documentation regarding the GUI, [look at the docs for `napari-deeplabcut`](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) - and specifically _\"3. Refining labels – the image folder contains a machinelabels-iter<#>.h5 file.\"_!" ] }, { @@ -379,9 +401,6 @@ }, "outputs": [], "source": [ - "#GUI pops up! \n", - "#sometimes you need to restart the kernel for the GUI to launch.\n", - "%gui wx\n", "deeplabcut.refine_labels(path_config_file)" ] }, @@ -421,7 +440,7 @@ }, "outputs": [], "source": [ - "#Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", + "# Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", "deeplabcut.check_labels(path_config_file)\n", "# if they are off, you can load them in the labeling_gui to adjust!" ] @@ -436,7 +455,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_training_dataset(path_config_file)" + "deeplabcut.create_training_dataset(path_config_file, engine=deeplabcut.Engine.PYTORCH)" ] }, { @@ -446,7 +465,7 @@ "id": "8fhL6nG2ywoW" }, "source": [ - "Now one can train the network again... (with the expanded data set)" + "Now one can train the network again... (with the expanded data set). We can continue training from the snapshot we already have by using the `snapshot_path` argument - instead of training the model from scratch, it will load the weights we already have and fine-tune them!" ] }, { @@ -459,8 +478,31 @@ }, "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file)" + "snapshot_path = ( # Edit me if needed! Select the path to the snapshot to continue training from!\n", + " Path(path_config_file).parent\n", + " / \"dlc-models-pytorch\"\n", + " / \"iteration-0\"\n", + " / \"ReachingAug30-trainset95shuffle1\"\n", + " / \"train\"\n", + " / \"snapshot-best-080.pt\"\n", + ")\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=10,\n", + " batch_size=8,\n", + " snapshot_path=snapshot_path,\n", + ")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -470,10 +512,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -485,7 +532,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb index 3b75f5b60a..0d26f986fe 100644 --- a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb @@ -8,7 +8,11 @@ }, "source": [ "# DeepLabCut Toolbox - Open-Field DEMO\n", - "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", "\n", "#### The notebook accompanies the following user-guide:\n", "\n", @@ -34,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -43,65 +47,50 @@ "outputs": [], "source": [ "# Importing the toolbox (takes several seconds)\n", + "from pathlib import Path\n", + "\n", "import deeplabcut" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", "id": "WOEHc0MeywnJ" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loaded, now creating training data...\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30 already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/short_mp3/CollectedData_Pranav.h5 not found (perhaps not annotated)\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1 already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1//train already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1//test already exists!\n", - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!\n" - ] - } - ], + "outputs": [], "source": [ - "# Loading example data set:\n", - "import os\n", + "# Create a variable to set the config.yaml file path:\n", + "# If this path does not point to the project from the URL below,\n", + "# edit it to make sure it does:\n", + "# https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/openfield-Pranav-2018-10-30\n", + "#\n", + "# Example - Linux/OSX\n", + "# path_config_file = \"/Users/john/DeepLabCut/examples/openfield-Pranav-2018-10-30/config.yaml\"\n", + "# Example - Windows\n", + "# path_config_file = r\"C:\\DeepLabCut\\examples\\openfield-Pranav-2018-10-30\\config.yaml\"\n", + "#\n", "# Note that parameters of this project can be seen at: *openfield-Pranav-2018-10-30/config.yaml*\n", - "from pathlib import Path\n", - "path_config_file = os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/config.yaml')\n", + "\n", + "path_config_file = str(Path.cwd() / \"openfield-Pranav-2018-10-30\" / \"config.yaml\")\n", "deeplabcut.load_demo_data(path_config_file)" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", "id": "ROlflqQLywnP" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating images with labels by Pranav.\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/m4s1_labeled already exists!\n", - "They are stored in the following folder: /home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/m4s1_labeled.\n", - "Attention: /home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/short_mp3 does not appear to have labeled data!\n", - "If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!\n" - ] - } - ], + "outputs": [], "source": [ - "#[OPTIONAL] Perhaps plot the labels to see how the frames were annotated:\n", - "#(note, this project was created in Linux, so you might have an error in Windows, but this is an optional step)\n", + "# [OPTIONAL] Perhaps plot the labels to see how the frames were annotated:\n", + "# (note, this project was created in Linux, so you might have an error in Windows, but this is an optional step)\n", + "\n", "deeplabcut.check_labels(path_config_file)" ] }, @@ -113,141 +102,30 @@ }, "source": [ "## Start training of Feature Detectors\n", - "This function trains the network for a specific shuffle of the training dataset. The user can set various parameters in */openfield-Pranav-2018-10-30/dlc-models/.../pose_cfg.yaml*. \n", "\n", - "Training can be stopped at any time. Note that the weights are only stored every 'save_iters' steps. For this demo the state it is advisable to store & display the progress very often. In practice this is inefficient. " + "This function trains the network for a specific shuffle of the training dataset. The user can set various parameters in `/openfield-Pranav-2018-10-30/dlc-models-pytorch/.../pytorch_config.yaml`. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", + "\n", + "Training can be stopped at any time. Note that the weights are only stored every 'save_epochs' epochs. For this demo the state it is advisable to store & display the progress very often. In practice this is inefficient. You should see the model start converging around 50 to 60 epochs; you can continue training it longer to improve performance." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", "id": "jg96O2acywnW", - "scrolled": false + "scrolled": true }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Config:\n", - "{'all_joints': [[0], [1], [2], [3]],\n", - " 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'],\n", - " 'batch_size': 1,\n", - " 'bottomheight': 400,\n", - " 'crop': True,\n", - " 'crop_pad': 0,\n", - " 'cropratio': 0.4,\n", - " 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat',\n", - " 'dataset_type': 'default',\n", - " 'display_iters': 1000,\n", - " 'fg_fraction': 0.25,\n", - " 'global_scale': 0.8,\n", - " 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt',\n", - " 'intermediate_supervision': False,\n", - " 'intermediate_supervision_layer': 12,\n", - " 'leftwidth': 400,\n", - " 'location_refinement': True,\n", - " 'locref_huber_loss': True,\n", - " 'locref_loss_weight': 0.05,\n", - " 'locref_stdev': 7.2801,\n", - " 'log_dir': 'log',\n", - " 'max_input_size': 1500,\n", - " 'mean_pixel': [123.68, 116.779, 103.939],\n", - " 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle',\n", - " 'min_input_size': 64,\n", - " 'minsize': 100,\n", - " 'mirror': False,\n", - " 'multi_step': [[0.005, 10000],\n", - " [0.02, 430000],\n", - " [0.002, 730000],\n", - " [0.001, 1030000]],\n", - " 'net_type': 'resnet_50',\n", - " 'num_joints': 4,\n", - " 'optimizer': 'sgd',\n", - " 'pos_dist_thresh': 17,\n", - " 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30',\n", - " 'regularize': False,\n", - " 'rightwidth': 400,\n", - " 'save_iters': 50000,\n", - " 'scale_jitter_lo': 0.5,\n", - " 'scale_jitter_up': 1.25,\n", - " 'scoremap_dir': 'test',\n", - " 'shuffle': True,\n", - " 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/snapshot',\n", - " 'stride': 8.0,\n", - " 'topheight': 400,\n", - " 'use_gt_segm': False,\n", - " 'video': False,\n", - " 'video_batch': False,\n", - " 'weigh_negatives': False,\n", - " 'weigh_only_present_joints': False,\n", - " 'weigh_part_predictions': False,\n", - " 'weight_decay': 0.0001}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO:tensorflow:Restoring parameters from /home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt\n", - "Display_iters overwritten as 10\n", - "Save_iters overwritten as 100\n", - "Training parameter:\n", - "{'stride': 8.0, 'weigh_part_predictions': False, 'weigh_negatives': False, 'fg_fraction': 0.25, 'weigh_only_present_joints': False, 'mean_pixel': [123.68, 116.779, 103.939], 'shuffle': True, 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/snapshot', 'log_dir': 'log', 'global_scale': 0.8, 'location_refinement': True, 'locref_stdev': 7.2801, 'locref_loss_weight': 0.05, 'locref_huber_loss': True, 'optimizer': 'sgd', 'intermediate_supervision': False, 'intermediate_supervision_layer': 12, 'regularize': False, 'weight_decay': 0.0001, 'mirror': False, 'crop_pad': 0, 'scoremap_dir': 'test', 'dataset_type': 'default', 'use_gt_segm': False, 'batch_size': 1, 'video': False, 'video_batch': False, 'crop': True, 'cropratio': 0.4, 'minsize': 100, 'leftwidth': 400, 'rightwidth': 400, 'topheight': 400, 'bottomheight': 400, 'all_joints': [[0], [1], [2], [3]], 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'], 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat', 'display_iters': 1000, 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt', 'max_input_size': 1500, 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle', 'min_input_size': 64, 'multi_step': [[0.005, 10000], [0.02, 430000], [0.002, 730000], [0.001, 1030000]], 'net_type': 'resnet_50', 'num_joints': 4, 'pos_dist_thresh': 17, 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30', 'save_iters': 50000, 'scale_jitter_lo': 0.5, 'scale_jitter_up': 1.25}\n", - "Starting training....\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "iteration: 10 loss: 0.3308 lr: 0.005\n", - "iteration: 20 loss: 0.0563 lr: 0.005\n", - "iteration: 30 loss: 0.0417 lr: 0.005\n", - "iteration: 40 loss: 0.0362 lr: 0.005\n", - "iteration: 50 loss: 0.0407 lr: 0.005\n", - "iteration: 60 loss: 0.0461 lr: 0.005\n", - "iteration: 70 loss: 0.0385 lr: 0.005\n", - "iteration: 80 loss: 0.0345 lr: 0.005\n", - "iteration: 90 loss: 0.0314 lr: 0.005\n", - "iteration: 100 loss: 0.0428 lr: 0.005\n", - "iteration: 110 loss: 0.0262 lr: 0.005\n", - "iteration: 120 loss: 0.0255 lr: 0.005\n", - "iteration: 130 loss: 0.0275 lr: 0.005\n", - "iteration: 140 loss: 0.0251 lr: 0.005\n", - "iteration: 150 loss: 0.0221 lr: 0.005\n", - "iteration: 160 loss: 0.0209 lr: 0.005\n", - "iteration: 170 loss: 0.0297 lr: 0.005\n", - "iteration: 180 loss: 0.0325 lr: 0.005\n", - "iteration: 190 loss: 0.0242 lr: 0.005\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mdeeplabcut\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_network\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath_config_file\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshuffle\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdisplayiters\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msaveiters\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m100\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/training.py\u001b[0m in \u001b[0;36mtrain_network\u001b[0;34m(config, shuffle, trainingsetindex, gputouse, max_snapshots_to_keep, autotune, displayiters, saveiters, maxiters)\u001b[0m\n\u001b[1;32m 87\u001b[0m \u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mposeconfigfile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mdisplayiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0msaveiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmaxiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmax_to_keep\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_snapshots_to_keep\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m#pass on path and file name for pose_cfg.yaml!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 88\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 89\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 90\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 91\u001b[0m \u001b[0mos\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mchdir\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstart_path\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/training.py\u001b[0m in \u001b[0;36mtrain_network\u001b[0;34m(config, shuffle, trainingsetindex, gputouse, max_snapshots_to_keep, autotune, displayiters, saveiters, maxiters)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 86\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 87\u001b[0;31m \u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mposeconfigfile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mdisplayiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0msaveiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmaxiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmax_to_keep\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_snapshots_to_keep\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m#pass on path and file name for pose_cfg.yaml!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 88\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 89\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/train.py\u001b[0m in \u001b[0;36mtrain\u001b[0;34m(config_yaml, displayiters, saveiters, maxiters, max_to_keep)\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[0mcurrent_lr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlr_gen\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_lr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 141\u001b[0m [_, loss_val, summary] = sess.run([train_op, total_loss, merged_summaries],\n\u001b[0;32m--> 142\u001b[0;31m feed_dict={learning_rate: current_lr})\n\u001b[0m\u001b[1;32m 143\u001b[0m \u001b[0mcum_loss\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mloss_val\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 144\u001b[0m \u001b[0mtrain_writer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_summary\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msummary\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self, fetches, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 898\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 899\u001b[0m result = self._run(None, fetches, feed_dict, options_ptr,\n\u001b[0;32m--> 900\u001b[0;31m run_metadata_ptr)\n\u001b[0m\u001b[1;32m 901\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrun_metadata\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 902\u001b[0m \u001b[0mproto_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtf_session\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTF_GetBuffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrun_metadata_ptr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_run\u001b[0;34m(self, handle, fetches, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 1133\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfinal_fetches\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mfinal_targets\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mhandle\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mfeed_dict_tensor\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1134\u001b[0m results = self._do_run(handle, final_targets, final_fetches,\n\u001b[0;32m-> 1135\u001b[0;31m feed_dict_tensor, options, run_metadata)\n\u001b[0m\u001b[1;32m 1136\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1137\u001b[0m \u001b[0mresults\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_do_run\u001b[0;34m(self, handle, target_list, fetch_list, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 1314\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mhandle\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1315\u001b[0m return self._do_call(_run_fn, feeds, fetches, targets, options,\n\u001b[0;32m-> 1316\u001b[0;31m run_metadata)\n\u001b[0m\u001b[1;32m 1317\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1318\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_do_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_prun_fn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhandle\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeeds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetches\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_do_call\u001b[0;34m(self, fn, *args)\u001b[0m\n\u001b[1;32m 1320\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_do_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1321\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1322\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1323\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0merrors\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOpError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1324\u001b[0m \u001b[0mmessage\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcompat\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mas_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmessage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_run_fn\u001b[0;34m(feed_dict, fetch_list, target_list, options, run_metadata)\u001b[0m\n\u001b[1;32m 1305\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_extend_graph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1306\u001b[0m return self._call_tf_sessionrun(\n\u001b[0;32m-> 1307\u001b[0;31m options, feed_dict, fetch_list, target_list, run_metadata)\n\u001b[0m\u001b[1;32m 1308\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1309\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_prun_fn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhandle\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeed_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetch_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_call_tf_sessionrun\u001b[0;34m(self, options, feed_dict, fetch_list, target_list, run_metadata)\u001b[0m\n\u001b[1;32m 1407\u001b[0m return tf_session.TF_SessionRun_wrapper(\n\u001b[1;32m 1408\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_session\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moptions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeed_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetch_list\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtarget_list\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1409\u001b[0;31m run_metadata)\n\u001b[0m\u001b[1;32m 1410\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1411\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0merrors\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_exception_on_not_ok_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mstatus\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], + "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10, saveiters=100)" + "# notice the variables \"save_epochs\" and \"displayiters\" that can be set in the function\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=5,\n", + ")" ] }, { @@ -267,98 +145,23 @@ "source": [ "## Evaluate a trained network\n", "\n", - "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results**.\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**.\n", "\n", "You can change various parameters in the ```config.yaml``` file of this project. For evaluation all the model descriptors (Task, TrainingFraction, Date etc.) are important. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated positions need to be so that they are shown in the plots. One can furthermore, change the colormap and dotsize for those graphs." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", "id": "kuprPKDdywne", "scrolled": false }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Config:\n", - "{'all_joints': [[0], [1], [2], [3]],\n", - " 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'],\n", - " 'batch_size': 1,\n", - " 'bottomheight': 400,\n", - " 'crop': True,\n", - " 'crop_pad': 0,\n", - " 'cropratio': 0.4,\n", - " 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat',\n", - " 'dataset_type': 'default',\n", - " 'display_iters': 1000,\n", - " 'fg_fraction': 0.25,\n", - " 'global_scale': 0.8,\n", - " 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt',\n", - " 'intermediate_supervision': False,\n", - " 'intermediate_supervision_layer': 12,\n", - " 'leftwidth': 400,\n", - " 'location_refinement': True,\n", - " 'locref_huber_loss': True,\n", - " 'locref_loss_weight': 0.05,\n", - " 'locref_stdev': 7.2801,\n", - " 'log_dir': 'log',\n", - " 'max_input_size': 1500,\n", - " 'mean_pixel': [123.68, 116.779, 103.939],\n", - " 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle',\n", - " 'min_input_size': 64,\n", - " 'minsize': 100,\n", - " 'mirror': False,\n", - " 'multi_step': [[0.005, 10000],\n", - " [0.02, 430000],\n", - " [0.002, 730000],\n", - " [0.001, 1030000]],\n", - " 'net_type': 'resnet_50',\n", - " 'num_joints': 4,\n", - " 'optimizer': 'sgd',\n", - " 'pos_dist_thresh': 17,\n", - " 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30',\n", - " 'regularize': False,\n", - " 'rightwidth': 400,\n", - " 'save_iters': 50000,\n", - " 'scale_jitter_lo': 0.5,\n", - " 'scale_jitter_up': 1.25,\n", - " 'scoremap_dir': 'test',\n", - " 'shuffle': True,\n", - " 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/test/snapshot',\n", - " 'stride': 8.0,\n", - " 'topheight': 400,\n", - " 'use_gt_segm': False,\n", - " 'video': False,\n", - " 'video_batch': False,\n", - " 'weigh_negatives': False,\n", - " 'weigh_only_present_joints': False,\n", - " 'weigh_part_predictions': False,\n", - " 'weight_decay': 0.0001}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/evaluation-results/ already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/evaluation-results/iteration-0/openfieldOct30-trainset95shuffle1 already exists!\n", - "Running DeepCut_resnet50_openfieldOct30shuffle1_2400 with # of trainingiterations: 2400\n", - "This net has already been evaluated!\n", - "The network is evaluated and the results are stored in the subdirectory 'evaluation_results'.\n", - "If it generalizes well, choose the best model for prediction and update the config file with the appropriate index for the 'snapshotindex'.\n", - "Use the function 'analyze_video' to make predictions on new videos.\n", - "Otherwise consider retraining the network (see DeepLabCut workflow Fig 2)\n" - ] - } - ], + "outputs": [], "source": [ - "deeplabcut.evaluate_network(path_config_file,plotting=False)" + "deeplabcut.evaluate_network(path_config_file, plotting=False)" ] }, { @@ -393,9 +196,7 @@ }, "outputs": [], "source": [ - "# Creating video path:\n", - "import os\n", - "videofile_path = os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4')" + "videofile_path = str(Path(path_config_file).parent / \"videos\" / \"m3v1mp4.mp4\")" ] }, { @@ -410,8 +211,8 @@ "outputs": [], "source": [ "print(\"Start analyzing the video!\")\n", - "#our demo video on a CPU with take ~30 min to analze! GPU is much faster!\n", - "deeplabcut.analyze_videos(path_config_file,[videofile_path])" + "# our demo video on a CPU with take ~5 min to analze! GPU is much faster!\n", + "deeplabcut.analyze_videos(path_config_file, [videofile_path])" ] }, { @@ -439,7 +240,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,[videofile_path])" + "deeplabcut.create_labeled_video(path_config_file, [videofile_path])" ] }, { @@ -465,9 +266,13 @@ "outputs": [], "source": [ "%matplotlib notebook\n", - "deeplabcut.plot_trajectories(path_config_file,[videofile_path],showfigures=True)\n", + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " [videofile_path],\n", + " showfigures=True,\n", + ")\n", "\n", - "#These plots can are interactive and can be customized (see https://matplotlib.org/)" + "# These plots are interactive and can be customized (see https://matplotlib.org/)" ] }, { @@ -493,7 +298,7 @@ }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,[videofile_path])" + "deeplabcut.extract_outlier_frames(path_config_file, [videofile_path])" ] }, { @@ -515,7 +320,9 @@ "source": [ "## Manually correct labels\n", "\n", - "This step allows the user to correct the labels in the extracted frames. Navigate to the folder corresponding to the video 'm3v1mp4' and use the GUI as described in the protocol to update the labels." + "This step allows the user to correct the labels in the extracted frames. Navigate to the folder corresponding to the video 'm3v1mp4' and use the GUI as described in the protocol to update the labels.\n", + "\n", + "For documentation regarding the GUI, [look at the docs for `napari-deeplabcut`](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) - and specifically _\"3. Refining labels – the image folder contains a machinelabels-iter<#>.h5 file.\"_!" ] }, { @@ -528,7 +335,6 @@ }, "outputs": [], "source": [ - "%gui qt6\n", "deeplabcut.refine_labels(path_config_file)" ] }, @@ -542,7 +348,7 @@ }, "outputs": [], "source": [ - "#Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", + "# Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", "deeplabcut.check_labels(path_config_file)" ] }, @@ -592,7 +398,7 @@ "id": "8fhL6nG2ywoW" }, "source": [ - "Now one can train the network again... (with the expanded data set)" + "Now one can train the network again... (with the expanded data set). We can continue training from the snapshot we already have by using the `snapshot_path` argument - instead of training the model from scratch, it will load the weights we already have and fine-tune them!" ] }, { @@ -605,8 +411,31 @@ }, "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1)" + "snapshot_path = ( # Edit me if needed! Select the path to the snapshot to continue training from!\n", + " Path(path_config_file).parent\n", + " / \"dlc-models-pytorch\"\n", + " / \"iteration-0\"\n", + " / \"openfieldOct30-trainset95shuffle1\"\n", + " / \"train\"\n", + " / \"snapshot-best-080.pt\"\n", + ")\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=10,\n", + " batch_size=8,\n", + " snapshot_path=snapshot_path,\n", + ")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -616,10 +445,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -631,7 +465,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Demo_napari.ipynb b/examples/JUPYTER/Demo_napari.ipynb index b9ff9512b9..764390965d 100644 --- a/examples/JUPYTER/Demo_napari.ipynb +++ b/examples/JUPYTER/Demo_napari.ipynb @@ -54,22 +54,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", "id": "jqLZhp7EoEI0" }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/mwmathis/opt/anaconda3/envs/DLC2K/lib/python3.8/site-packages/statsmodels/compat/pandas.py:65: FutureWarning: pandas.Int64Index is deprecated and will be removed from pandas in a future version. Use pandas.Index with the appropriate dtype instead.\n", - " from pandas import Int64Index as NumericIndex\n" - ] - } - ], + "outputs": [], "source": [ "import deeplabcut" ] @@ -84,15 +75,20 @@ }, "outputs": [], "source": [ - "task='Reaching' # Enter the name of your experiment Task\n", - "experimenter='Mackenzie' # Enter the name of the experimenter\n", - "video=['/Users/mwmathis/Documents/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/videos/reachingvideo1.avi'] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", + "task = \"Reaching\" # Enter the name of your experiment Task\n", + "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", + "video = [\n", + " \"/Users/mwmathis/Documents/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/videos/reachingvideo1.avi\"\n", + "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", "\n", - "path_config_file=deeplabcut.create_new_project(task,experimenter,video,copy_videos=True) \n", + "path_config_file = deeplabcut.create_new_project(task, experimenter, video, copy_videos=True)\n", "\n", - "# NOTE: The function returns the path, where your project is. \n", - "# You could also enter this manually (e.g. if the project is already created and you want to pick up, where you stopped...)\n", - "#path_config_file = '/home/Mackenzie/Reaching/config.yaml' # Enter the path of the config file that was just created from the above step (check the folder)" + "# NOTE: The function returns the path, where your project is.\n", + "\n", + "# You could also enter this manually (e.g. if the project is already created and you\n", + "# want to pick up, where you stopped...): Enter the path of the config file that was\n", + "# just created from the above step (check the folder)\n", + "# path_config_file = \"/home/Mackenzie/Reaching/config.yaml\"" ] }, { @@ -148,10 +144,10 @@ }, "outputs": [], "source": [ - "#there are other ways to grab frames, such as uniformly; please see the paper:\n", + "# there are other ways to grab frames, such as uniformly; please see the paper:\n", "\n", - "#AUTOMATIC:\n", - "deeplabcut.extract_frames(path_config_file) " + "# AUTOMATIC:\n", + "deeplabcut.extract_frames(path_config_file)" ] }, { @@ -177,8 +173,8 @@ "# Attention: If you have not installed the napari-dlc plugin, do so now by running this cell:\n", "!pip install napari-deeplabcut\n", "\n", - "#if the plugin does not appear upon launch, consider running in the terminal the above command \n", - "#within the same conda env and then re-starting kernel in your notebook (Kernel > restart)." + "# if the plugin does not appear upon launch, consider running in the terminal the above command\n", + "# within the same conda env and then re-starting kernel in your notebook (Kernel > restart)." ] }, { @@ -194,6 +190,7 @@ "# napari will pop up! Please go to plugin > deeplabcut to start:\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -219,7 +216,7 @@ }, "outputs": [], "source": [ - "deeplabcut.check_labels(path_config_file) #this creates a subdirectory with the frames + your labels" + "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" ] }, { @@ -245,7 +242,7 @@ "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**. For most all use cases we have seen, the defaults are perfectly fine.\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pytorch_config.yaml**. For most all use cases we have seen, the defaults are perfectly fine.\n", "\n", "Now it is the time to start training the network!" ] @@ -262,7 +259,7 @@ "outputs": [], "source": [ "deeplabcut.create_training_dataset(path_config_file)\n", - "#remember, there are several networks you can pick, the default is resnet-50!" + "# remember, there are several networks you can pick, the default is resnet-50!" ] }, { @@ -299,7 +296,7 @@ "source": [ "## Start evaluating\n", "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { @@ -338,9 +335,9 @@ }, "outputs": [], "source": [ - "videofile_path = ['videos/video3.avi','videos/video4.avi'] #Enter a folder OR a list of videos to analyze.\n", + "videofile_path = [\"videos/video3.avi\", \"videos/video4.avi\"] # Enter a folder OR a list of videos to analyze.\n", "\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype='.avi')" + "deeplabcut.analyze_videos(path_config_file, videofile_path, videotype=\".avi\")" ] }, { @@ -374,7 +371,7 @@ }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,['/videos/video3.avi']) #pass a specific video" + "deeplabcut.extract_outlier_frames(path_config_file, [\"/videos/video3.avi\"]) # pass a specific video" ] }, { @@ -398,10 +395,11 @@ }, "outputs": [], "source": [ - "#now you can edit the \"machine-labeled file\" within napari; \n", - "#just again drop the file and images into the workspace after you load the plugin\n", + "# now you can edit the \"machine-labeled file\" within napari;\n", + "# just again drop the file and images into the workspace after you load the plugin\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -432,7 +430,7 @@ }, "outputs": [], "source": [ - "#NOW, merge this with your original data:\n", + "# NOW, merge this with your original data:\n", "\n", "deeplabcut.merge_datasets(path_config_file)" ] @@ -469,7 +467,7 @@ }, "source": [ "## Create labeled video\n", - "This function is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", "\n", "THIS HAS MANY FUN OPTIONS! \n", "\n", @@ -497,7 +495,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -522,7 +520,7 @@ "outputs": [], "source": [ "%matplotlib notebook #for making interactive plots.\n", - "deeplabcut.plot_trajectories(path_config_file,videofile_path)" + "deeplabcut.plot_trajectories(path_config_file, videofile_path)" ] } ], @@ -533,10 +531,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2K]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2K-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -548,7 +551,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_yourowndata.ipynb index 6b0478306f..8a62f53afe 100644 --- a/examples/JUPYTER/Demo_yourowndata.ipynb +++ b/examples/JUPYTER/Demo_yourowndata.ipynb @@ -8,7 +8,12 @@ }, "source": [ "# DeepLabCut Toolbox\n", - "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", "\n", "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", "This shows the most simple code to do so, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", @@ -52,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -73,15 +78,25 @@ }, "outputs": [], "source": [ - "task='Reaching' # Enter the name of your experiment Task\n", - "experimenter='Mackenzie' # Enter the name of the experimenter\n", - "video=['videos/video1.avi','videos/video2.avi'] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", + "task = \"Reaching\" # Enter the name of your experiment Task\n", + "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", + "video = [\n", + " \"videos/video1.avi\",\n", + " \"videos/video2.avi\",\n", + "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", "\n", - "path_config_file=deeplabcut.create_new_project(task,experimenter,video,copy_videos=True) \n", + "path_config_file = deeplabcut.create_new_project(\n", + " task,\n", + " experimenter,\n", + " video,\n", + " copy_videos=True,\n", + ")\n", "\n", - "# NOTE: The function returns the path, where your project is. \n", - "# You could also enter this manually (e.g. if the project is already created and you want to pick up, where you stopped...)\n", - "#path_config_file = '/home/Mackenzie/Reaching/config.yaml' # Enter the path of the config file that was just created from the above step (check the folder)" + "# NOTE: The function returns the path, where your project is.\n", + "# You could also enter this manually (e.g. if the project is already created\n", + "# and you want to pick up where you stopped...)\n", + "# Enter the path of the config file that was just created from the above step (check the folder):\n", + "# path_config_file = \"/home/Mackenzie/Reaching/config.yaml\"" ] }, { @@ -101,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -138,10 +153,10 @@ "outputs": [], "source": [ "%matplotlib inline\n", - "#there are other ways to grab frames, such as uniformly; please see the paper:\n", + "# there are other ways to grab frames, such as uniformly; please see the paper:\n", "\n", - "#AUTOMATIC:\n", - "deeplabcut.extract_frames(path_config_file) " + "# AUTOMATIC:\n", + "deeplabcut.extract_frames(path_config_file)" ] }, { @@ -153,7 +168,9 @@ "source": [ "## Label the extracted frames\n", "\n", - "Only videos in the config file can be used to extract the frames. Extracted labels for each video are stored in the project directory under the subdirectory **'labeled-data'**. Each subdirectory is named after the name of the video. The toolbox has a labeling toolbox which could be used for labeling. " + "Only videos in the config file can be used to extract the frames. Extracted labels for each video are stored in the project directory under the subdirectory **'labeled-data'**. Each subdirectory is named after the name of the video. The toolbox has a labeling toolbox which could be used for labeling. \n", + "\n", + "Check out [our `napari-deeplabcut` docs](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) for more information about labelling!" ] }, { @@ -173,6 +190,7 @@ "\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -198,7 +216,7 @@ }, "outputs": [], "source": [ - "deeplabcut.check_labels(path_config_file) #this creates a subdirectory with the frames + your labels" + "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" ] }, { @@ -224,7 +242,7 @@ "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**. For most all use cases we have seen, the defaults are perfectly fine.\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and creates a `pytorch_config.yaml` file, defining the model architecture and containing various parameters used for training the network. For most all use cases we have seen, the defaults are perfectly fine. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", "\n", "Now it is the time to start training the network!" ] @@ -241,7 +259,8 @@ "outputs": [], "source": [ "deeplabcut.create_training_dataset(path_config_file)\n", - "#remember, there are several networks you can pick, the default is resnet-50!" + "\n", + "# remember, there are several networks you can pick, the default is resnet-50!" ] }, { @@ -253,6 +272,8 @@ "source": [ "## Start training:\n", "\n", + "The user can set various parameters in `.../project-name/dlc-models-pytorch/.../pytorch_config.yaml`. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", + "\n", "This function trains the network for a specific shuffle of the training dataset. " ] }, @@ -278,7 +299,7 @@ "source": [ "## Start evaluating\n", "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { @@ -317,9 +338,9 @@ }, "outputs": [], "source": [ - "videofile_path = ['videos/video3.avi','videos/video4.avi'] #Enter a folder OR a list of videos to analyze.\n", + "videofile_path = [\"videos/video3.avi\", \"videos/video4.avi\"] # Enter a folder OR a list of videos to analyze.\n", "\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype='.avi')" + "deeplabcut.analyze_videos(path_config_file, videofile_path, videotype=\".avi\")" ] }, { @@ -336,7 +357,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -353,7 +374,7 @@ }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,['/videos/video3.avi']) #pass a specific video" + "deeplabcut.extract_outlier_frames(path_config_file, [\"/videos/video3.avi\"]) # pass a specific video" ] }, { @@ -408,7 +429,7 @@ }, "outputs": [], "source": [ - "#NOW, merge this with your original data:\n", + "# NOW, merge this with your original data:\n", "\n", "deeplabcut.merge_datasets(path_config_file)" ] @@ -445,18 +466,38 @@ }, "source": [ "## Create labeled video\n", - "This function is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", + "\n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", "\n", "THIS HAS MANY FUN OPTIONS! \n", "\n", - "``deeplabcut.create_labeled_video(config, videos, videotype='avi', shuffle=1, trainingsetindex=0, filtered=False, save_frames=False, Frames2plot=None, delete=False, displayedbodyparts='all', codec='mp4v', outputframerate=None, destfolder=None, draw_skeleton=False, trailpoints=0, displaycropped=False)``\n", + "```python\n", + "deeplabcut.create_labeled_video(\n", + " config,\n", + " videos,\n", + " videotype='avi',\n", + " shuffle=1,\n", + " trainingsetindex=0,\n", + " filtered=False,\n", + " save_frames=False,\n", + " Frames2plot=None,\n", + " delete=False,\n", + " displayedbodyparts='all',\n", + " codec='mp4v',\n", + " outputframerate=None,\n", + " destfolder=None,\n", + " draw_skeleton=False,\n", + " trailpoints=0,\n", + " displaycropped=False,\n", + ")\n", + "```\n", "\n", "So please check:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -473,7 +514,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -498,7 +539,7 @@ "outputs": [], "source": [ "%matplotlib notebook #for making interactive plots.\n", - "deeplabcut.plot_trajectories(path_config_file,videofile_path)" + "deeplabcut.plot_trajectories(path_config_file, videofile_path)" ] } ], @@ -509,10 +550,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -524,7 +570,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb index b562493a97..c9808a0f35 100644 --- a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb @@ -56,25 +56,11 @@ }, "outputs": [], "source": [ - "import tensorflow as tf\n", - "tf.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Pm_PC1Q8lRrH" - }, - "outputs": [], - "source": [ - "#let's make sure we see a GPU:\n", - "#tf.test.gpu_device_name()\n", - "#or\n", - "from tensorflow.python.client import device_lib\n", - "device_lib.list_local_devices()" + "import torch\n", + "\n", + "# Let's make sure we see a GPU:\n", + "print(torch.__version__)\n", + "print(torch.cuda.is_available())" ] }, { @@ -94,10 +80,11 @@ }, "outputs": [], "source": [ - "#GUIs don't work on in Docker (or the cloud), so label your data locally on your computer! \n", - "#This notebook is for you to train and run video analysis!\n", + "# GUIs don't work on in Docker (or the cloud), so label your data locally on your computer!\n", + "# This notebook is for you to train and run video analysis!\n", "import os\n", - "os.environ[\"DLClight\"]=\"True\"" + "\n", + "os.environ[\"DLClight\"] = \"True\"" ] }, { @@ -112,8 +99,7 @@ "outputs": [], "source": [ "# now we are ready to train!\n", - "import deeplabcut\n", - "deeplabcut.__version__" + "import deeplabcut" ] }, { @@ -133,7 +119,8 @@ }, "outputs": [], "source": [ - "path_config_file = '/home/mackenzie/DEEPLABCUT/DeepLabCut2.0/examples/Reaching-Mackenzie-2018-08-30/config.yaml' #change to yours!" + "# change to yours!\n", + "path_config_file = \"/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"" ] }, { @@ -155,11 +142,12 @@ }, "source": [ "## Create a training dataset\n", - "This function generates the training data information for DeepCut (which requires a mat file) based on the pandas dataframes that hold label information. The user can set the fraction of the training set size (from all labeled image in the hd5 file) in the config.yaml file. While creating the dataset, the user can create multiple shuffles. \n", + "\n", + "This function generates the training data required for DeepLabCut. The user can set the fraction of the training set size (from all labeled images in the hd5 file) in the `config.yaml` file. While creating the dataset, the user can create multiple shuffles. \n", "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**." + "This function also creates new subdirectories under **dlc-models-pytorch** and creates a `pytorch_config.yaml` file, defining the model architecture and containing various parameters used for training the network. For most all use cases we have seen, the defaults are perfectly fine. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n" ] }, { @@ -168,16 +156,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.create_training_dataset(path_config_file,Shuffles=[1])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### now go edit the pose_cfg.yaml to make display_iters: low (i.e. 10), and save_iters: 500 (for demo's)\n", - "\n", - "Now it is the time to start training the network!" + "deeplabcut.create_training_dataset(path_config_file, Shuffles=[1])" ] }, { @@ -202,54 +181,18 @@ }, "outputs": [], "source": [ - "#reset in case you started a session before...\n", - "#tf.reset_default_graph()\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=5,\n", + ")\n", "\n", - "deeplabcut.train_network(path_config_file, shuffle=1, saveiters=1000, displayiters=10)\n", + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it\n", + "# hits the end (default, 200 epochs).\n", "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 1.3M iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry....\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Firstly, if the above cell ran, you can stop it with \"stop\" or cntrl-C; you will get a Keyboard Interrupt error (this is fine!)\n", - "\n", - "### A couple tips for possible troubleshooting (1): \n", - "\n", - "if you get **permission errors** when you run this step (above), first check if the weights downloaded. As some docker containers might not have privileges for this (it can be user specific). They should be under 'init_weights' (see path in the pose_cfg.yaml file). You can enter the DOCKER in the terminal:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "see more here: https://github.com/MMathisLab/Docker4DeepLabCut2.0#using-the-docker-for-training-and-video-analysis" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can \"cd\" in the terminal to this location! i.e. copy and paste this in: **\"cd usr/local/lib/python3.6/dist-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/\n", - "\"** \n", - "\n", - "And if you type \"ls\" to see the list of files, you should see the resnet:\n", - "**resnet_v1_50.ckpt**\n", - "\n", - "If it is not there, run **\"sudo download.sh\"**\n", - "then change the permissions: **\"sudo chown yourusername:yourusername resnet_v1_50.ckpt\"**\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Troubleshooting (2): \n", - "if it appears the training does not start (i.e. \"Starting training...\" does not print immediately),\n", - "then you have another session running on your GPU. Go check \"nvidia-smi\" and look at the process names. You can only have 1 per GPU!)" + "# If you end training before it hits the end, you will see what looks like\n", + "# an error message, but it's not an error - don't worry...." ] }, { @@ -261,7 +204,7 @@ "source": [ "## Start evaluating\n", "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { @@ -277,7 +220,8 @@ "source": [ "deeplabcut.evaluate_network(path_config_file)\n", "\n", - "# Here you want to see a low pixel error! Of course, it can only be as good as the labeler, so be sure your labels are good!" + "# Here you want to see a low pixel error! Of course, it can only\n", + "# be as good as the labeler, so be sure your labels are good!" ] }, { @@ -317,8 +261,10 @@ }, "outputs": [], "source": [ - "videofile_path = ['/home/mackenzie/DEEPLABCUT/DeepLabCut2.0/examples/Reaching-Mackenzie-2018-08-30/videos/MovieS2_Perturbation_noLaser_compressed.avi'] #Enter the list of videos to analyze.\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path)" + "videofile_path = [\n", + " \"/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/videos/MovieS2_Perturbation_noLaser_compressed.avi\"\n", + "] # Enter the list of videos to analyze.\n", + "deeplabcut.analyze_videos(path_config_file, videofile_path)" ] }, { @@ -329,7 +275,7 @@ }, "source": [ "## Create labeled video\n", - "This function is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " ] }, { @@ -343,7 +289,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -369,10 +315,9 @@ "outputs": [], "source": [ "%matplotlib notebook \n", - "#for making interactive plots.\n", - "#deeplabcut.plot_trajectories(path_config_file,videofile_path, plotting=True)\n", - "\n", - "deeplabcut.plot_trajectories(path_config_file,videofile_path,showfigures=True)" + "# for making interactive plots.\n", + "# deeplabcut.plot_trajectories(path_config_file, videofile_path, plotting=True)\n", + "deeplabcut.plot_trajectories(path_config_file, videofile_path, showfigures=True)" ] } ], @@ -386,8 +331,13 @@ "toc_visible": true, "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [default]", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -401,7 +351,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/openfield-Pranav-2018-10-30/config.yaml b/examples/openfield-Pranav-2018-10-30/config.yaml index 64c2ce17b2..ed8c31fdf3 100644 --- a/examples/openfield-Pranav-2018-10-30/config.yaml +++ b/examples/openfield-Pranav-2018-10-30/config.yaml @@ -2,10 +2,17 @@ Task: openfield scorer: Pranav date: Oct30 +multianimalproject: +identity: + # Project path (change when moving around) project_path: WILL BE AUTOMATICALLY UPDATED BY DEMO CODE +# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) +engine: pytorch + + # Annotation data set configuration (and individual video cropping parameters) video_sets: WILL BE AUTOMATICALLY UPDATED BY DEMO CODE: @@ -16,23 +23,33 @@ bodyparts: - rightear - tailbase + +# Fraction of video to start/stop when extracting frames for labeling/refinement start: 0 stop: 1 numframes2pick: 20 + # Plotting configuration +skeleton: [] +skeleton_color: black pcutoff: 0.4 dotsize: 8 alphavalue: 0.7 colormap: jet + # Training,Evaluation and Analysis configuration TrainingFraction: - 0.95 iteration: 0 default_net_type: resnet_50 +default_augmenter: imgaug snapshotindex: -1 +detector_snapshotindex: -1 batch_size: 4 +detector_batch_size: 1 + # Cropping Parameters (for analysis and outlier frame detection) cropping: false @@ -42,8 +59,18 @@ x2: 640 y1: 277 y2: 624 + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) corner2move2: - 50 - 50 move2corner: true + + +# Conversion tables to fine-tune SuperAnimal weights +SuperAnimalConversionTables: + superanimal_topviewmouse: + snout: nose + leftear: left_ear + rightear: right_ear + tailbase: tail_base diff --git a/examples/stereo_example.zip b/examples/stereo_example.zip new file mode 100644 index 0000000000..85b591cbe0 Binary files /dev/null and b/examples/stereo_example.zip differ diff --git a/examples/test.sh b/examples/test.sh index 427482f179..b9be0a511d 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -6,14 +6,14 @@ rm -r OUT cd .. pip uninstall deeplabcut python3 setup.py sdist bdist_wheel -pip install dist/deeplabcut-2.3.9-py3-none-any.whl +pip install dist/deeplabcut-3.0.0-none-any.whl cd examples -python3 testscript.py +python3 testscript_tensorflow_single_animal.py python3 testscript_3d.py #does not work in container #python3 testscript_mobilenets.py -python3 testscript_multianimal.py +python3 testscript_tensorflow_multi_animal.py #python3 testscript_openfielddata_netcomparison.py #python3 testscript_openfielddata_augmentationcomparison.py diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index dda11770cd..a31c034a9f 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -20,13 +20,15 @@ This script tests various functionalities in an automatic way. It produces nothing of interest scientifically. """ -import os, deeplabcut -import zipfile, urllib.request, shutil -from datetime import datetime as dt + import glob -from pathlib import Path +import os +import shutil import subprocess +import zipfile +from pathlib import Path +import deeplabcut if __name__ == "__main__": print("Imported DLC!") @@ -34,12 +36,11 @@ scorer = "Alex" # Enter the name of the experimenter/labeler num_cameras = 2 # Enter the number of cameras - basepath = str(Path(os.path.realpath(__file__)).parents[1]) + basepath = str(Path(os.path.realpath(__file__)).parents[0]) videoname = "reachingvideo1" video = [ os.path.join( basepath, - "examples", "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi", @@ -88,7 +89,7 @@ output2, ] ) - except: + except Exception: pass """ @@ -103,8 +104,8 @@ # checking if 2d test project is available try: config = glob.glob(os.path.join(basepath, "TEST*", "config.yaml"))[-1] - except: - raise RuntimeError("Please run the testscript.py first before testing for 3d") + except Exception as e: + raise RuntimeError("Please run the testscript_tensorflow_single_animal.py first before testing for 3d") from e dfolder = None @@ -121,10 +122,8 @@ cfg["skeleton"] = [["bodypart1", "bodypart2"], ["objectA", "bodypart3"]] deeplabcut.auxiliaryfunctions.write_config_3d(path_config_file, cfg) - except: - raise ( - "Please delete the project and re-try." - ) # otherwise the cfg is an empty array! + except Exception as e: + raise RuntimeError("Please delete the project and re-try.") from e # otherwise the cfg is an empty array! """ # Creating the name of the project @@ -138,11 +137,8 @@ project_name = path_config_file.split(os.sep)[-2] os.chdir(os.path.join(project_name, "calibration_images")) - # Downloading the calibration images - url = "http://www.vision.caltech.edu/bouguetj/calib_doc/htmls/stereo_example.zip" - file_name = "stereo_example.zip" - with urllib.request.urlopen(url) as response, open(file_name, "wb") as out_file: - shutil.copyfileobj(response, out_file) + + file_name = os.path.join(basepath, "stereo_example.zip") with zipfile.ZipFile(file_name) as zf: zf.extractall() @@ -150,7 +146,8 @@ cwd = os.getcwd() [os.remove(file) for file in os.listdir(cwd) if not file.endswith(".jpg")] - # change the file names for calibration images to match the name of cameras in config.yaml file.i.e. camera-1 and camera-2 + # change the file names for calibration images to match the name of + # cameras in config.yaml file.i.e. camera-1 and camera-2 cam1_images = glob.glob(os.path.join(cwd, "left*.jpg")) cam2_images = glob.glob(os.path.join(cwd, "right*.jpg")) # Sorting images @@ -159,13 +156,13 @@ for idx, name in enumerate(cam1_images): os.rename( name, - os.path.join(cwd, str("camera-1_" + "{0:0=2d}".format(idx + 1) + ".jpg")), + os.path.join(cwd, str("camera-1_" + f"{idx + 1:0=2d}" + ".jpg")), ) for idx, name in enumerate(cam2_images): os.rename( name, - os.path.join(cwd, str("camera-2_" + "{0:0=2d}".format(idx + 1) + ".jpg")), + os.path.join(cwd, str("camera-2_" + f"{idx + 1:0=2d}" + ".jpg")), ) # Removing some of the images where the corner was not detected @@ -180,13 +177,11 @@ print("TRIANGULATING") video_dir = os.path.join(os.path.dirname(basepath), folder) - deeplabcut.auxiliaryfunctions.edit_config( - path_config_file, edits={"pcutoff": 0.1} - ) # otherwise get all-nan slices + deeplabcut.auxiliaryfunctions.edit_config(path_config_file, edits={"pcutoff": 0.1}) # otherwise get all-nan slices deeplabcut.triangulate(path_config_file, video_dir, save_as_csv=True) print("CREATING LABELED VIDEO 3-D") - deeplabcut.create_labeled_video_3d(path_config_file, [video_dir], start=5, end=10) + deeplabcut.create_labeled_video_3d(path_config_file, [video_dir], start=5, end=10, video_extensions=".avi") # output_path = [os.path.join(basepath,folder)] # deeplabcut.create_labeled_video_3d(path_config_file,output_path,start=5,end=10) diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index dbaf8c67b8..01e033ee65 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -38,23 +37,19 @@ It produces nothing of interest scientifically. """ -task = "TEST-deterministic" # Enter the name of your experiment Task -scorer = "Alex" # Enter the name of the experimenter/labeler - +import os -import os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd import numpy as np +import pandas as pd +import deeplabcut + +task = "TEST-deterministic" # Enter the name of your experiment Task +scorer = "Alex" # Enter the name of the experimenter/labeler print("Imported DLC!") basepath = os.path.dirname(os.path.abspath("testscript.py")) videoname = "reachingvideo1" -video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) -] +video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # to test destination folder: # dfolder=basepath @@ -106,7 +101,7 @@ videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -118,7 +113,8 @@ print("CREATING TRAININGSET") deeplabcut.create_training_dataset(path_config_file) -# posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(1),'train/pose_cfg.yaml') +# posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] +# + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(1),'train/pose_cfg.yaml') shuffle = 1 posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) diff --git a/examples/testscript_mobilenets.py b/examples/testscript_mobilenets.py index e758b14c5a..18342cb347 100644 --- a/examples/testscript_mobilenets.py +++ b/examples/testscript_mobilenets.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -15,25 +14,27 @@ @author: alex DEVELOPERS: -This script tests various functionalities (creating project ,training, evaluating, outlierextraction, retraining...) in an automatic way. +This script tests various functionalities (creating project ,training, evaluating, outlierextraction, retraining...) in +an automatic way. For that purpose, it trains ResNet and MobileNet briefly on a "fake" dataset. It should take about 4:15 minutes to run this in a CPU. (incl. downloading the ResNet + MobileNet weights) It produces nothing of interest scientifically. """ + import os os.environ["DLClight"] = "True" -import deeplabcut from pathlib import Path -import pandas as pd + import numpy as np +import pandas as pd + +import deeplabcut -def Cuttrainingschedule( - path_config_file, shuffle, trainingsetindex=0, initweights="imagenet", lastvalue=10 -): +def Cuttrainingschedule(path_config_file, shuffle, trainingsetindex=0, initweights="imagenet", lastvalue=10): cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) posefile = os.path.join( cfg["project_path"], @@ -72,7 +73,7 @@ def Cuttrainingschedule( ) print("CHANGING training parameters to end quickly!") - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) + deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) return @@ -82,11 +83,7 @@ def Cuttrainingschedule( print("Imported DLC!") basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # to test destination folder: dfolder = os.path.join(basepath, "OUT") @@ -96,9 +93,7 @@ def Cuttrainingschedule( augmenter_type = "tensorpack" # imgaug' print("CREATING PROJECT") - path_config_file = deeplabcut.create_new_project( - task, scorer, video, copy_videos=True - ) + path_config_file = deeplabcut.create_new_project(task, scorer, video, copy_videos=True) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) cfg["numframes2pick"] = 5 @@ -143,7 +138,7 @@ def Cuttrainingschedule( videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -153,9 +148,7 @@ def Cuttrainingschedule( print("Plot labels...") deeplabcut.check_labels(path_config_file) - for shuffle, net_type in enumerate( - ["mobilenet_v2_0.35", "resnet_50"] - ): #'mobilenet_v2_1.0']): # 'resnet_50']): + for shuffle, net_type in enumerate(["mobilenet_v2_0.35", "resnet_50"]): #'mobilenet_v2_1.0']): # 'resnet_50']): """ if shuffle==0: keepdeconvweights=True @@ -164,9 +157,7 @@ def Cuttrainingschedule( """ print("CREATING TRAININGSET", net_type) if "resnet_50" == net_type: # this tests the default condition... - deeplabcut.create_training_dataset( - path_config_file, Shuffles=[shuffle], augmenter_type=augmenter_type - ) + deeplabcut.create_training_dataset(path_config_file, Shuffles=[shuffle], augmenter_type=augmenter_type) else: deeplabcut.create_training_dataset( path_config_file, @@ -200,7 +191,7 @@ def Cuttrainingschedule( shuffle=shuffle, save_as_csv=True, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("CREATE VIDEO") @@ -209,7 +200,7 @@ def Cuttrainingschedule( [newvideo], shuffle=shuffle, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("Making plots") @@ -218,7 +209,7 @@ def Cuttrainingschedule( [newvideo], shuffle=shuffle, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("EXTRACT OUTLIERS") @@ -230,7 +221,7 @@ def Cuttrainingschedule( epsilon=0, automatic=True, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) file = os.path.join( cfg["project_path"], @@ -242,9 +233,7 @@ def Cuttrainingschedule( print("RELABELING") DF = pd.read_hdf(file, "df_with_missing") DLCscorer = np.unique(DF.columns.get_level_values(0))[0] - DF.columns.set_levels( - [scorer.replace(DLCscorer, scorer)], level=0, inplace=True - ) + DF.columns.set_levels([scorer.replace(DLCscorer, scorer)], level=0, inplace=True) DF = DF.drop("likelihood", axis=1, level=2) DF.to_csv( os.path.join( @@ -261,7 +250,7 @@ def Cuttrainingschedule( vname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -270,17 +259,11 @@ def Cuttrainingschedule( deeplabcut.merge_datasets(path_config_file) print("CREATING TRAININGSET") - deeplabcut.create_training_dataset( - path_config_file, Shuffles=[shuffle], net_type=net_type - ) - Cuttrainingschedule( - path_config_file, shuffle, lastvalue=stoptrain, initweights="previteration" - ) + deeplabcut.create_training_dataset(path_config_file, Shuffles=[shuffle], net_type=net_type) + Cuttrainingschedule(path_config_file, shuffle, lastvalue=stoptrain, initweights="previteration") print("TRAINING from previous snapshot!!!!!") - deeplabcut.train_network( - path_config_file, shuffle=shuffle, keepdeconvweights=keepdeconvweights - ) + deeplabcut.train_network(path_config_file, shuffle=shuffle, keepdeconvweights=keepdeconvweights) print("ANALYZING some individual frames") deeplabcut.analyze_time_lapse_frames( diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 760464a24a..8faf1c3a5d 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -10,8 +9,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Created on Mon Nov 5 18:06:13 2018 +"""Created on Mon Nov 5 18:06:13 2018. @author: alex @@ -27,17 +25,17 @@ Results for 15001 training iterations: 95 1 train error: 2.89 pixels. Test error: 2.81 pixels. With pcutoff of 0.1 train error: 2.89 pixels. Test error: 2.81 pixels -The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for ffmpeg) to create the video. +The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for +ffmpeg) to create the video. """ -import deeplabcut + import os +import deeplabcut if __name__ == "__main__": # Loading example data set - path_config_file = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml" - ) + path_config_file = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml") deeplabcut.load_demo_data(path_config_file) shuffle = 13 @@ -45,9 +43,7 @@ cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) # example how to set pose config variables: - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) + posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) edits = {"save_iters": 15000, "display_iters": 1000, "multi_step": [[0.005, 15001]]} DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) @@ -58,12 +54,8 @@ deeplabcut.evaluate_network(path_config_file, Shuffles=[shuffle], plotting=True) print("Analyze Video") - videofile_path = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4" - ) - deeplabcut.analyze_videos( - path_config_file, [videofile_path], shuffle=shuffle - ) # ,videotype='.mp4') + videofile_path = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4") + deeplabcut.analyze_videos(path_config_file, [videofile_path], shuffle=shuffle) # ,videotype='.mp4') print("Create Labeled Video") deeplabcut.create_labeled_video( diff --git a/examples/testscript_openfielddata_augmentationcomparison.py b/examples/testscript_openfielddata_augmentationcomparison.py index c98b1944e4..6e88425d40 100644 --- a/examples/testscript_openfielddata_augmentationcomparison.py +++ b/examples/testscript_openfielddata_augmentationcomparison.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -10,9 +9,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" - -This is a test script to compare the loaders and models. +"""This is a test script to compare the loaders and models. This script creates one identical splits for the openfield test dataset and trains it with imgaug (default), scalecrop and the tensorpack loader. We also compare 3 backbones (mobilenet, resnet, efficientnet) @@ -52,15 +49,13 @@ Notice: despite the higher RMSE for imgaug due to the augmentation, the network performs much better on the testvideo (see Neuron Primer: https://www.cell.com/neuron/pdf/S0896-6273(20)30717-0.pdf) - """ - import os os.environ["CUDA_VISIBLE_DEVICES"] = str(0) + import deeplabcut -import numpy as np # Loading example data set path_config_file = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml") @@ -82,9 +77,7 @@ ) for idx, shuffle in enumerate(Shuffles): - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) + posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) # Setting specific parameters for training if idx % 3 == 0: # imgaug @@ -115,9 +108,7 @@ print("Analyze Video") - videofile_path = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4" - ) + videofile_path = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4") deeplabcut.analyze_videos(path_config_file, [videofile_path], shuffle=shuffle) diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index 3b09d0bc26..3a668c5caa 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -8,32 +8,26 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Testscript human network +"""Testscript human network.""" -""" -import os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd -import numpy as np +import os + +import deeplabcut Task = "human_dancing" YourName = "teamDLC" MODEL_NAME = "horse_sideview" # full_human" -basepath = os.path.dirname(os.path.abspath("testscript.py")) +basepath = os.path.dirname(os.path.abspath("testscript_tensorflow_single_animal.py")) videoname = "reachingvideo1" -video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) -] +video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # legacy mode: """ configfile, path_train_config=deeplabcut.create_pretrained_human_project(Task, YourName,video, videotype='avi', analyzevideo=True, - createlabeledvideo=True, copy_videos=False) #must leave copy_videos=True + createlabeledvideo=True, copy_videos=False) + #must leave copy_videos=True """ # new way: configfile, path_train_config = deeplabcut.create_pretrained_project( @@ -41,10 +35,11 @@ YourName, video, model=MODEL_NAME, - videotype="avi", + video_extensions="avi", analyzevideo=True, createlabeledvideo=True, copy_videos=False, + engine=deeplabcut.Engine.TF, ) # must leave copy_videos=True @@ -90,7 +85,7 @@ videoname, "CollectedData_" + cfg["scorer"] + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -166,7 +161,7 @@ videoname, "CollectedData_" + cfg["scorer"] + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) diff --git a/examples/testscript_pytorch_multi_animal.py b/examples/testscript_pytorch_multi_animal.py new file mode 100644 index 0000000000..4a4de3c55a --- /dev/null +++ b/examples/testscript_pytorch_multi_animal.py @@ -0,0 +1,139 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Testscript for single animal PyTorch projects.""" + +from __future__ import annotations + +from pathlib import Path + +from utils import ( + SyntheticProjectParameters, + cleanup, + create_fake_project, + log_step, + run, +) + +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.compat import Engine +from deeplabcut.pose_estimation_pytorch.config.utils import ( + is_model_cond_top_down, + is_model_top_down, +) + + +def main( + net_types: list[str], + params: SyntheticProjectParameters, + epochs: int = 1, + top_down_epochs: int = 1, + detector_epochs: int = 1, + save_epochs: int = 1, + batch_size: int = 1, + detector_batch_size: int = 1, + max_snapshots_to_keep: int = 5, + device: str = "cpu", + logger: dict | None = None, + conditions_shuffle: int = 0, + create_labeled_videos: bool = False, + delete_after_test_run: bool = False, +) -> None: + project_path = Path("synthetic-data-niels-multi-animal").resolve() + config_path = project_path / "config.yaml" + create_fake_project(path=project_path, params=params) + + engine = Engine.PYTORCH + cfg = af.read_config(config_path) + trainset_index = 0 + train_frac = cfg["TrainingFraction"][trainset_index] + try: + for net_type in net_types: + epochs_ = epochs + if is_model_top_down(net_type): + epochs_ = top_down_epochs + try: + pytorch_cfg_updates = { + "train_settings.display_iters": 50, + "train_settings.epochs": epochs_, + "train_settings.batch_size": batch_size, + "train_settings.dataloader_workers": 0, + "runner.device": device, + "runner.snapshots.save_epochs": save_epochs, + "runner.snapshots.max_snapshots": max_snapshots_to_keep, + "detector.train_settings.display_iters": 1, + "detector.train_settings.epochs": detector_epochs, + "detector.train_settings.batch_size": detector_batch_size, + "detector.train_settings.dataloader_workers": 0, + "detector.runner.snapshots.save_epochs": save_epochs, + "detector.runner.snapshots.max_snapshots": max_snapshots_to_keep, + "logger": logger, + } + if is_model_cond_top_down(net_type): + pytorch_cfg_updates["inference.conditions.shuffle"] = conditions_shuffle + pytorch_cfg_updates["inference.conditions.snapshot_index"] = -1 + run( + config_path=config_path, + train_fraction=train_frac, + trainset_index=trainset_index, + net_type=net_type, + videos=[str(project_path / "videos" / "video.mp4")], + device=device, + engine=engine, + pytorch_cfg_updates=pytorch_cfg_updates, + create_labeled_videos=create_labeled_videos, + ) + except Exception as err: + log_step(f"FAILED TO RUN {net_type}") + log_step(str(err)) + log_step("Continuing to next model") + raise err + + finally: + if delete_after_test_run: + cleanup(project_path) + + +if __name__ == "__main__": + wandb_logger = { + "type": "WandbLogger", + "project_name": "testscript-dev", + "run_name": "test-logging", + } + net_types = [ + "top_down_resnet_50", + "resnet_50", + "dekr_w32", + "rtmpose_m", + "ctd_coam_w32", + ] + main( + net_types=net_types, + params=SyntheticProjectParameters( + multianimal=True, + num_bodyparts=4, + num_individuals=3, + num_unique=0, + num_frames=25, + frame_shape=(256, 256), + ), + batch_size=2, + detector_batch_size=2, + epochs=8, + top_down_epochs=2, + detector_epochs=10, + save_epochs=4, + max_snapshots_to_keep=2, + device="cpu", # "cpu", "cuda:0", "mps" + logger=None, + conditions_shuffle=net_types.index("resnet_50") + 1, # shuffles start at index 1 + create_labeled_videos=True, + delete_after_test_run=True, + ) diff --git a/examples/testscript_pytorch_single_animal.py b/examples/testscript_pytorch_single_animal.py new file mode 100644 index 0000000000..8536f2be12 --- /dev/null +++ b/examples/testscript_pytorch_single_animal.py @@ -0,0 +1,110 @@ +"""Testscript for single animal PyTorch projects.""" + +from __future__ import annotations + +from pathlib import Path + +from utils import ( + SyntheticProjectParameters, + cleanup, + copy_project_for_test, + create_fake_project, + log_step, + run, +) + +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.compat import Engine + + +def main( + synthetic_data: bool, + net_types: list[str], + epochs: int = 1, + save_epochs: int = 1, + max_snapshots_to_keep: int = 5, + batch_size: int = 1, + device: str = "cpu", + logger: dict | None = None, + synthetic_data_params: SyntheticProjectParameters = None, + create_labeled_videos: bool = False, + delete_after_test_run: bool = False, +) -> None: + if synthetic_data_params is None: + synthetic_data_params = SyntheticProjectParameters( + multianimal=False, + num_bodyparts=6, + ) + engine = Engine.PYTORCH + if synthetic_data: + project_path = Path("synthetic-data-niels-single-animal").resolve() + videos = [str(project_path / "videos" / "video.mp4")] + create_fake_project(path=project_path, params=synthetic_data_params) + + else: + project_path = copy_project_for_test() + videos = [str(project_path / "videos" / "m3v1mp4.mp4")] + + config_path = project_path / "config.yaml" + cfg = af.read_config(config_path) + trainset_index = 0 + train_frac = cfg["TrainingFraction"][trainset_index] + try: + for net_type in net_types: + try: + run( + config_path=config_path, + train_fraction=train_frac, + trainset_index=trainset_index, + net_type=net_type, + videos=videos, + device=device, + engine=engine, + pytorch_cfg_updates={ + "train_settings.display_iters": 50, + "train_settings.epochs": epochs, + "train_settings.batch_size": batch_size, + "runner.device": device, + "runner.snapshots.save_epochs": save_epochs, + "runner.snapshots.max_snapshots": max_snapshots_to_keep, + "logger": logger, + }, + create_labeled_videos=create_labeled_videos, + ) + + except Exception as err: + log_step(f"FAILED TO RUN {net_type}") + log_step(str(err)) + log_step("Continuing to next model") + raise err + finally: + if delete_after_test_run: + cleanup(project_path) + + +if __name__ == "__main__": + wandb_logger = { + "type": "WandbLogger", + "project_name": "testscript-dev", + "run_name": "test-logging", + } + main( + synthetic_data=True, + net_types=["cspnext_m", "resnet_50", "hrnet_w32"], + batch_size=4, + epochs=8, + save_epochs=2, + max_snapshots_to_keep=2, + device="cpu", # "cpu", "cuda:0", "mps" + logger=None, + synthetic_data_params=SyntheticProjectParameters( + multianimal=False, + num_bodyparts=4, + num_individuals=1, + num_unique=0, + num_frames=12, + frame_shape=(128, 128), + ), + create_labeled_videos=True, + delete_after_test_run=True, + ) diff --git a/examples/testscript_superanimal_adaptation.py b/examples/testscript_superanimal_adaptation.py index 02a660313d..42d6e2fba9 100644 --- a/examples/testscript_superanimal_adaptation.py +++ b/examples/testscript_superanimal_adaptation.py @@ -8,19 +8,16 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Test script for super animal adaptation -""" -import deeplabcut +"""Test script for super animal adaptation.""" + import os +import deeplabcut if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "m3v1mp4" - video = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", videoname + ".mp4" - ) + video = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", videoname + ".mp4") video = deeplabcut.ShortenVideo( video, start="00:00:00", @@ -31,12 +28,14 @@ print("adaptation training for superanimal_topviewmouse") superanimal_name = "superanimal_topviewmouse" - videotype = ".mp4" + video_extensions = ".mp4" scale_list = [200, 300, 400] deeplabcut.video_inference_superanimal( [video], superanimal_name, - videotype=".mp4", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".mp4", video_adapt=True, scale_list=scale_list, pcutoff=0.1, diff --git a/examples/testscript_superanimal_create_pretrained_project.py b/examples/testscript_superanimal_create_pretrained_project.py new file mode 100644 index 0000000000..b697a3e285 --- /dev/null +++ b/examples/testscript_superanimal_create_pretrained_project.py @@ -0,0 +1,38 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Testscript for creating a pretrained project from a super animal model.""" + +import glob +import shutil +from pathlib import Path + +import deeplabcut + +if __name__ == "__main__": + superanimal_name = "superanimal_quadruped" + working_dir = Path(__file__).resolve().parent + video_dir = working_dir / "openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4" + project_name = "pretrained" + + deeplabcut.create_pretrained_project( + project_name, + "max", + [str(video_dir)], + engine=deeplabcut.Engine.PYTORCH, + ) + + dirs_to_delete = glob.glob(f"{working_dir}/{project_name}*") + + # Delete directories + for directory in dirs_to_delete: + shutil.rmtree(directory) + + print("Test passed!") diff --git a/examples/testscript_superanimal_inference.py b/examples/testscript_superanimal_inference.py index b4b49c42b6..4db84b2b91 100644 --- a/examples/testscript_superanimal_inference.py +++ b/examples/testscript_superanimal_inference.py @@ -8,22 +8,16 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Testscript for super animal inference +"""Testscript for super animal inference.""" -""" -import deeplabcut import os +import deeplabcut if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] print("testing superanimal_topviewmouse") superanimal_name = "superanimal_topviewmouse" @@ -31,7 +25,9 @@ deeplabcut.video_inference_superanimal( video, superanimal_name, - videotype=".avi", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".avi", scale_list=scale_list, ) @@ -40,6 +36,8 @@ deeplabcut.video_inference_superanimal( video, superanimal_name, - videotype=".avi", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".avi", scale_list=scale_list, ) diff --git a/examples/testscript_superanimal_transfer_learning.py b/examples/testscript_superanimal_transfer_learning.py index 1f36f99074..6d9c347e4f 100644 --- a/examples/testscript_superanimal_transfer_learning.py +++ b/examples/testscript_superanimal_transfer_learning.py @@ -8,24 +8,33 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Test script for super animal adaptation -""" -import deeplabcut +"""Test script for super animal adaptation.""" + import os +import deeplabcut +from deeplabcut.modelzoo.weight_initialization import build_weight_init + print(deeplabcut.__file__) if __name__ == "__main__": - superanimal_name = "superanimal_topviewmouse" basepath = os.path.dirname(os.path.realpath(__file__)) config_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "config.yaml") + model_name = "hrnet_w32" + detector_name = "fasterrcnn_resnet50_fpn_v2" - deeplabcut.create_training_dataset(config_path, superanimal_name=superanimal_name) + weight_init = build_weight_init( + cfg=config_path, + super_animal=superanimal_name, + model_name=model_name, + detector_name=detector_name, + with_decoder=False, + ) + deeplabcut.create_training_dataset(config_path, weight_init=weight_init) deeplabcut.train_network( config_path, - maxiters=10, + epochs=1, superanimal_name=superanimal_name, superanimal_transfer_learning=True, ) diff --git a/examples/testscript_multianimal.py b/examples/testscript_tensorflow_multi_animal.py similarity index 81% rename from examples/testscript_multianimal.py rename to examples/testscript_tensorflow_multi_animal.py index f1215d13e5..f5a69cef33 100644 --- a/examples/testscript_multianimal.py +++ b/examples/testscript_tensorflow_multi_animal.py @@ -9,16 +9,22 @@ # Licensed under GNU Lesser General Public License v3.0 # import os -import deeplabcut +import pickle +import random +from pathlib import Path + +import matplotlib import numpy as np import pandas as pd -import pickle + +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows + +import deeplabcut +from deeplabcut.core.engine import Engine from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions from deeplabcut.utils.auxfun_videos import VideoReader -import random -from pathlib import Path -MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0", "mobilenet_v2_0.35"] +MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0"] N_ITER = 5 @@ -31,6 +37,7 @@ SCORER = "dlc_team" NUM_FRAMES = 5 TRAIN_SIZE = 0.8 + ENGINE = Engine.TF # NET = "dlcr101_ms5" NET = "dlcrnet_ms5" @@ -42,14 +49,10 @@ DESTFOLDER = basepath video = "m3v1mp4" - video_path = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4" - ) + video_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4") print("Creating project...") - config_path = deeplabcut.create_new_project( - TASK, SCORER, [video_path], copy_videos=True, multianimal=True - ) + config_path = deeplabcut.create_new_project(TASK, SCORER, [video_path], copy_videos=True, multianimal=True) print("Project created.") @@ -78,33 +81,27 @@ bodyparts_single, bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) - animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [ - n_animals - ] * len(bodyparts_single) - map_ = dict(zip(range(len(animals)), animals)) + animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) + map_ = dict(zip(range(len(animals)), animals, strict=False)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2)] bodyparts += [bp for bp in bodyparts_single for _ in range(2)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], names=["scorer", "individuals", "bodyparts", "coords"], ) - index = [ - os.path.join(rel_folder, image) - for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png") - ] - fake_data = np.tile( - np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1) - ) + index = [os.path.join(rel_folder, image) for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png")] + fake_data = np.tile(np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1)) df = pd.DataFrame(fake_data, index=index, columns=columns) output_path = os.path.join(image_folder, f"CollectedData_{SCORER}.csv") df.to_csv(output_path) df.to_hdf( - output_path.replace("csv", "h5"), "df_with_missing", format="table", mode="w" + output_path.replace("csv", "h5"), + key="df_with_missing", + format="table", + mode="w", ) print("Artificial data created.") @@ -114,7 +111,10 @@ print("Creating train dataset...") deeplabcut.create_multianimaltraining_dataset( - config_path, net_type=NET, crop_size=(200, 200) + config_path, + net_type=NET, + crop_size=(200, 200), + engine=ENGINE, ) print("Train dataset created.") @@ -134,7 +134,7 @@ print("Editing pose config...") model_folder = auxiliaryfunctions.get_model_folder( - TRAIN_SIZE, 1, cfg, cfg["project_path"] + TRAIN_SIZE, 1, cfg, engine=ENGINE, modelprefix=cfg["project_path"] ) pose_config_path = os.path.join(model_folder, "train", "pose_cfg.yaml") edits = { @@ -153,9 +153,7 @@ print("Network trained.") print("Evaluating network...") - deeplabcut.evaluate_network( - config_path, plotting=True, per_keypoint_evaluation=True - ) + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) print("Network evaluated....") @@ -194,9 +192,7 @@ print("Video created.") print("Convert detections to tracklets...") - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.convert_detections2tracklets(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Tracklets created...") h5path = os.path.splitext(new_video_path)[0] + scorer + "_el.h5" try: @@ -213,9 +209,7 @@ individuals = [map_[ind] for ind in animals_id for _ in range(3)] scorer = [SCORER] * len(individuals) coords = ["x", "y", "likelihood"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(3) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(3)] bodyparts += [bp for bp in bodyparts_single for _ in range(3)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], @@ -227,9 +221,7 @@ df.to_hdf(h5path, key="data") print("Plotting trajectories...") - deeplabcut.plot_trajectories( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Trajectory plotted.") print("Creating labeled video...") @@ -244,15 +236,11 @@ print("Labeled video created.") print("Filtering predictions...") - deeplabcut.filterpredictions( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.filterpredictions(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Predictions filtered.") print("Extracting outlier frames...") - deeplabcut.extract_outlier_frames( - config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER - ) + deeplabcut.extract_outlier_frames(config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER) print("Outlier frames extracted.") vname = Path(new_video_path).stem @@ -284,23 +272,21 @@ vname, "CollectedData_" + SCORER + ".h5", ), - "df_with_missing", + key="df_with_missing", ) print("MERGING") deeplabcut.merge_datasets(config_path) # iteration + 1 print("CREATING TRAININGSET updated training set") - deeplabcut.create_training_dataset(config_path, net_type=NET) + deeplabcut.create_training_dataset(config_path, net_type=NET, engine=ENGINE) print("Training network...") deeplabcut.train_network(config_path, maxiters=N_ITER) print("Network trained.") print("Evaluating network...") - deeplabcut.evaluate_network( - config_path, plotting=True, per_keypoint_evaluation=True - ) + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) print("Network evaluated....") @@ -320,16 +306,16 @@ deeplabcut.export_model(config_path, shuffle=1, make_tar=False) print("Merging datasets...") - trainIndices, testIndices = deeplabcut.mergeandsplit( - config_path, trainindex=0, uniform=True - ) + trainIndices, testIndices = deeplabcut.mergeandsplit(config_path, trainindex=0, uniform=True) print("Creating two identical splits...") deeplabcut.create_multianimaltraining_dataset( config_path, Shuffles=[4, 5], + net_type=NET, trainIndices=[trainIndices, trainIndices], testIndices=[testIndices, testIndices], + engine=ENGINE, ) print("ALL DONE!!! - default multianimal cases are functional.") diff --git a/examples/testscript.py b/examples/testscript_tensorflow_single_animal.py similarity index 82% rename from examples/testscript.py rename to examples/testscript_tensorflow_single_animal.py index 3e6c57e2d5..65869d3b38 100644 --- a/examples/testscript.py +++ b/examples/testscript_tensorflow_single_animal.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -22,36 +21,36 @@ It produces nothing of interest scientifically. """ + import os -import deeplabcut import platform -import scipy.io as sio -import subprocess +import random from pathlib import Path +import matplotlib import numpy as np import pandas as pd +import scipy.io as sio +import deeplabcut +from deeplabcut.core.engine import Engine from deeplabcut.utils import auxiliaryfunctions -import random +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows USE_SHELVE = random.choice([True, False]) -MODELS = ["resnet_50", "efficientnet-b0", "mobilenet_v2_0.35"] +MODELS = ["resnet_50", "efficientnet-b0"] if __name__ == "__main__": task = "TEST" # Enter the name of your experiment Task scorer = "Alex" # Enter the name of the experimenter/labeler + engine = Engine.TF print("Imported DLC!") basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # For testing a color video: # videoname='baby4hin2min' @@ -71,12 +70,11 @@ else: augmenter_type3 = "tensorpack" # Does not work on WINDOWS - N_ITER = 5 + N_ITER = 6 + SAVE_ITER = 3 print("CREATING PROJECT") - path_config_file = deeplabcut.create_new_project( - task, scorer, video, copy_videos=True - ) + path_config_file = deeplabcut.create_new_project(task, scorer, video, copy_videos=True) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) cfg["numframes2pick"] = 5 @@ -91,6 +89,7 @@ print("CREATING-SOME LABELS FOR THE FRAMES") frames = os.listdir(os.path.join(cfg["project_path"], "labeled-data", videoname)) + frames = [fn for fn in frames if fn.endswith(".png")] # As this next step is manual, we update the labels by putting them on the diagonal (fixed for all frames) for index, bodypart in enumerate(cfg["bodyparts"]): columnindex = pd.MultiIndex.from_product( @@ -122,7 +121,7 @@ videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -133,7 +132,10 @@ print("CREATING TRAININGSET") deeplabcut.create_training_dataset( - path_config_file, net_type=NET, augmenter_type=augmenter_type + path_config_file, + net_type=NET, + augmenter_type=augmenter_type, + engine=engine, ) # Check the training image paths are correctly stored as arrays of strings @@ -166,7 +168,7 @@ ) DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(posefile) - DLC_config["save_iters"] = N_ITER + DLC_config["save_iters"] = SAVE_ITER DLC_config["display_iters"] = 2 print("CHANGING training parameters to end quickly!") @@ -177,7 +179,14 @@ print("EVALUATE") deeplabcut.evaluate_network( - path_config_file, plotting=True, per_keypoint_evaluation=True + path_config_file, + plotting=True, + per_keypoint_evaluation=True, + snapshots_to_evaluate=[ + "snapshot-3", + "snapshot-5", + "snapshot-6", + ], # snapshot-5 intentionally missing :) ) # deeplabcut.evaluate_network(path_config_file,plotting=True,trainingsetindex=33) print("CUT SHORT VIDEO AND ANALYZE (with dynamic cropping!)") @@ -193,10 +202,10 @@ outsuffix="short", outpath=os.path.join(cfg["project_path"], "videos"), ) - except: # if ffmpeg is broken/missing + except Exception: # if ffmpeg is broken/missing print("using alternative method") newvideo = os.path.join(cfg["project_path"], "videos", videoname + "short.mp4") - from moviepy.editor import VideoFileClip, VideoClip + from moviepy.editor import VideoClip, VideoFileClip clip = VideoFileClip(video[0]) clip.reader.initialize() @@ -218,14 +227,11 @@ def make_frame(t): ) print("analyze again...") - deeplabcut.analyze_videos( - path_config_file, [newvideo], save_as_csv=True, destfolder=DESTFOLDER - ) + deeplabcut.analyze_videos(path_config_file, [newvideo], save_as_csv=True, destfolder=DESTFOLDER) print("CREATE VIDEO") - deeplabcut.create_labeled_video( - path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True - ) + successful = deeplabcut.create_labeled_video(path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True) + assert all(successful), "Failed to create a labeled video!" print("Making plots") deeplabcut.plot_trajectories(path_config_file, [newvideo], destfolder=DESTFOLDER) @@ -275,16 +281,14 @@ def make_frame(t): vname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", ) print("MERGING") deeplabcut.merge_datasets(path_config_file) # iteration + 1 print("CREATING TRAININGSET") - deeplabcut.create_training_dataset( - path_config_file, net_type=NET, augmenter_type=augmenter_type2 - ) + deeplabcut.create_training_dataset(path_config_file, net_type=NET, augmenter_type=augmenter_type2, engine=engine) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) posefile = os.path.join( @@ -301,7 +305,7 @@ def make_frame(t): "train/pose_cfg.yaml", ) DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(posefile) - DLC_config["save_iters"] = N_ITER + DLC_config["save_iters"] = SAVE_ITER DLC_config["display_iters"] = 1 print("CHANGING training parameters to end quickly!") @@ -320,11 +324,9 @@ def make_frame(t): outpath=os.path.join(cfg["project_path"], "videos"), ) - except: # if ffmpeg is broken - newvideo2 = os.path.join( - cfg["project_path"], "videos", videoname + "short2.mp4" - ) - from moviepy.editor import VideoFileClip, VideoClip + except Exception: # if ffmpeg is broken + newvideo2 = os.path.join(cfg["project_path"], "videos", videoname + "short2.mp4") + from moviepy.editor import VideoClip, VideoFileClip clip = VideoFileClip(video[0]) clip.reader.initialize() @@ -349,27 +351,25 @@ def make_frame(t): ) print("Extracting skeleton distances, filter and plot filtered output") - deeplabcut.analyzeskeleton( - path_config_file, [newvideo2], save_as_csv=True, destfolder=DESTFOLDER - ) + deeplabcut.analyzeskeleton(path_config_file, [newvideo2], save_as_csv=True, destfolder=DESTFOLDER) deeplabcut.filterpredictions(path_config_file, [newvideo2]) - deeplabcut.create_labeled_video( + successful = deeplabcut.create_labeled_video( path_config_file, [newvideo2], destfolder=DESTFOLDER, displaycropped=True, filtered=True, ) + assert all(successful), "Failed to create a labeled video!" print("Creating a Johansson video!") - deeplabcut.create_labeled_video( + successful = deeplabcut.create_labeled_video( path_config_file, [newvideo2], destfolder=DESTFOLDER, keypoints_only=True ) + assert all(successful), "Failed to create a labeled video!" - deeplabcut.plot_trajectories( - path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True - ) + deeplabcut.plot_trajectories(path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True) print("ALL DONE!!! - default cases without Tensorpack loader are functional.") @@ -381,6 +381,7 @@ def make_frame(t): Shuffles=[2], net_type=NET, augmenter_type=augmenter_type3, + engine=engine, ) posefile = os.path.join( @@ -406,9 +407,7 @@ def make_frame(t): deeplabcut.auxiliaryfunctions.write_plainconfig(posefile, DLC_config) print("TRAINING shuffle 2, with smaller allocated memory") - deeplabcut.train_network( - path_config_file, shuffle=2, allow_growth=True, maxiters=updated_max_iters - ) + deeplabcut.train_network(path_config_file, shuffle=2, allow_growth=True, maxiters=updated_max_iters) print("ANALYZING some individual frames") deeplabcut.analyze_time_lapse_frames( @@ -420,9 +419,7 @@ def make_frame(t): deeplabcut.export_model(path_config_file, shuffle=2, make_tar=False) print("Merging datasets...") - trainIndices, testIndices = deeplabcut.mergeandsplit( - path_config_file, trainindex=0, uniform=True - ) + trainIndices, testIndices = deeplabcut.mergeandsplit(path_config_file, trainindex=0, uniform=True) print("Creating two identical splits...") deeplabcut.create_training_dataset( @@ -430,6 +427,7 @@ def make_frame(t): Shuffles=[4, 5], trainIndices=[trainIndices, trainIndices], testIndices=[testIndices, testIndices], + engine=engine, ) print("ALL DONE!!! - default cases are functional.") diff --git a/examples/testscript_transreid.py b/examples/testscript_transreid.py index b3467832ef..cb10de7f26 100644 --- a/examples/testscript_transreid.py +++ b/examples/testscript_transreid.py @@ -9,14 +9,16 @@ # Licensed under GNU Lesser General Public License v3.0 # import os -import deeplabcut -import numpy as np -import pandas as pd import pickle -from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions import random from pathlib import Path +import numpy as np +import pandas as pd + +import deeplabcut +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions + # MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0", "mobilenet_v2_0.35"] MODELS = [ "dlcrnet_ms5", @@ -43,14 +45,10 @@ DESTFOLDER = basepath video = "m3v1mp4" - video_path = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4" - ) + video_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4") print("Creating project...") - config_path = deeplabcut.create_new_project( - TASK, SCORER, [video_path], copy_videos=True, multianimal=True - ) + config_path = deeplabcut.create_new_project(TASK, SCORER, [video_path], copy_videos=True, multianimal=True) print("Project created.") @@ -79,34 +77,23 @@ bodyparts_single, bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) - animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [ - n_animals - ] * len(bodyparts_single) - map_ = dict(zip(range(len(animals)), animals)) + animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) + map_ = dict(zip(range(len(animals)), animals, strict=False)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2)] bodyparts += [bp for bp in bodyparts_single for _ in range(2)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], names=["scorer", "individuals", "bodyparts", "coords"], ) - index = [ - os.path.join(rel_folder, image) - for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png") - ] - fake_data = np.tile( - np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1) - ) + index = [os.path.join(rel_folder, image) for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png")] + fake_data = np.tile(np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1)) df = pd.DataFrame(fake_data, index=index, columns=columns) output_path = os.path.join(image_folder, f"CollectedData_{SCORER}.csv") df.to_csv(output_path) - df.to_hdf( - output_path.replace("csv", "h5"), "df_with_missing", format="table", mode="w" - ) + df.to_hdf(output_path.replace("csv", "h5"), key="df_with_missing", format="table", mode="w") print("Artificial data created.") print("Checking labels...") @@ -114,9 +101,7 @@ print("Labels checked.") print("Creating train dataset...") - deeplabcut.create_multianimaltraining_dataset( - config_path, net_type=NET, crop_size=(200, 200) - ) + deeplabcut.create_multianimaltraining_dataset(config_path, net_type=NET, crop_size=(200, 200)) print("Train dataset created.") # Check the training image paths are correctly stored as arrays of strings @@ -134,9 +119,7 @@ assert all(len(pickledata[i]["joints"]) == 3 for i in range(num_images)) print("Editing pose config...") - model_folder = auxiliaryfunctions.get_model_folder( - TRAIN_SIZE, 1, cfg, cfg["project_path"] - ) + model_folder = auxiliaryfunctions.get_model_folder(TRAIN_SIZE, 1, cfg, cfg["project_path"]) pose_config_path = os.path.join(model_folder, "train", "pose_cfg.yaml") edits = { "global_scale": 0.5, @@ -191,9 +174,7 @@ print("Video created.") print("Convert detections to tracklets...") - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.convert_detections2tracklets(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Tracklets created...") ### adding it here @@ -202,9 +183,7 @@ trainposeconfigfile, testposeconfigfile, snapshotfolder, - ) = deeplabcut.return_train_network_path( - config_path, shuffle=1, modelprefix=modelprefix, trainingsetindex=0 - ) + ) = deeplabcut.return_train_network_path(config_path, shuffle=1, modelprefix=modelprefix, trainingsetindex=0) print("Creating triplet dataset") @@ -212,7 +191,7 @@ config_path, [new_video_path], TESTTRACKER, - videotype="mp4", + video_extensions="mp4", ) train_epochs = 10 @@ -230,9 +209,7 @@ ckpt_folder=snapshotfolder, ) - transformer_checkpoint = os.path.join( - snapshotfolder, f"dlc_transreid_{train_epochs}.pth" - ) + transformer_checkpoint = os.path.join(snapshotfolder, f"dlc_transreid_{train_epochs}.pth") print("Stitching tracklets based on transformer") @@ -245,9 +222,7 @@ ) print("Plotting trajectories...") - deeplabcut.plot_trajectories( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Trajectory plotted.") print("Creating labeled video...") @@ -262,15 +237,11 @@ print("Labeled video created.") print("Filtering predictions...") - deeplabcut.filterpredictions( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.filterpredictions(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Predictions filtered.") print("Extracting outlier frames...") - deeplabcut.extract_outlier_frames( - config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER - ) + deeplabcut.extract_outlier_frames(config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER) print("Outlier frames extracted.") vname = Path(new_video_path).stem @@ -303,7 +274,7 @@ vname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -329,7 +300,7 @@ config_path, [new_video_path], shuffle=3, - videotype="mp4", + video_extensions="mp4", save_as_csv=True, destfolder=DESTFOLDER, cropping=[0, 50, 0, 50], @@ -345,7 +316,7 @@ deeplabcut.transformer_reID( config_path, [new_video_path], - videotype="mp4", + video_extensions="mp4", shuffle=3, n_tracks=n_tracks, track_method=TESTTRACKER, @@ -359,7 +330,7 @@ deeplabcut.create_labeled_video( config_path, [new_video_path], - videotype="mp4", + video_extensions="mp4", shuffle=3, track_method="ellipse", destfolder=DESTFOLDER, @@ -368,7 +339,7 @@ deeplabcut.create_labeled_video( config_path, [new_video_path], - videotype="mp4", + video_extensions="mp4", shuffle=3, track_method="transformer", destfolder=DESTFOLDER, diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 0000000000..9390cea392 --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,438 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +import shutil +import string +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import matplotlib + +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows + +import cv2 +import numpy as np +import pandas as pd +from PIL import Image + +import deeplabcut +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.compat import Engine +from deeplabcut.generate_training_dataset import get_existing_shuffle_indices + + +def log_step(message: Any) -> None: + print(100 * "-") + print(str(message)) + print(100 * "-") + + +def cleanup(test_path: Path) -> None: + if test_path.exists(): + shutil.rmtree(test_path) + + +@dataclass(frozen=True) +class SyntheticProjectParameters: + multianimal: bool + num_bodyparts: int + num_frames: int = 10 + num_individuals: int = 1 + num_unique: int = 0 + identity: bool = False + frame_shape: tuple[int, int] = (480, 640) + + def bodyparts(self) -> list[str]: + return [i for i in string.ascii_lowercase[: self.num_bodyparts]] + + def unique(self) -> list[str]: + return [f"unique_{i}" for i in string.ascii_lowercase[: self.num_unique]] + + def individuals(self) -> list[str]: + return [f"animal_{i}" for i in range(self.num_individuals)] + + +def sample_pose_random( + gen: np.random.Generator, + num_individuals: int, + num_bodyparts: int, + num_unique: int, + img_h: int, + img_w: int, +) -> np.ndarray: + """Fully random pose sampling.""" + xs = gen.choice(img_w, size=(num_individuals, num_bodyparts), replace=False) + ys = gen.choice(img_h, size=(num_individuals, num_bodyparts), replace=False) + pose = np.stack([xs, ys], axis=-1) + + image_data = pose.reshape(-1) + if num_unique > 0: + unique_pose = np.stack( + [ + gen.choice(img_w, size=(1, num_unique), replace=False), + gen.choice(img_h, size=(1, num_unique), replace=False), + ], + axis=-1, + ) + image_data = np.concatenate([image_data, unique_pose.reshape(-1)]) + return image_data + + +def sample_pose_from_center( + center_xs: np.ndarray, + center_ys: np.ndarray, + num_individuals: int, + num_bodyparts: int, + num_unique: int, + radius: int = 25, +) -> np.ndarray: + """Sample keypoints from the center of each individual.""" + pose = np.zeros((num_individuals, num_bodyparts, 2)) + for i, (xc, yc) in enumerate(zip(center_xs, center_ys, strict=False)): + if i < num_individuals: + x_start, x_end = xc - radius + 1, xc + radius - 1 + y_start, y_end = yc - radius + 1, yc + radius - 1 + pose[i, :, 0] = np.linspace(start=x_start, stop=x_end, num=num_bodyparts) + pose[i, :, 1] = np.linspace(start=y_start, stop=y_end, num=num_bodyparts) + + image_data = pose.reshape(-1) + if num_unique > 0: + xc, yc = center_xs[-1], center_ys[-1] + x_start, x_end = xc - radius + 1, xc + radius - 1 + y_start, y_end = yc - radius + 1, yc + radius - 1 + unique_pose = np.zeros((1, num_unique, 2)) + unique_pose[0, :, 0] = np.linspace(start=x_start, stop=x_end, num=num_unique) + unique_pose[0, :, 1] = np.linspace(start=y_start, stop=y_end, num=num_unique) + image_data = np.concatenate([image_data, unique_pose.reshape(-1)]) + return image_data + + +def gen_fake_data( + scorer: str, + video_name: str, + params: SyntheticProjectParameters, +) -> pd.DataFrame: + kpt_entries = ["x", "y"] + col_names = ["scorer", "individuals", "bodyparts", "coords"] + col_values = [] + for i in params.individuals(): + for b in params.bodyparts(): + col_values += [(scorer, i, b, entry) for entry in kpt_entries] + + for unique_bpt in params.unique(): + col_values += [(scorer, "single", unique_bpt, entry) for entry in kpt_entries] + + index_data = [] + pose_data = [] + gen = np.random.default_rng(seed=0) + + # sample starting points for each individual + img_h, img_w = params.frame_shape[:2] + radius = 8 + center_xs = gen.choice( + np.arange(radius, img_w - radius), + size=params.num_individuals + 1, # in case unique bodyparts + replace=False, + ) + center_ys = gen.choice( + np.arange(radius, img_h - radius), + size=params.num_individuals + 1, # in case unique bodyparts + replace=False, + ) + + for frame_index in range(params.num_frames): + index_data.append(("labeled-data", video_name, f"img{frame_index:04}.png")) + pose_data.append( + sample_pose_from_center( + center_xs, + center_ys, + num_individuals=params.num_individuals, + num_bodyparts=params.num_bodyparts, + num_unique=params.num_unique, + radius=radius, + ) + ) + mvt_x = gen.integers(low=-1, high=4, size=center_xs.size) + mvt_y = gen.integers(low=-1, high=4, size=center_ys.size) + center_xs = np.clip(center_xs + mvt_x, radius, img_w - radius) + center_ys = np.clip(center_ys + mvt_y, radius, img_h - radius) + + pose = np.stack(pose_data) + pose[params.num_frames // 2, :] = np.nan # add missing row in a frame + for idv in range(params.num_individuals): + idv_start = 2 * params.num_bodyparts * idv + idv_end = 2 * params.num_bodyparts * (idv + 1) + if params.num_frames > idv + 1: + pose[idv + 1, idv_start:idv_end] = np.nan + + for bpt in range(params.num_bodyparts): + frame_idx = 1 + params.num_individuals + bpt + idv_idx = bpt % params.num_individuals + offset = 2 * params.num_bodyparts * idv_idx + bpt_start, bpt_end = 2 * bpt + offset, 2 * (bpt + 1) + offset + if params.num_frames + 1 > frame_idx: + pose[frame_idx, bpt_start:bpt_end] = np.nan + + return pd.DataFrame( + pose, + index=pd.MultiIndex.from_tuples(index_data), + columns=pd.MultiIndex.from_tuples(col_values, names=col_names), + ) + + +def gen_fake_image( + project_root: Path, + row: pd.Series, + params: SyntheticProjectParameters, + radius: int = 5, +): + img_h, img_w = params.frame_shape + image_array = np.zeros((*params.frame_shape, 3), dtype=np.uint8) + for i, idv in enumerate(params.individuals()): + r = int(255 * (i + 1) / params.num_individuals) + if "individuals" in row.index.names: + idv_data = row.droplevel("scorer").loc[idv] + else: + idv_data = row.droplevel("scorer") + + keypoints = idv_data.to_numpy().reshape((-1, 2)) + if not np.all(np.isnan(keypoints)): + idv_center = np.nanmean(keypoints, axis=0) + x, y = int(idv_center[0]), int(idv_center[1]) + xmin, xmax = max(0, x - radius), min(img_w - 1, x + radius) + ymin, ymax = max(0, y - radius), min(img_h - 1, y + radius) + image_array[ymin:ymax, xmin:xmax, 0] = r + + for j, bpt in enumerate(params.bodyparts()): + g = int(255 * (j + 1) / params.num_bodyparts) + + bpt_data = idv_data.loc[bpt] + if np.all(~pd.isnull(bpt_data)): + x, y = int(bpt_data.x), int(bpt_data.y) + xmin, xmax = max(0, x - radius), min(img_w - 1, x + radius) + ymin, ymax = max(0, y - radius), min(img_h - 1, y + radius) + image_array[ymin:ymax, xmin:xmax, 0] = r + image_array[ymin:ymax, xmin:xmax, 1] = g + + if params.num_unique > 0: + unique_data = row.droplevel("scorer").loc["single"] + for i, unique_bpt in enumerate(params.unique()): + bpt_data = unique_data.loc[unique_bpt] + if np.all(~pd.isnull(bpt_data)): + x, y = int(bpt_data.x), int(bpt_data.y) + xmin, xmax = max(0, x - radius), min(img_w - 1, x + radius) + ymin, ymax = max(0, y - radius), min(img_h - 1, y + radius) + image_array[ymin:ymax, xmin:xmax, 2] = int(255 * (i + 1) / params.num_unique) + + img = Image.fromarray(image_array) + img.save(project_root / Path(*row.name)) + + +def generate_video_from_images(image_dir: Path, output_video: Path) -> None: + images = [p for p in image_dir.iterdir() if p.is_file() and p.suffix == ".png"] + images = sorted(images, key=lambda f: f.stem) + if len(images) == 0: + return + + height, width, channels = cv2.imread(str(images[0])).shape + fourcc = cv2.VideoWriter_fourcc(*"MJPG") + out = cv2.VideoWriter(str(output_video), fourcc, 10, (width, height)) + for img_path in images: + img = cv2.imread(str(img_path)) + out.write(img) + out.release() + + +def create_fake_project(path: Path, params: SyntheticProjectParameters) -> None: + if path.exists(): + raise ValueError("Cannot create a fake project at an existing path") + + scorer = "synthetic" + video_name = "cat" + path.mkdir(parents=True, exist_ok=False) + config = { + "Task": "synthetic", + "scorer": scorer, + "date": "Nov11", + "multianimalproject": params.multianimal, + "identity": params.identity, + "project_path": str(path / "config.yaml"), + "TrainingFraction": [0.8], + "iteration": 0, + "default_net_type": "resnet_50", + "default_augmenter": "default", + "default_track_method": "ellipse", + "snapshotindex": "all", + "batch_size": 8, + "pcutoff": 0.6, + "video_sets": { + str(path / "videos" / video_name): { + "crop": (0, params.frame_shape[1], 0, params.frame_shape[0]), + }, + }, + "start": 0, + "stop": 1, + "numframes2pick": 10, + "dotsize": 4, + "alphavalue": 1.0, + "colormap": "rainbow", + } + if not params.multianimal: + config["bodyparts"] = params.bodyparts() + assert params.num_individuals == 1 + assert params.num_unique == 0 + else: + config["bodyparts"] = "MULTI!" + config["multianimalbodyparts"] = params.bodyparts() + config["uniquebodyparts"] = params.unique() + config["individuals"] = params.individuals() + + af.write_config(str(path / "config.yaml"), config) + image_dir = path / "labeled-data" / video_name + image_dir.mkdir(parents=True, exist_ok=False) + + df = gen_fake_data( + scorer=scorer, + video_name=video_name, + params=params, + ) + print("SYNTHETIC DATA:") + print(df) + print("\n") + if not params.multianimal: + df.columns = df.columns.droplevel("individuals") + + df.to_hdf(image_dir / f"CollectedData_{scorer}.h5", key="df_with_missing") + df.to_csv(image_dir / f"CollectedData_{scorer}.csv") + + for idx in range(params.num_frames): + gen_fake_image(path, df.iloc[idx], params=params, radius=5) + + output_video = path / "videos" / "video.mp4" + output_video.parent.mkdir(exist_ok=True) + generate_video_from_images(image_dir, output_video) + + +def copy_project_for_test() -> Path: + data_path = Path.cwd() / "openfield-Pranav-2018-10-30" + test_path = Path.cwd() / "pytorch-testscript1234-openfield-Pranav-2018-10-30" + if not test_path.exists(): + shutil.copytree(data_path, test_path) + + project_config = af.read_config(str(test_path / "config.yaml")) + videos = list(project_config["video_sets"].keys()) + video = videos[0] + crop = project_config["video_sets"][video] + project_config["video_sets"] = {str(test_path / "videos" / "m3v1mp4.mp4"): crop} + af.write_config(str(test_path / "config.yaml"), project_config) + return test_path + + +def run( + config_path: Path, + train_fraction: float, + trainset_index: int, + net_type: str, + videos: list[str], + device: str, + engine: Engine = Engine.PYTORCH, + pytorch_cfg_updates: dict | None = None, + create_labeled_videos: bool = False, +) -> None: + times = [time.time()] + log_step(f"Testing with net type {net_type}") + log_step("Creating the training dataset") + deeplabcut.create_training_dataset(str(config_path), net_type=net_type, engine=engine) + existing_shuffles = get_existing_shuffle_indices(config_path, train_fraction=train_fraction, engine=engine) + shuffle_index = existing_shuffles[-1] + + log_step(f"Starting training for train_frac {train_fraction}, shuffle {shuffle_index}") + deeplabcut.train_network( + config=str(config_path), + shuffle=shuffle_index, + trainingsetindex=trainset_index, + device=device, + pytorch_cfg_updates=pytorch_cfg_updates, + ) + times.append(time.time()) + log_step(f"Train time: {times[-1] - times[-2]} seconds") + + log_step(f"Starting evaluation for train_frac {train_fraction}, shuffle {shuffle_index}") + deeplabcut.evaluate_network( + config=str(config_path), + Shuffles=[shuffle_index], + trainingsetindex=trainset_index, + device=device, + plotting=True, + per_keypoint_evaluation=True, + ) + times.append(time.time()) + log_step(f"Evaluation time: {times[-1] - times[-2]} seconds") + + if len(videos) > 0: + log_step(f"Analyzing videos for {train_fraction}, shuffle {shuffle_index}") + video_kwargs = dict(videos=videos, shuffle=shuffle_index, trainingsetindex=trainset_index) + deeplabcut.analyze_videos(str(config_path), **video_kwargs, device=device, auto_track=False) + times.append(time.time()) + log_step(f"Video analysis time: {times[-1] - times[-2]} seconds") + log_step(f"Total test time: {times[-1] - times[0]} seconds") + + cfg = af.read_config(config_path) + if cfg.get("multianimalproject"): + if create_labeled_videos: + deeplabcut.create_video_with_all_detections(str(config_path), **video_kwargs) + + # relaxed tracking parameters + deeplabcut.convert_detections2tracklets( + str(config_path), + **video_kwargs, + inferencecfg=dict( + boundingboxslack=10, + iou_threshold=0.2, + max_age=5, + method="m1", + min_hits=1, + minimalnumberofconnections=2, + pafthreshold=0.1, + pcutoff=0.1, + topktoretain=3, + variant=0, + withid=False, + ), + ) + deeplabcut.stitch_tracklets(str(config_path), **video_kwargs, min_length=3) + + if create_labeled_videos: + log_step(f"Making labeled video, {train_fraction}, shuffle={shuffle_index}") + results = deeplabcut.create_labeled_video( + config=str(config_path), + videos=videos, + shuffle=shuffle_index, + trainingsetindex=trainset_index, + ) + assert all(results), f"Failed to create some labeled video for {videos}" + + +if __name__ == "__main__": + create_fake_project( + path=Path("synthetic-data-niels"), + params=SyntheticProjectParameters( + multianimal=True, + num_bodyparts=4, + num_individuals=3, + num_unique=1, + num_frames=50, + frame_shape=(128, 256), + ), + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..a578f17dd9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,218 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ "setuptools>=61" ] + +[project] +name = "deeplabcut" +version = "3.0.0" +description = "Markerless pose-estimation of user-defined features with deep learning" +readme = { file = "README.md", content-type = "text/markdown" } +keywords = [ + "animal behavior", + "markerless tracking", + "neuroscience", + "pose estimation", +] +license = { text = "LGPL-3.0-or-later" } +requires-python = ">=3.10" +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Image Processing", +] +dependencies = [ + "albumentations<=1.4.3", + "dlclibrary>=0.0.12", + "einops", + "filelock>=3.12,<3.16", + "filterpy>=1.4.4", + "h5py>=3.15.1; platform_system=='Darwin'", + "huggingface-hub>=0.23", + "imageio-ffmpeg", + "imgaug>=0.4", + "matplotlib>=3.3,<3.9,!=3.7,!=3.7.1", + "networkx>=2.6", + "numba>=0.54", + "numpy>=1.18.5,<2", + "packaging>=26", + "pandas[hdf5,performance]>=2.2,<3", + "pillow>=7.1", + "pycocotools", + "pydantic>=2,<3", + "pyyaml", + "ruamel-yaml>=0.15", + "scikit-image>=0.17", + "scikit-learn>=1", + "scipy>=1.9", + "statsmodels>=0.11", + "tables>3.8", + "timm", + "torch>=2", + "torchvision", + "tqdm", +] +[[project.authors]] +name = "M-Lab of Adaptive Intelligence" +email = "mackenzie@deeplabcut.org" +[[project.authors]] +name = "Mathis Group for Computational Neuroscience and AI" +email = "alexander@deeplabcut.org" +[project.optional-dependencies] +gui = [ + "napari-deeplabcut>=0.3.1", + "pyside6; platform_system!='Linux' or platform_machine!='x86_64'", + # Avoid 6.10.0 only on Linux x86_64 (fails for older glib versions) + "pyside6<6.10; platform_system=='Linux' and platform_machine=='x86_64'", + "qdarkstyle==3.1", +] +openvino = [ "openvino-dev==2022.1" ] +docs = [ + "jupyter-book==1.0.4.post1", + "numpydoc", + "sphinxcontrib-mermaid", +] +fmpose3d = [ "fmpose3d>=0.0.8" ] +# Use only one of [tf, tf-cu11, tf-cu12, tf-latest]. Do not combine extras. +tf = [ + "protobuf<7", + "tensorflow>=2.12,<2.16; python_version<'3.12'", + "tensorflow>=2.16.1,<2.18; python_version>='3.12'", + "tensorflow-io-gcs-filesystem==0.31; platform_system=='Windows' and python_version<'3.12'", + "tensorflow-metal==1.2; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow-metal>=1.2; platform_system=='Darwin' and python_version>='3.12'", + "tensorpack>=0.11", + "tf-keras<2.15; python_version<'3.12'", + "tf-keras>=2.15,<2.18; python_version>='3.12'", + "tf-slim>=1.1", +] +tf-cu11 = [ + "protobuf<7", + "tensorflow==2.14", + "tensorflow-io-gcs-filesystem==0.31; platform_system=='Windows'", + "tensorflow-metal==1.2; platform_system=='Darwin'", + "tensorpack==0.11", + "tf-keras==2.14.1", + "tf-slim==1.1", + "torch<2.1", + "torchvision<0.16", +] +tf-cu12 = [ + "protobuf<7", + "tensorflow==2.18", + "tensorflow-metal==1.2; platform_system=='Darwin'", + "tensorpack==0.11", + "tf-keras==2.18", + "tf-slim==1.1", + "torch<2.11", + "torchvision<0.26", +] +tf-latest = [ + "protobuf<7", + "tensorflow>=2.18", + "tensorflow-metal>=1.2; platform_system=='Darwin'", + "tensorpack>=0.11", + "tf-keras", + "tf-slim>=1.1", +] +# apple_mchips is kept for older systems, prefer [tf] in new projects. +apple_mchips = [ + "protobuf<7; platform_system=='Darwin'", + "tensorflow>=2.12,<2.15; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow>=2.15,<2.18; platform_system=='Darwin' and python_version>='3.12'", + "tensorflow-metal==1.2; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow-metal>=1.2; platform_system=='Darwin' and python_version>='3.12'", + "tensorpack>=0.11; platform_system=='Darwin'", + "tf-keras; platform_system=='Darwin'", + "tf-slim>=1.1; platform_system=='Darwin'", +] +modelzoo = [ "huggingface-hub" ] +wandb = [ "wandb" ] +[project.scripts] +dlc = "deeplabcut.__main__:main" +[project.urls] +Homepage = "https://www.deeplabcut.org" +Repository = "https://github.com/DeepLabCut/DeepLabCut" +Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" + +[dependency-groups] +dev = [ + "coverage", + "nbformat>5", + "pre-commit", + "pytest", + "pytest-cov", + "ruff", +] + +[tool.setuptools] +include-package-data = false +[tool.setuptools.package-data] +"*" = [ "*.yaml", "*.yml", "*.json", "*.qss", "*.png", "*.md", "*.sh" ] +[tool.setuptools.packages.find] +include = [ "deeplabcut*" ] +exclude = [ "tests*", "docs*", "examples*" ] + +[tool.uv] +# One of tf / tf-cu12 / tf-latest. apple_mchips matches [tf] on macOS but conflicts with +# [tf-cu12] and [tf-latest] (overlapping tensorflow pins cannot be unified with uv's lock). +conflicts = [ + [ + { extra = "tf" }, + { extra = "tf-cu11" }, + { extra = "tf-cu12" }, + { extra = "tf-latest" }, + { extra = "apple_mchips" }, + ], + [ + { extra = "tf-cu11" }, + { extra = "tf-cu12" }, + { extra = "fmpose3d" }, + ], +] +[[tool.uv.dependency-metadata]] +name = "openvino-dev" +version = "2022.1.0" +requires-dist = [] +[tool.uv.pip] +torch-backend = "auto" + +[tool.ruff] +target-version = "py310" +line-length = 120 +fix = true +[tool.ruff.lint] +select = [ "E", "F", "B", "I", "UP" ] +ignore = [ "E741", "B007" ] +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ "F401", "E402" ] +"deeplabcut/**/__init__.py" = [ "F403" ] +"deeplabcut/gui/window.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py" = [ "F403" ] +"*.ipynb" = [ "E402" ] +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.pyproject-fmt] +max_supported_python = "3.12" +generate_python_version_classifiers = true +# Avoid collapsing tables to field.key = value format (less readable) +table_format = "long" + +[tool.pytest.ini_options] +markers = [ + "require_models: mark test as requiring models to run", + "fmpose3d: tests for fmpose3d integration", + "unittest: fast unit-level tests", + "functional: functional/integration-style tests", + "deprecated: tests for deprecated APIs kept for backward-compatibility", +] diff --git a/reinstall.sh b/reinstall.sh index 3c73d45b83..ec60dc6a8e 100755 --- a/reinstall.sh +++ b/reinstall.sh @@ -1,3 +1,4 @@ pip uninstall deeplabcut +rm -rf dist/ build/ *.egg-info python3 setup.py sdist bdist_wheel -pip install dist/deeplabcut-2.3.9-py3-none-any.whl +pip install dist/deeplabcut-3.0.0-py3-none-any.whl diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5a0ef62d49..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -dlclibrary -ipython -filterpy -ruamel.yaml>=0.15.0 -intel-openmp -imageio-ffmpeg -imgaug==0.4.0 -numba>=0.54.0 -matplotlib<=3.5.2 -networkx>=2.6 -numpy>=1.18.5 -pandas>=1.0.1,!=1.5.0 -pyyaml -scikit-image>=0.17 -scikit-learn>=1.0 -scipy>=1.9 -statsmodels>=0.11 -tensorflow>=2.0,<2.13.0 -tables==3.8.0 -tensorpack==0.11 -tf_slim==1.1.0 -torch==1.12 -tqdm -Pillow>=7.1 diff --git a/setup.py b/setup.py index 9915904ac5..c3e301e85e 100644 --- a/setup.py +++ b/setup.py @@ -1,102 +1,12 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +"""DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) © A. + +& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for +contributors. +https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ -import setuptools - -with open("README.md", encoding="utf-8", errors="replace") as fh: - long_description = fh.read() - - -setuptools.setup( - name="deeplabcut", - version="2.3.9", - author="A. & M. Mathis Labs", - author_email="alexander@deeplabcut.org", - description="Markerless pose-estimation of user-defined features with deep learning", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/DeepLabCut/DeepLabCut", - install_requires=[ - "dlclibrary>=0.0.6", - "filterpy>=1.4.4", - "ruamel.yaml>=0.15.0", - "imgaug>=0.4.0", - "imageio-ffmpeg", - "numba>=0.54", - "matplotlib>=3.3,!=3.7.0,!=3.7.1", - "networkx>=2.6", - "numpy>=1.18.5", - "pandas>=1.0.1,!=1.5.0", - "scikit-image>=0.17", - "scikit-learn>=1.0", - "scipy>=1.9", - "statsmodels>=0.11", - "tables>=3.7.0", - "torch<=1.12", - "tensorpack>=0.11", - "tf_slim>=1.1.0", - "tqdm", - "pyyaml", - "Pillow>=7.1", - ], - extras_require={ - "gui": [ - "pyside6<6.3.2", - "qdarkstyle==3.1", - "napari-deeplabcut>=0.2.1.2", - ], - "openvino": ["openvino-dev==2022.1.0"], - "docs": ["numpydoc"], - "tf": [ - "tensorflow>=2.0,<=2.10" - ], # Last supported TF version on Windows Native is 2.10 - "apple_mchips": ["tensorflow-macos<2.13.0", "tensorflow-metal"], - "modelzoo": ["huggingface_hub"], - }, - scripts=["deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh"], - packages=setuptools.find_packages(), - data_files=[ - ( - "deeplabcut", - [ - "deeplabcut/pose_cfg.yaml", - "deeplabcut/inference_cfg.yaml", - "deeplabcut/reid_cfg.yaml", - "deeplabcut/pose_estimation_tensorflow/models/pretrained/pretrained_model_urls.yaml", - "deeplabcut/pose_estimation_tensorflow/superanimal_configs/superquadruped.yaml", - "deeplabcut/pose_estimation_tensorflow/superanimal_configs/supertopview.yaml", - "deeplabcut/gui/style.qss", - "deeplabcut/gui/media/logo.png", - "deeplabcut/gui/media/dlc_1-01.png", - "deeplabcut/gui/assets/logo.png", - "deeplabcut/gui/assets/logo_transparent.png", - "deeplabcut/gui/assets/welcome.png", - "deeplabcut/gui/assets/icons/help.png", - "deeplabcut/gui/assets/icons/help2.png", - "deeplabcut/gui/assets/icons/new_project.png", - "deeplabcut/gui/assets/icons/new_project2.png", - "deeplabcut/gui/assets/icons/open.png", - "deeplabcut/gui/assets/icons/open2.png", - "deeplabcut/modelzoo/models.json", - ], - ) - ], - include_package_data=True, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Operating System :: OS Independent", - ], - entry_points="""[console_scripts] - dlc=dlc:main""", -) +from setuptools import setup -# https://www.python.org/dev/peps/pep-0440/#compatible-release +# All configuration is now in pyproject.toml. This file is kept for backward compatibility +setup() diff --git a/tests/conftest.py b/tests/conftest.py index 1e67d0ca9f..30bd45364d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,23 +8,34 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np + import os import pickle -import pytest -import shutil import urllib.request import zipfile -from deeplabcut.pose_estimation_tensorflow.lib import inferenceutils from io import BytesIO + +import numpy as np +import pytest from PIL import Image from tqdm import tqdm +from deeplabcut.core import inferenceutils -TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") +TESTS_DIR = os.path.dirname(os.path.realpath(__file__)) +TEST_DATA_DIR = os.path.join(TESTS_DIR, "data") +REQUIRED_TEST_FILES = [ + os.path.join(TEST_DATA_DIR, "dets.pickle"), + os.path.join(TEST_DATA_DIR, "outputs.pickle"), + os.path.join(TEST_DATA_DIR, "image.png"), + os.path.join(TEST_DATA_DIR, "trimouse_assemblies.pickle"), + os.path.join(TEST_DATA_DIR, "montblanc_tracks.h5"), + os.path.join(TEST_DATA_DIR, "trimouse_calib.h5"), +] -def unzip_from_url(url, dest_folder): + +def unzip_from_url(url: str, dest_folder: str) -> None: """Directly extract files without writing the archive to disk.""" os.makedirs(dest_folder, exist_ok=True) resp = urllib.request.urlopen(url) @@ -36,16 +47,23 @@ def unzip_from_url(url, dest_folder): pass -def pytest_sessionstart(session): - unzip_from_url( - "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", - os.path.split(TEST_DATA_DIR)[0], - ) - session.__DATA_FOLDER = TEST_DATA_DIR +def _test_data_ready() -> bool: + return all(os.path.exists(path) for path in REQUIRED_TEST_FILES) + +@pytest.fixture(scope="session", autouse=True) +def ensure_test_data(): + """Ensure shared test data exists once per pytest session. -def pytest_sessionfinish(session, exitstatus): - shutil.rmtree(session.__DATA_FOLDER) + This is autouse so tests that directly open files under tests/data/ + keep working without being rewritten. + """ + if not _test_data_ready(): + unzip_from_url( + "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", + TESTS_DIR, + ) + yield @pytest.fixture(scope="function") diff --git a/tests/core/debug/test_debug_logger.py b/tests/core/debug/test_debug_logger.py new file mode 100644 index 0000000000..c00a4566b6 --- /dev/null +++ b/tests/core/debug/test_debug_logger.py @@ -0,0 +1,483 @@ +from __future__ import annotations + +import logging +from uuid import uuid4 + +import pytest + +import deeplabcut.core.debug.debug_logger as debug_mod +from deeplabcut.core.debug import ( + DebugSection, + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + build_debug_report, + collect_executable_summary, + collect_version_summary, + format_debug_report, + get_debug_recorder, + install_debug_recorder, + log_timing, +) + + +@pytest.fixture +def logger_name() -> str: + return f"deeplabcut.tests.debug.{uuid4()}" + + +@pytest.fixture +def clean_logger(logger_name: str): + """Create an isolated logger namespace and fully clean it afterwards.""" + logger = logging.getLogger(logger_name) + old_level = logger.level + old_propagate = logger.propagate + old_handlers = list(logger.handlers) + + logger.setLevel(logging.DEBUG) + logger.propagate = False + + yield logger + + for handler in list(logger.handlers): + logger.removeHandler(handler) + try: + handler.close() + except Exception: + pass + + for handler in old_handlers: + logger.addHandler(handler) + + logger.setLevel(old_level) + logger.propagate = old_propagate + + # Remove recorder marker installed by install_debug_recorder(). + logger.__dict__.pop("_dlc_debug_recorder", None) + + +def test_install_debug_recorder_is_idempotent(logger_name: str, clean_logger): + recorder1 = install_debug_recorder(logger_name=logger_name, capacity=10) + recorder2 = install_debug_recorder(logger_name=logger_name, capacity=99) + + assert recorder1 is recorder2 + assert isinstance(recorder1, InMemoryDebugRecorder) + assert get_debug_recorder(logger_name=logger_name) is recorder1 + + +def test_recorder_captures_messages_and_exceptions(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=10, handler_level=logging.DEBUG) + + logger.info("hello %s", "dlc") + try: + raise ValueError("boom") + except ValueError: + logger.exception("something failed") + + records = recorder.snapshot() + + assert len(records) == 2 + assert records[0].message == "hello dlc" + assert records[0].level == "INFO" + assert records[1].message == "something failed" + assert records[1].level == "ERROR" + assert records[1].exc_text is not None + assert "ValueError: boom" in records[1].exc_text + + +def test_recorder_is_bounded(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=2, handler_level=logging.DEBUG) + + logger.debug("first") + logger.debug("second") + logger.debug("third") + + messages = [rec.message for rec in recorder.snapshot()] + assert messages == ["second", "third"] + + +def test_render_text_contains_recent_messages(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=5) + + logger.warning("alpha") + logger.error("beta") + + text = recorder.render_text(limit=10) + + assert "WARNING" in text + assert "ERROR" in text + assert "alpha" in text + assert "beta" in text + assert logger_name in text + + +def test_clear_resets_records_and_drop_count(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=5) + + logger.info("before clear") + assert recorder.snapshot() + + recorder.clear() + + assert recorder.snapshot() == [] + assert recorder.dropped_count == 0 + assert recorder.render_text() == "" + + +def test_log_timing_emits_when_enabled( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", True) + + ticks = iter([1_000_000_000, 1_005_000_000]) # 5.000 ms + monkeypatch.setitem(wrapped.__globals__, "perf_counter_ns", lambda: next(ticks)) + + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "tiny-step", threshold_ms=0.0): + pass + + assert calls == [ + (logging.DEBUG, "%s took %.3f ms", ("tiny-step", 5.0)), + ] + + +def test_log_timing_is_silent_when_disabled( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", False) + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "should-not-appear", threshold_ms=0.0): + pass + + assert calls == [] + + +def test_log_timing_respects_threshold( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", True) + + ticks = iter([1_000_000_000, 1_001_000_000]) # 1.000 ms + monkeypatch.setitem(wrapped.__globals__, "perf_counter_ns", lambda: next(ticks)) + + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "tiny-step", threshold_ms=2.0): + pass + + assert calls == [] + + +# ----------- Report building tests ----------- +def test_build_debug_report_includes_runtime_libraries_tools_and_recent_logs( + monkeypatch: pytest.MonkeyPatch, +): + recorder = InMemoryDebugRecorder(capacity=10, level=logging.DEBUG) + + record = logging.LogRecord( + name="deeplabcut.tests.debug", + level=logging.INFO, + pathname=__file__, + lineno=123, + msg="hello %s", + args=("report",), + exc_info=None, + ) + recorder.handle(record) + + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.11.9", + "platform": "TestOS-1.0", + "executable": "bin/python", + }, + ) + + monkeypatch.setattr( + debug_mod, + "_version", + lambda dist_name: { + "alpha": "1.2.3", + "opencv-python": "9.9.9-dist", + }.get(dist_name, "not-installed"), + ) + + monkeypatch.setattr( + debug_mod, + "_module_version", + lambda module_name: { + "cv2": "4.10.0", + }.get(module_name, "not-installed"), + ) + + monkeypatch.setattr( + debug_mod, + "_module_path", + lambda module_name: { + "alpha": "/tmp/site-packages/alpha/__init__.py", + "cv2": "/tmp/site-packages/cv2/__init__.py", + }.get(module_name, "unknown"), + ) + + monkeypatch.setattr( + debug_mod, + "_command_version", + lambda command, version_args: { + "ffmpeg": "ffmpeg 6.1", + }.get(command, "unavailable"), + ) + + monkeypatch.setattr( + debug_mod, + "_which", + lambda command: { + "ffmpeg": "/usr/bin/ffmpeg", + }.get(command, "not-found"), + ) + + report = build_debug_report( + recorder=recorder, + libraries=( + LibrarySpec("alpha"), + LibrarySpec( + "opencv-python", + dist_name="opencv-python", + module_name="cv2", + prefer_module_version=True, + ), + ), + executables=(ExecutableSpec("ffmpeg"),), + include_module_paths=True, + include_executable_paths=True, + log_limit=20, + ) + + assert "## Runtime" in report + assert "- python: 3.11.9" in report + assert "- platform: TestOS-1.0" in report + assert "- executable: bin/python" in report + + assert "## Libraries" in report + assert "- alpha: 1.2.3" in report + assert "- opencv-python: 4.10.0" in report + assert "- alpha_module_path: alpha/__init__.py" in report + assert "- opencv-python_module_path: cv2/__init__.py" in report + + assert "## External tools" in report + assert "- ffmpeg: ffmpeg 6.1" in report + assert "- ffmpeg_path: bin/ffmpeg" in report + + assert "## Recent logs" in report + assert "deeplabcut.tests.debug" in report + assert "INFO" in report + assert "hello report" in report + assert "```text" in report + + +def test_build_debug_report_default_grouped_sections_and_skips_unavailable_tf( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.12.0", + "platform": "GroupedTestOS", + "executable": "python", + }, + ) + + def fake_collect_version_summary(*, libraries=None, include_module_paths=False): + if libraries == debug_mod.DLC_CORE_LIBS: + return {"deeplabcut": "1.0.0", "numpy": "2.0.0"} + if libraries == debug_mod.DLC_GUI_LIBS: + return {"PySide6": "6.8.0"} + if libraries == debug_mod.DLC_TF_LIBS: + return { + "tensorflow": "not-installed", + "tf_keras": "not-installed", + "tensorpack": "unknown", + "tf_slim": "not-installed", + } + raise AssertionError("unexpected libraries input") + + monkeypatch.setattr(debug_mod, "collect_version_summary", fake_collect_version_summary) + + monkeypatch.setattr( + debug_mod, + "collect_executable_summary", + lambda *, executables=None, include_paths=True: { + "ffmpeg": "unavailable", + "ffmpeg_path": "not-found", + }, + ) + + report = build_debug_report( + recorder=None, + libraries=None, + executables=None, + ) + + assert "## Runtime" in report + assert "## DeepLabCut core libraries" in report + assert "- deeplabcut: 1.0.0" in report + assert "## GUI libraries" in report + assert "- PySide6: 6.8.0" in report + + # All TF values are unavailable/unknown, so the section should be omitted. + assert "## TensorFlow libraries" not in report + + # External tools should still be shown even when unavailable. + assert "## External tools" in report + assert "- ffmpeg: unavailable" in report + assert "- ffmpeg_path: not-found" in report + + assert "## Recent logs" in report + assert "" in report + + +def test_collect_version_summary_prefers_module_version_and_falls_back_to_distribution( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr( + debug_mod, + "_module_version", + lambda module_name: { + "cv2": "not-installed", + }.get(module_name, "unknown"), + ) + + monkeypatch.setattr( + debug_mod, + "_version", + lambda dist_name: { + "opencv-python": "4.9.0.80", + "missing-lib": "not-installed", + }.get(dist_name, "not-installed"), + ) + + summary = collect_version_summary( + libraries=( + LibrarySpec( + "opencv-python", + dist_name="opencv-python", + module_name="cv2", + prefer_module_version=True, + ), + LibrarySpec("missing-lib"), + ) + ) + + assert summary["opencv-python"] == "4.9.0.80" + assert summary["missing-lib"] == "not-installed" + + +def test_collect_executable_summary_reports_unavailable_tool_and_path( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr(debug_mod, "_command_version", lambda command, version_args: "unavailable") + monkeypatch.setattr(debug_mod, "_which", lambda command: "not-found") + + summary = collect_executable_summary( + executables=(ExecutableSpec("ghosttool"),), + include_paths=True, + ) + + assert summary == { + "ghosttool": "unavailable", + "ghosttool_path": "not-found", + } + + +def test_build_debug_report_uses_no_captured_logs_placeholder_for_empty_recorder( + monkeypatch: pytest.MonkeyPatch, +): + recorder = InMemoryDebugRecorder(capacity=5, level=logging.DEBUG) + + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.11.0", + "platform": "EmptyLogsOS", + "executable": "python", + }, + ) + + monkeypatch.setattr( + debug_mod, + "collect_executable_summary", + lambda *, executables=None, include_paths=True: {}, + ) + + report = build_debug_report( + recorder=recorder, + libraries=(), + executables=(), + ) + + assert "## Runtime" in report + assert "## Libraries" in report + assert "- " in report + assert "## Recent logs" in report + assert "" in report + + +def test_format_debug_report_renders_empty_section_and_logs_block(): + text = format_debug_report( + sections=[ + DebugSection(title="Example", items={}), + ], + logs_text="line one\nline two", + ) + + assert "## Example" in text + assert "- " in text + assert "## Recent logs" in text + assert "```text" in text + assert "line one" in text + assert "line two" in text diff --git a/tests/core/inferenceutils/test_map_computation.py b/tests/core/inferenceutils/test_map_computation.py new file mode 100644 index 0000000000..c2cd4fffe9 --- /dev/null +++ b/tests/core/inferenceutils/test_map_computation.py @@ -0,0 +1,418 @@ +"""Tests mAP computation from inferenceutils.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from deeplabcut.core import inferenceutils +from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + ], + }, + ], +) +@pytest.mark.parametrize( + "predictions", + [ + { + "img0": [ + [ + [100.0, 10.0, 0.9], + [150.0, 15.0, 0.7], + [202.0, 20.0, 0.8], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [140.0, 17.0, 0.7], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [np.nan, np.nan, 0.0], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + ], +) +def test_map_single_image_simple(ground_truth: dict, predictions: dict): + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {k: np.array(v) for k, v in predictions.items()} + _evaluate(gt, pred) + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [326.0, 236.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0.0, 0.0, 0], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0, 0, 0], + [457.0, 832.0, 2], + ], + [ + [452.0, 321.0, 2], + [213.0, 387.0, 2], + [213.0, 832.0, 2], + ], + [ + [253.0, 238.0, 2], + [213.0, 238.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + ], +) +def test_map_single_image_random_errors(ground_truth: dict): + rng = np.random.default_rng(seed=0) + + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {} + for k, gt_kpts in gt.items(): + num_idv, num_bpt = gt_kpts.shape[:2] + + error = rng.integers(low=-30, high=30, size=(num_idv, num_bpt, 2)) + scores = rng.random(size=(num_idv, num_bpt)) + + pred[k] = np.zeros(shape=(num_idv, num_bpt, 3)) + pred[k][..., :2] = np.clip(gt_kpts[..., :2] + error, 0, 1024) + pred[k][..., 2] = scores + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + max_idv = num_individuals.max(initial=0) + + gt = {} + pred = {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + # padding needed as we then stack + gt_kpts = np.zeros((max_idv, num_joints, 3)) + pred_kpts = -np.ones((max_idv, num_joints, 3)) + + gt_kpts[:gt_idv] = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[:gt_idv, :, :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # set scores + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # predictions that are ground truth + error + matched = min(gt_idv, pred_idv) + if matched > 0: + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) + matched_pred = gt_kpts[:matched, :, :2] + error + pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) + + # random predictions + unmatched = pred_idv - matched + if unmatched > 0: + pred_kpts[matched:pred_idv, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) + + pred[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + max_idv = num_individuals.max(initial=0) + + gt = {} + pred = {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + # padding needed as we then stack + gt_kpts = np.zeros((max_idv, num_joints, 3)) + pred_kpts = -np.ones((max_idv, num_joints, 3)) + + gt_kpts[:gt_idv] = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[:gt_idv, :, :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # drop some ground truth keypoints + gt_vis_mask = rng.random(size=(max_idv, num_joints)) < 0.2 + gt_kpts[gt_vis_mask, 2] = 0 + + # set scores + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # predictions that are ground truth + error + matched = min(gt_idv, pred_idv) + if matched > 0: + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) + matched_pred = gt_kpts[:matched, :, :2] + error + pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) + + # random predictions + unmatched = pred_idv - matched + if unmatched > 0: + pred_kpts[matched:pred_idv, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) + + pred[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): + for k, v in gt.items(): + print(20 * "-") + print(k) + print("GT") + print(v) + print("PR") + print(pred[k]) + + gt_assemblies = _to_assemblies(gt, ground_truth=True) + pred_assemblies = _to_assemblies(pred, ground_truth=False) + oks = inferenceutils.evaluate_assembly_greedy( + assemblies_gt=gt_assemblies, + assemblies_pred=pred_assemblies, + oks_sigma=0.1, + oks_thresholds=np.linspace(0.5, 0.95, 10), + margin=0.0, + symmetric_kpts=None, + ) + + num_joints = gt[list(gt.keys())[0]].shape[1] + coco_gt = _to_coco_ground_truth(gt, num_joints, bbox_margin=0) + coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) + coco_oks = eval_coco(coco_gt, coco_pred, num_joints) + print(20 * "-") + print("dlc mAP:") + for k, v in oks.items(): + print(k) + print(v) + print() + print(20 * "-") + print(f"pycocotools mAP: {coco_oks}") + print() + assert oks["mAP"] == coco_oks + + +def _to_assemblies( + data: dict[str, np.ndarray], + ground_truth: bool, +) -> dict[str, list[inferenceutils.Assembly]]: + images = list(data.keys()) + raw_data = np.stack([data[i] for i in images], axis=0) + + # mask not visible entries + mask = raw_data[..., 2] <= 0 + raw_data[mask] = np.nan + + # set the "score" to 1 for ground truth + if ground_truth: + raw_data[~mask, 2] = 1 + + return {images[i]: assembly for i, assembly in inferenceutils._parse_ground_truth_data(raw_data).items()} + + +def _to_coco_ground_truth( + data: dict[str, np.ndarray], + num_joints: int, + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> dict[str, list[dict]]: + w, h = image_size + anns, images = [], [] + for path, image_keypoints in data.items(): + id_ = len(images) + 1 + images.append(dict(id=id_, file_name=path, width=w, height=h)) + + assert image_keypoints.shape[1] == num_joints + for _idv_id, kpts in enumerate(image_keypoints): + visible = kpts[:, 2] > 0 + num_keypoints = visible.sum() + + if num_keypoints > 1: + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + anns.append( + { + "id": len(anns) + 1, + "image_id": id_, + "category_id": 1, + "area": area, + "bbox": bbox.tolist(), + "keypoints": kpts.reshape(-1).tolist(), + "iscrowd": 0, + "num_keypoints": num_keypoints, + } + ) + + keypoints = [f"bpt{i}" for i in range(num_joints)] + category = dict(id=1, name="animal", supercategory="animal", keypoints=keypoints) + return {"annotations": anns, "categories": [category], "images": images} + + +def _to_coco_predictions( + ground_truth: dict, + predictions: dict[str, np.ndarray], + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> list[dict]: + w, h = image_size + num_joints = len(ground_truth["categories"][0]["keypoints"]) + path_to_id = {img["file_name"]: img["id"] for img in ground_truth["images"]} + + coco_predictions = [] + for path, image_keypoints in predictions.items(): + assert image_keypoints.shape[1] == num_joints + + img_id = path_to_id[path] + valid_predictions = [kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1))] + for kpts in valid_predictions: + score = float(np.nanmean(kpts[:, 2]).item()) + kpts = kpts.copy() + kpts[:, 2] = 2 + + # NaN predictions to infinity + kpts[np.isnan(kpts)] = np.inf + + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + coco_predictions.append( + { + "image_id": img_id, + "category_id": 1, + "keypoints": kpts.reshape(-1).tolist(), + "bbox": bbox.tolist(), + "area": area, + "score": score, + } + ) + + return coco_predictions + + +def eval_coco( + ground_truth: dict, + predictions: list[dict], + num_joints: int, +) -> float | None: + try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + coco = COCO() + coco.dataset["annotations"] = ground_truth["annotations"] + coco.dataset["categories"] = ground_truth["categories"] + coco.dataset["images"] = ground_truth["images"] + coco.dataset["info"] = {"description": "Generated by DeepLabCut"} + coco.createIndex() + + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="keypoints") + coco_eval.params.kpt_oks_sigmas = np.array(num_joints * [0.1]) + coco_eval.evaluate() + coco_eval.accumulate() + coco_eval.summarize() + return float(coco_eval.stats[0]) + + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/core/metrics/test_metrics_api.py b/tests/core/metrics/test_metrics_api.py new file mode 100644 index 0000000000..a38516ab58 --- /dev/null +++ b/tests/core/metrics/test_metrics_api.py @@ -0,0 +1,110 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""General tests for the metrics API.""" + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +import deeplabcut.core.metrics as metrics + + +def _get_gt_and_pred_with_constant_err(num_idv: int, num_bpt: int, error: float) -> tuple[np.ndarray, np.ndarray]: + gt = np.arange(num_idv * num_bpt * 3).astype(float).reshape((num_idv, num_bpt, 3)) + gt[..., 2] = 2 + predictions = gt.copy() + predictions[..., 2] = 0.9 + predictions[..., :2] += error + return gt, predictions + + +def test_computing_metrics_with_no_predictions(): + gt = np.arange(5 * 6 * 3).astype(float).reshape((5, 6, 3)) + gt[..., 2] = 2 + metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": np.zeros((0, 12, 3))}, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_computing_metrics_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_metrics_with_unique_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + gt_unique, pred_unique = _get_gt_and_pred_with_constant_err(1, 8, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt={"image": gt_unique}, + unique_bodypart_poses={"image": pred_unique}, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_metrics_per_bpt_with_unique_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + gt_unique, pred_unique = _get_gt_and_pred_with_constant_err(1, 8, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt={"image": gt_unique}, + unique_bodypart_poses={"image": pred_unique}, + per_keypoint_rmse=True, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + for bpt_idx in range(gt.shape[1]): + key = f"rmse_keypoint_{bpt_idx}" + assert key in results + assert_almost_equal(results[key], np.sqrt(2) * error) + for bpt_idx in range(gt_unique.shape[1]): + key = f"rmse_unique_keypoint_{bpt_idx}" + assert key in results + assert_almost_equal(results[key], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_computing_metrics_single_animal(error): + # only works for small errors: otherwise another matching can be found + gt = np.arange(6 * 3).astype(float).reshape((1, 6, 3)) + gt[..., 2] = 2 + predictions = gt.copy() + predictions[..., 2] = 0.9 + predictions[..., :2] += error + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + single_animal=True, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) diff --git a/tests/core/metrics/test_metrics_identity_accuracy.py b/tests/core/metrics/test_metrics_identity_accuracy.py new file mode 100644 index 0000000000..017653d59e --- /dev/null +++ b/tests/core/metrics/test_metrics_identity_accuracy.py @@ -0,0 +1,219 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for the scoring methods.""" + +import numpy as np +import pytest + +import deeplabcut.core.metrics.identity + + +@pytest.mark.parametrize( + "data", + [ + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[2.0, 2.0, 0.8]], + [[1.0, 1.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.8, 0.5]], + [[0.51, 0.49]], + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[1.0, 1.0, 2]], + [[0, 0, 0]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.4, 0.6]], + [[0.6, 0.4]], + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], + [[1.0, 1.0, 2]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.6, 0.4]], + [[0.6, 0.4]], # both assemblies assigned to idv 1 + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], + [[1.0, 1.0, 2]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 0.5, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.6, 0.4]], + [[0.4, 0.6]], # both assigned to wrong ID + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], # x, y, visibility + [[1.0, 1.0, 2]], + ] + }, + "accuracy": { + "arm_accuracy": 0.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3], [0.6, 0.2]], + [[0.6, 0.3], [0.6, 0.2]], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2", "i3"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + [[110.0, 110.0, 0.9], [98.0, 91.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3, 0.0], [0.6, 0.2, 0.2]], # assigned to correct ID + [[0.6, 0.3, 0.1], [0.6, 0.2, 0.2]], # should not matter, not assigned to GT + [[0.6, 0.3, 0.1], [0.6, 0.2, 0.2]], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2", "i3"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + [[110.0, 110.0, 0.9], [98.0, 91.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3, 0.1], [0.6, 0.2, 0.1]], # assigned to correct ID + [[0.1, 0.2, 0.7], [0.4, 0.3, 0.2]], # 1st correct, 2nd wrong + [ + [0.6, 0.3, 0.5], + [0.6, 0.2, 0.4], + ], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + [[90.0, 90, 2], [80, 80, 2.0]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 0.5, + }, + }, + ], +) +def test_id_accuracy(data) -> None: + scores = deeplabcut.core.metrics.identity.compute_identity_scores( + individuals=data["individuals"], + bodyparts=data["bodyparts"], + predictions={k: np.array(v) for k, v in data["predictions"].items()}, + identity_scores={k: np.array(v) for k, v in data["identity_scores"].items()}, + ground_truth={k: np.array(v) for k, v in data["ground_truth"].items()}, + ) + assert scores == data["accuracy"] diff --git a/tests/core/metrics/test_metrics_map_computation.py b/tests/core/metrics/test_metrics_map_computation.py new file mode 100644 index 0000000000..5ef6a64f75 --- /dev/null +++ b/tests/core/metrics/test_metrics_map_computation.py @@ -0,0 +1,399 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests that mAP computation is correct.""" + +from __future__ import annotations + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +from deeplabcut.core.metrics.api import prepare_evaluation_data +from deeplabcut.core.metrics.distance_metrics import compute_oks +from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + ], + }, + ], +) +@pytest.mark.parametrize( + "predictions", + [ + { + "img0": [ + [ + [100.0, 10.0, 0.9], + [150.0, 15.0, 0.7], + [202.0, 20.0, 0.8], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [140.0, 17.0, 0.7], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [np.nan, np.nan, 0.0], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + ], +) +def test_map_single_image_simple(ground_truth: dict, predictions: dict): + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {k: np.array(v) for k, v in predictions.items()} + _evaluate(gt, pred) + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [326.0, 236.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0.0, 0.0, 0], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0, 0, 0], + [457.0, 832.0, 2], + ], + [ + [452.0, 321.0, 2], + [213.0, 387.0, 2], + [213.0, 832.0, 2], + ], + [ + [253.0, 238.0, 2], + [213.0, 238.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + ], +) +def test_map_single_image_random_errors(ground_truth: dict): + rng = np.random.default_rng(seed=0) + + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {} + for k, gt_kpts in gt.items(): + num_idv, num_bpt = gt_kpts.shape[:2] + + error = rng.integers(low=-30, high=30, size=(num_idv, num_bpt, 2)) + scores = rng.random(size=(num_idv, num_bpt)) + + pred[k] = np.zeros(shape=(num_idv, num_bpt, 3)) + pred[k][..., :2] = np.clip(gt_kpts[..., :2] + error, 0, 1024) + pred[k][..., 2] = scores + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + + gt, pred = {}, {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + gt_kpts = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[..., :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # create predictions array + pred_kpts = np.zeros((pred_idv, num_joints, 3)) + # set scores + pred_kpts[..., 2] = rng.random(size=(pred_idv, num_joints)) + + # predictions that are ground truth + error + matched = min(gt_idv, pred_idv) + if matched > 0: + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) + matched_pred = gt_kpts[:matched, :, :2] + error + pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) + + # random predictions + unmatched = pred_idv - matched + if unmatched > 0: + pred_kpts[matched:, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) + + pred[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + + gt, pred = {}, {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + gt_kpts = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[..., :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # drop some ground truth keypoints + gt_vis_mask = rng.random(size=(gt_idv, num_joints)) < 0.2 + gt_kpts[gt_vis_mask, 2] = 0 + + # generate predicted keypoints + pred_kpts = np.zeros((pred_idv, num_joints, 3)) + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # predictions that are ground truth + error + matched = min(gt_idv, pred_idv) + if matched > 0: + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) + matched_pred = gt_kpts[:matched, :, :2] + error + pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) + + # random predictions + unmatched = pred_idv - matched + if unmatched > 0: + pred_kpts[matched:, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) + + pred[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): + for k, v in gt.items(): + print(20 * "-") + print(k) + print("GT") + print(v) + print("PR") + print(pred[k]) + + data = prepare_evaluation_data(gt, pred) + oks = compute_oks(data, oks_bbox_margin=0) + + num_joints = gt[list(gt.keys())[0]].shape[1] + coco_gt = _to_coco_ground_truth(gt, num_joints, bbox_margin=0) + coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) + coco_oks = eval_coco(coco_gt, coco_pred, num_joints) + print(20 * "-") + print("dlc mAP:") + for k, v in oks.items(): + print(k) + print(v) + print(20 * "-") + print(f"pycocotools mAP: {coco_oks}") + print() + dlc_map = oks["mAP"] / 100 + assert_almost_equal(dlc_map, coco_oks) + + +def _to_coco_ground_truth( + data: dict[str, np.ndarray], + num_joints: int, + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> dict[str, list[dict]]: + w, h = image_size + anns, images = [], [] + for path, image_keypoints in data.items(): + id_ = len(images) + 1 + images.append(dict(id=id_, file_name=path, width=w, height=h)) + + assert image_keypoints.shape[1] == num_joints + for _idv_id, kpts in enumerate(image_keypoints): + visible = kpts[:, 2] > 0 + num_keypoints = visible.sum() + + if num_keypoints > 1: + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + anns.append( + { + "id": len(anns) + 1, + "image_id": id_, + "category_id": 1, + "area": area, + "bbox": bbox.tolist(), + "keypoints": kpts.reshape(-1).tolist(), + "iscrowd": 0, + "num_keypoints": num_keypoints, + } + ) + + keypoints = [f"bpt{i}" for i in range(num_joints)] + category = dict(id=1, name="animal", supercategory="animal", keypoints=keypoints) + return { + "info": {"description": "Generated COCO ground truth dataset"}, # Add this + "annotations": anns, + "categories": [category], + "images": images, + } + + +def _to_coco_predictions( + ground_truth: dict, + predictions: dict[str, np.ndarray], + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> list[dict]: + w, h = image_size + num_joints = len(ground_truth["categories"][0]["keypoints"]) + path_to_id = {img["file_name"]: img["id"] for img in ground_truth["images"]} + + coco_predictions = [] + for path, image_keypoints in predictions.items(): + assert image_keypoints.shape[1] == num_joints + + img_id = path_to_id[path] + valid_predictions = [kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1))] + for kpts in valid_predictions: + score = float(np.nanmean(kpts[:, 2]).item()) + kpts = kpts.copy() + kpts[:, 2] = 2 + + # NaN predictions to infinity + kpts[np.isnan(kpts)] = np.inf + + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + coco_predictions.append( + { + "image_id": img_id, + "category_id": 1, + "keypoints": kpts.reshape(-1).tolist(), + "bbox": bbox.tolist(), + "area": area, + "score": score, + } + ) + + return coco_predictions + + +def eval_coco( + ground_truth: dict, + predictions: list[dict], + num_joints: int, +) -> float | None: + try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + coco = COCO() + coco.dataset["annotations"] = ground_truth["annotations"] + coco.dataset["categories"] = ground_truth["categories"] + coco.dataset["images"] = ground_truth["images"] + coco.dataset["info"] = {"description": "Generated by DeepLabCut"} + coco.createIndex() + + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="keypoints") + coco_eval.params.kpt_oks_sigmas = np.array(num_joints * [0.1]) + coco_eval.evaluate() + coco_eval.accumulate() + coco_eval.summarize() + return float(coco_eval.stats[0]) + + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/core/metrics/test_metrics_rmse_computation.py b/tests/core/metrics/test_metrics_rmse_computation.py new file mode 100644 index 0000000000..187279df31 --- /dev/null +++ b/tests/core/metrics/test_metrics_rmse_computation.py @@ -0,0 +1,374 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests RMSE computation.""" + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +from deeplabcut.core.metrics.distance_metrics import ( + compute_detection_rmse, + compute_rmse, +) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[100.0, 10.0, 2], [150.0, 15.0, 2], [200.0, 20.0, 2]], + ], + [ # predicted pose + [[100.0, 10.0, 0.9], [150.0, 15.0, 0.8], [200.0, 20.0, 0.8]], + ], + (0, 0), + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [12.0, 10.0, 0.9], [12.0, 10.0, 0.9]], + [[22.0, 20.0, 0.9], [22.0, 20.0, 0.9], [22.0, 20.0, 0.9]], + ], + (2, 2), + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 12.0, 0.9], [10.0, 12.0, 0.9], [10.0, 12.0, 0.9]], + [[20.0, 22.0, 0.9], [20.0, 22.0, 0.9], [20.0, 22.0, 0.9]], + ], + (2, 2), + ), + ], +) +def test_rmse_single_image(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + computed_results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = computed_results["rmse"], computed_results["rmse_pcutoff"] + expected_rmse, expected_rmse_cutoff = result + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 22.0, 0.2], [20.0, 22.0, 0.2], [20.0, 22.0, 0.2]], + ], + (1, 0), # 2 pixel error on half of keypoints, 0 on the other half + ), + ], +) +def test_rmse_pcutoff(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + expected_rmse, expected_rmse_cutoff = result + + computed_results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = computed_results["rmse"], computed_results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [float("nan"), float("nan"), 0], [10.0, 10.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + (1, 1), # only 2 valid ground truth bodyparts + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [float("nan"), float("nan"), 0]], + [[float("nan"), float("nan"), 0], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose, swapped prediction order + [[20.0, 20.0, 0.9], [21.0, 20.0, 0.9], [21.0, 20.0, 0.9]], + [[15.0, 10.0, 0.4], [15.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + (3, 1), # only 2 valid GT bodyparts + ), + ], +) +def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + expected_rmse, expected_rmse_cutoff = result + + results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = results["rmse"], results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, data_unique, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [np.nan, np.nan, 0], [10.0, 10.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + None, # unique data + (1, 1), # error 2 on one, 0 on the other; only 2 valid GT + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, perfect detections but misassembled + [[10.0, 10.0, 0.9], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], + [[40.0, 40.0, 0.9], [20.0, 20.0, 0.4], [60.0, 60.0, 0.9]], + ], + None, # unique data + (0, 0), # all pose perfect + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, small error in pose and misassembled + [[12.0, 10.0, 0.9], [52.0, 50.0, 0.9], [32.0, 30.0, 0.9]], + [[42.0, 40.0, 0.9], [18.0, 20.0, 0.4], [62.0, 60.0, 0.9]], + ], + None, # unique data + (2, 2), # pixel error of 2 on x-axis for all predictions + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, small error in low-conf pose and misassembled + [[12.0, 10.0, 0.4], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], + [[40.0, 40.0, 0.9], [22.0, 20.0, 0.4], [62.0, 60.0, 0.4]], + ], + None, # unique data + (1, 0), # error of 2 on half, 0 on the other half (with good conf) + ), + ( # more ground truth than detections + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + ], + [ # predicted pose, no error + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + None, # unique data + (0, 0), + ), + ( # more detections than GT + [ # ground truth pose + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, no error + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + ], + None, # unique data + (0, 0), + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [np.nan, np.nan, 0], [10.0, 10.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + ( # unique data + [[[20, 20, 2], [22, 23, 2]]], + [[[20, 20, 0.8], [22, 23, 0.7]]], + ), + (0.5, 0.5), # error 2 on one, 0 on the other; only 2 valid GT + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, perfect detections but misassembled + [[10.0, 10.0, 0.9], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], + [[40.0, 40.0, 0.9], [20.0, 20.0, 0.4], [60.0, 60.0, 0.9]], + ], + ( # unique data + [], # missing ground truth for unique bodyparts + [[[20, 20, 0.8], [22, 23, 0.7]]], + ), + (0, 0), # all pose perfect + ), + ], +) +def test_detection_rmse(gt: list, pred: list, data_unique: tuple[list, list] | None, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + data_unique = [(np.asarray(data_unique[0]), np.asarray(data_unique[1]))] if data_unique else None + expected_rmse, expected_rmse_cutoff = result + rmse, rmse_cutoff = compute_detection_rmse(data, pcutoff=0.6, data_unique=data_unique) + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, unique_gt, unique_pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.2], [20.0, 24.0, 0.2], [20.0, 20.0, 0.2]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + ], + # 4 pixel error on 2 keypoints, 0 error on 5 keypoints + (1.0, 0.0), + ), + ( + [np.zeros((0, 3, 2))], # no GT pose + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[15.0, 10.0, 0.5], [11.0, 10.0, 0.9]], + ], + # 5 pixel error on 1 keypoint, 1 pixel error on the other + (3.0, 1.0), + ), + ], +) +def test_rmse_with_unique( + gt: list, pred: list, unique_gt: list, unique_pred: list, result: tuple[float, float] +) -> None: + data = [(np.asarray(gt), np.asarray(pred))] + data_unique = [(np.asarray(unique_gt), np.asarray(unique_pred))] + expected_rmse, expected_rmse_cutoff = result + + results = compute_rmse( + data, + False, + pcutoff=0.6, + data_unique=data_unique, + oks_bbox_margin=10.0, + ) + rmse, rmse_cutoff = results["rmse"], results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, unique_gt, unique_pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.2], [20.0, 24.0, 0.2], [20.0, 20.0, 0.2]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + ], + # 4 pixel error on 2 keypoints, 0 error on 5 keypoints + [(1.0, 0.0), [2.0, 2.0, 0.0], [0.0, 0.0]], + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 12.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.7], [20.0, 24.0, 0.6], [20.0, 20.0, 0.8]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[12.0, 10.0, 0.9], [11.0, 10.0, 0.9]], + ], + [ # errors: 3 with 0px, 1 with 1px, 2 with 2px, 2 with 4px => 13/8 + (1.625, 1.625), + [3.0, 2.0, 0.0], + [2.0, 1.0], + ], + ), + ], +) +def test_rmse_per_bodypart_with_unique( + gt: list, + pred: list, + unique_gt: list, + unique_pred: list, + result: tuple[tuple[float, float], list[float], list[float]], +) -> None: + data = [(np.asarray(gt), np.asarray(pred))] + data_unique = [(np.asarray(unique_gt), np.asarray(unique_pred))] + expected_rmse, expected_rmse_cutoff = result[0] + bodypart_rmse = result[1] + unique_rmse = result[2] + + results = compute_rmse( + data, + single_animal=False, + pcutoff=0.6, + data_unique=data_unique, + per_keypoint_results=True, + oks_bbox_margin=10.0, + ) + assert_almost_equal(results["rmse"], expected_rmse) + assert_almost_equal(results["rmse_pcutoff"], expected_rmse_cutoff) + for bpt_index, bpt_rmse in enumerate(bodypart_rmse): + key = f"rmse_keypoint_{bpt_index}" + assert key in results + assert_almost_equal(results[key], bpt_rmse) + + for bpt_index, bpt_rmse in enumerate(unique_rmse): + key = f"rmse_unique_keypoint_{bpt_index}" + assert key in results + assert_almost_equal(results[key], bpt_rmse) diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py new file mode 100644 index 0000000000..86e50eecc9 --- /dev/null +++ b/tests/create_project/test_video_set_configuration.py @@ -0,0 +1,264 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Unit tests for deeplabcut.create_project.new module.""" + +import logging +import warnings +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +import deeplabcut.create_project.new as new_module +from deeplabcut.utils.auxfun_videos import VideoReader + + +@pytest.fixture +def project_directory(tmpdir_factory) -> Path: + proj_dir = Path(tmpdir_factory.mktemp("test-project")) + return proj_dir + + +@pytest.fixture +def mock_video_file(tmpdir_factory) -> Path: + """Create a mock video file for testing.""" + fake_folder = tmpdir_factory.mktemp("some_video") + video_path = Path(fake_folder) / "test_video.avi" + video_path.write_bytes(b"fake video content") + return video_path + + +@pytest.fixture +def mock_video_reader() -> VideoReader: + """Create a mock VideoReader.""" + mock_reader = Mock(spec=VideoReader) + mock_reader.get_bbox.return_value = (0, 640, 277, 624) + return mock_reader + + +@pytest.fixture +def video_directory(tmpdir_factory) -> Path: + """Create a directory with multiple video files.""" + video_dir = Path(tmpdir_factory.mktemp("some_videos")) + video_dir.mkdir(exist_ok=True) + + # Create multiple video files with different extensions + (video_dir / "video1.avi").write_bytes(b"fake video 1") + (video_dir / "video2.mp4").write_bytes(b"fake video 2") + (video_dir / "video3.mov").write_bytes(b"fake video 3") + (video_dir / "not_a_video.txt").write_text("text file") + + return video_dir + + +def test_project_directory_creation_basic( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, +): + """Test that project directories are created correctly.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test-project", + experimenter="test-user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + project_path = Path(config_path).parent + assert project_path.exists() + assert (project_path / "videos").exists() + assert (project_path / "labeled-data").exists() + assert (project_path / "training-datasets").exists() + assert (project_path / "dlc-models").exists() + + +@pytest.mark.parametrize("copy_videos", [True, False]) +def test_single_video_file( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """Test adding a single video file.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=copy_videos, + ) + + project_path = Path(config_path).parent + video_path = project_path / "videos" / "test_video.avi" + assert video_path.exists() or video_path.is_symlink() + + # Content should match + if copy_videos: + assert mock_video_file.read_bytes() == video_path.read_bytes() + + +@pytest.mark.parametrize("copy_videos", [True, False]) +def test_video_directory( + tmpdir: Path, + video_directory: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """Test adding videos from a directory.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(video_directory)], + working_directory=str(tmpdir), + video_extensions=".avi", + copy_videos=copy_videos, + ) + + project_path = Path(config_path).parent + assert (project_path / "videos" / "video1.avi").exists() or (project_path / "videos" / "video1.avi").is_symlink() + + # Content should match + if copy_videos: + assert (project_path / "videos" / "video1.avi").read_bytes() == (video_directory / "video1.avi").read_bytes() + + +@pytest.mark.parametrize("copy_videos", [True, False]) +def test_mixed_video_files_and_directories( + tmpdir, + mock_video_file: Path, + video_directory: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """Test adding both video files and directories.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file), str(video_directory)], + working_directory=str(tmpdir), + video_extensions=".avi", + copy_videos=copy_videos, + ) + + project_path = Path(config_path).parent + videos_dir = project_path / "videos" + # Should have both the single file and files from directory + assert (videos_dir / mock_video_file.name).exists() or (videos_dir / mock_video_file.name).is_symlink() + assert (videos_dir / "video1.avi").exists() or (videos_dir / "video1.avi").is_symlink() + + +def test_empty_video_directory( + tmpdir: Path, + mock_video_reader: VideoReader, +): + """Test handling of empty video directory.""" + empty_dir = tmpdir / "empty_videos" + empty_dir.mkdir() + + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + with warnings.catch_warnings(record=True) as w: + result = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(empty_dir)], + working_directory=str(tmpdir), + video_extensions=".avi", + copy_videos=False, + ) + # Should return "nothingcreated" when no valid videos found + assert result == "nothingcreated" or len(w) > 0 + + +def test_valid_video_included_in_config( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, +): + """Test that valid videos are included in the config file.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + from deeplabcut.utils import auxiliaryfunctions + + cfg = auxiliaryfunctions.read_config(config_path) + logging.debug(f"Config content: {cfg}") + logging.debug(f"Video sets in config: {cfg.get('video_sets', {})}") + logging.debug(f"Video sets keys: {list(cfg.get('video_sets', {}).keys())}") + + assert "video_sets" in cfg + assert len(cfg["video_sets"]) > 0 + # Check that video path is in video_sets + video_keys = [Path(k) for k in cfg["video_sets"].keys()] + project_video = Path(config_path).parent / "videos" / mock_video_file.name + + assert any(k.resolve() == project_video.resolve() for k in video_keys) + + +def test_invalid_video_removed_from_project( + tmpdir: Path, + mock_video_file: Path, +): + """Test that invalid videos are removed from the project.""" + # Mock VideoReader to raise IOError + mock_reader = Mock(side_effect=OSError("Cannot open video")) + + with patch("deeplabcut.create_project.new.VideoReader", mock_reader): + with warnings.catch_warnings(record=True): + result = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + # Should return "nothingcreated" when no valid videos + assert result == "nothingcreated" + + +def test_config_file_video_sets_format( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, +): + """Test that video_sets in config has correct format.""" + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): + config_path = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + from deeplabcut.utils import auxiliaryfunctions + + cfg = auxiliaryfunctions.read_config(config_path) + + assert "video_sets" in cfg + assert isinstance(cfg["video_sets"], dict) + + # Check format of video_sets entries + for _video_path, video_info in cfg["video_sets"].items(): + assert isinstance(video_info, dict) + assert "crop" in video_info + assert isinstance(video_info["crop"], str) diff --git a/tests/generate_training_dataset/test_trainingset_manipulation.py b/tests/generate_training_dataset/test_trainingset_manipulation.py new file mode 100644 index 0000000000..cf78ba3996 --- /dev/null +++ b/tests/generate_training_dataset/test_trainingset_manipulation.py @@ -0,0 +1,37 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" + +from __future__ import annotations + +import pytest + +import deeplabcut.generate_training_dataset.trainingsetmanipulation as trainingsetmanipulation + + +@pytest.mark.parametrize("train_fraction", [1, 2, 5, 17, 24, 29, 34, 47, 50, 53, 61, 68, 75, 90, 95, 97, 99]) +@pytest.mark.parametrize("n_train", [1, 2, 3, 5, 7, 11, 37, 62, 153]) +@pytest.mark.parametrize("n_test", [1, 2, 3, 5, 7, 13, 19, 85, 112]) +def test_compute_padding(train_fraction: int, n_train: int, n_test: int) -> None: + """ + More complete tests can be run with: + "train_fraction": list(range(1, 100)) + "n_train": list(range(1, 200)) + "n_test": list(range(1, 200)) + + This was done locally, but as it's many many tests to run a subset was selected here + """ + train_frac = train_fraction / 100 + train_pad, test_pad = trainingsetmanipulation._compute_padding(train_frac, n_train, n_test) + print() + print(train_fraction, n_train, n_test, train_pad, test_pad) + frac = round((n_train + train_pad) / (n_train + n_test + train_pad + test_pad), 2) + assert train_frac == frac diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py new file mode 100644 index 0000000000..dacd9978cf --- /dev/null +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -0,0 +1,580 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" + +from __future__ import annotations + +import logging +import pickle +from unittest.mock import MagicMock, patch + +import pytest +from ruamel.yaml import YAML + +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + +SHUFFLE_DATA = [ + {"name": "pJun17-t50s1", "index": 1, "train_fraction": 0.5, "split": 1, "engine": "torch"}, + {"name": "pJun17-t50s2", "index": 2, "train_fraction": 0.5, "split": 1, "engine": "tf"}, + {"name": "pJun17-t60s1", "index": 1, "train_fraction": 0.6, "split": 2, "engine": "torch"}, + {"name": "pJun17-t60s2", "index": 2, "train_fraction": 0.6, "split": 3, "engine": "torch"}, +] +SPLITS_DATA = { + 1: {"train": [0, 1], "test": [2, 3]}, + 2: {"train": [0, 1, 2], "test": [3, 4]}, + 3: {"train": [4, 3, 2], "test": [1, 0]}, +} + +BASE_SPLIT = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4)) +# Splits that should be equal to the base +EQ_SPLIT = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4)) +# Splits that should not be equal to the base +ADD_SPLIT = metadata.DataSplit(train_indices=(1, 2, 5), test_indices=(3, 4)) +ADD_SPLIT2 = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4, 5)) +SUBS_SPLIT = metadata.DataSplit(train_indices=(1, 3), test_indices=(2, 4)) +DEL_SPLIT = metadata.DataSplit(train_indices=(1,), test_indices=(3, 4)) +DEL_SPLIT2 = metadata.DataSplit(train_indices=(1, 2), test_indices=(3,)) + +SHUFFLES = { + 1: metadata.ShuffleMetadata("pJun17-t50s1", 0.5, 1, Engine.PYTORCH, BASE_SPLIT), + 2: metadata.ShuffleMetadata("pJun17-t50s2", 0.5, 2, Engine.PYTORCH, ADD_SPLIT), + 3: metadata.ShuffleMetadata("pJun17-t50s3", 0.5, 3, Engine.TF, BASE_SPLIT), + 4: metadata.ShuffleMetadata("pJun17-t50s4", 0.5, 4, Engine.PYTORCH, DEL_SPLIT), +} + + +@pytest.mark.parametrize( + "data", + [ + { + "shuffles": {SHUFFLE_DATA[idx]["name"]: SHUFFLE_DATA[idx] for idx in [0, 1, 2]}, + "splits": {idx: SPLITS_DATA[idx] for idx in [1, 2]}, + }, + { + "shuffles": {SHUFFLE_DATA[idx]["name"]: SHUFFLE_DATA[idx] for idx in [0]}, + "splits": {idx: SPLITS_DATA[idx] for idx in [1, 2]}, + }, + ], +) +@pytest.mark.parametrize("load_splits", [True, False]) +def test_load_metadata(tmpdir, data: dict, load_splits: bool): + """Tests that loading the metadata from files doesn't fail.""" + # write data to tmp file + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + with open(meta_path, "w") as f: + YAML().dump(data, f) + + print(cfg_path) + print(meta_path) + print(data["shuffles"]) + print(data["splits"]) + print() + + for _name, s in data["shuffles"].items(): + split = data["splits"][s["split"]] + train, test = split["train"], split["test"] + _create_doc_data(cfg, trainset_dir, s["train_fraction"], s["index"], train, test) + + trainset_meta = metadata.TrainingDatasetMetadata.load(str(cfg_path), load_splits=load_splits) + for s in trainset_meta.shuffles: + print(s) + + assert len(data["shuffles"]) == len(trainset_meta.shuffles) + + for s in trainset_meta.shuffles: + shuffle_in = data["shuffles"][s.name] + split_idx = data["splits"][shuffle_in["split"]] + assert s.train_fraction == shuffle_in["train_fraction"] + assert s.engine == Engine(shuffle_in["engine"]) + if load_splits: + assert s.split is not None + assert s.split.train_indices == tuple(split_idx["train"]) + assert s.split.test_indices == tuple(split_idx["test"]) + else: + assert s.split is None + s_with_split = s.load_split(cfg, trainset_dir) + assert s_with_split.split.train_indices == tuple(split_idx["train"]) + assert s_with_split.split.test_indices == tuple(split_idx["test"]) + + +@pytest.mark.parametrize( + "data", + [ + { + "task": "ch", + "date": "Aug1", + "shuffles": (SHUFFLES[1],), + "expected": { + "shuffles": {SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}}, + }, + }, + { + "task": "t", + "date": "Jan1", + "shuffles": (SHUFFLES[1], SHUFFLES[3]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[3].name: { + "index": 3, + "train_fraction": 0.5, + "split": 1, + "engine": "tensorflow", + }, + }, + }, + }, + { + "task": "t", + "date": "Jan1", + "shuffles": (SHUFFLES[1], SHUFFLES[2]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[2].name: {"index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch"}, + }, + }, + }, + { + "shuffles": (SHUFFLES[1], SHUFFLES[2], SHUFFLES[3]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[2].name: {"index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch"}, + SHUFFLES[3].name: { + "index": 3, + "train_fraction": 0.5, + "split": 1, + "engine": "tensorflow", + }, + }, + }, + }, + ], +) +def test_save_metadata_simple(tmpdir, data): + """Tests that saving the metadata creates the expected file.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, data["shuffles"]) + print(trainset_meta) + + trainset_meta.save() + with open(meta_path) as f: + meta = YAML().load(f) + print(data) + print(meta) + assert data["expected"] == meta + + +@pytest.mark.parametrize( + "shuffles", + [[SHUFFLES[i] for i in indices] for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]], +) +def test_save_metadata(tmpdir, shuffles): + """Tests that saving the metadata and reloading it leads to the same instance.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + for s in shuffles: + train, test = ( + s.split.train_indices, + s.split.test_indices, + ) + _create_doc_data(cfg, trainset_dir, s.train_fraction, s.index, train, test) + + trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple(shuffles)) + print(trainset_meta) + trainset_meta.save() + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + print(reloaded) + print() + + for s in trainset_meta.shuffles: + print(s) + print() + for s in reloaded.shuffles: + print(s) + print() + reloaded_with_splits = [s.load_split(cfg, trainset_dir) for s in reloaded.shuffles] + assert len(reloaded.shuffles) == len(trainset_meta.shuffles) + assert len(reloaded_with_splits) == len(trainset_meta.shuffles) + assert tuple(reloaded_with_splits) == trainset_meta.shuffles + + +def test_add_shuffle(tmpdir): + """Tests that a shuffle can be added correctlt.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + trainset_meta_added = trainset_meta.add(SHUFFLES[2]) + assert len(trainset_meta.shuffles) == 1 + assert len(trainset_meta_added.shuffles) == 2 + assert trainset_meta_added.shuffles == (SHUFFLES[1], SHUFFLES[2]) + + +def test_add_shuffle_twice(tmpdir): + """Tests that a shuffle can be added correctlt.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + trainset_meta_added = trainset_meta.add(SHUFFLES[2]) + trainset_meta_added_2 = trainset_meta.add(SHUFFLES[2]) + assert len(trainset_meta.shuffles) == 1 + assert trainset_meta.shuffles == (SHUFFLES[1],) + assert len(trainset_meta_added.shuffles) == len(trainset_meta_added_2.shuffles) + assert trainset_meta_added.shuffles == trainset_meta_added_2.shuffles + + +def test_add_shuffle_sorts_to_correct_order(tmpdir): + """Tests that a shuffle can be added correctlt.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1], SHUFFLES[3])) + trainset_meta_added = trainset_meta.add(SHUFFLES[2]) + assert len(trainset_meta.shuffles) == 2 + assert len(trainset_meta_added.shuffles) == 3 + assert trainset_meta_added.shuffles == (SHUFFLES[1], SHUFFLES[2], SHUFFLES[3]) + + +@pytest.mark.parametrize( + "shuffles", [indices for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]] +) +@pytest.mark.parametrize("shuffle_to_add", [1, 2, 3, 4]) +def test_add_shuffle_indices(tmpdir, shuffles, shuffle_to_add): + """Tests.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple([SHUFFLES[i] for i in shuffles])) + if shuffle_to_add in shuffles: + with pytest.raises(RuntimeError): + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=False) + + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=True) + assert len(trainset_meta_added.shuffles) == len(shuffles) + assert [s.index for s in trainset_meta_added.shuffles] == shuffles + else: + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=False) + indices = [s.index for s in trainset_meta_added.shuffles] + assert len(trainset_meta_added.shuffles) == len(shuffles) + 1 + assert indices == list(sorted(shuffles + [shuffle_to_add])) + + +@pytest.mark.parametrize( + "split1, split2, equal", + [ + (BASE_SPLIT, EQ_SPLIT, True), + (BASE_SPLIT, ADD_SPLIT, False), + (BASE_SPLIT, ADD_SPLIT2, False), + (BASE_SPLIT, SUBS_SPLIT, False), + (BASE_SPLIT, DEL_SPLIT, False), + (BASE_SPLIT, DEL_SPLIT2, False), + ], +) +def test_data_split_equality(split1, split2, equal): + """Tests that equality functions as expected for DataSplits.""" + print(split1) + print(split2) + print(equal) + assert (split1 == split2) == equal + + +@pytest.mark.parametrize("split_idx", [1, 4, 20, 1000]) +@pytest.mark.parametrize("indices", [(2, 1), (10, 1), (1, 21, 20), (1, 2, 4, 3)]) +@pytest.mark.parametrize("sorted_indices", [(1, 2), (10, 12), (3, 4), (1, 1000, 1200)]) +def test_data_split_requires_sorted(split_idx: int, indices: tuple[int], sorted_indices: tuple[int]): + """Tests that equality functions as expected for DataSplits.""" + with pytest.raises(RuntimeError): + metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(sorted_indices)) + + with pytest.raises(RuntimeError): + metadata.DataSplit(train_indices=tuple(sorted_indices), test_indices=tuple(indices)) + + with pytest.raises(RuntimeError): + metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(indices)) + + metadata.DataSplit(train_indices=tuple(sorted_indices), test_indices=tuple(sorted_indices)) + + +@pytest.mark.parametrize( + "shuffles", + [ + ({"idx": 3, "train": [1], "test": [2], "train_fraction": 0.5},), + ( + {"idx": 1, "train": [1], "test": [2], "train_fraction": 0.5}, + {"idx": 5, "train": [1, 2, 3], "test": [4, 5], "train_fraction": 0.6}, + {"idx": 4, "train": [1, 3], "test": [2], "train_fraction": 0.66}, + ), + ], +) +def test_create_metadata_from_shuffles(tmpdir, shuffles): + """Tests that equality functions as expected for DataSplits.""" + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + print(trainset_dir) + for s in shuffles: + doc = f"Documentation_data-ex_{s['train_fraction']}shuffle{s['idx']}.pickle" + doc_path = trainset_dir.join(doc) + with open(doc_path, "wb") as f: + pickle.dump([[], s["train"], s["test"], s["train_fraction"]], f, pickle.HIGHEST_PROTOCOL) + + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + print() + print(trainset_metadata) + assert len(trainset_metadata.shuffles) == len(shuffles) + + for shuffle_data, shuffle in zip(shuffles, trainset_metadata.shuffles, strict=False): + print(shuffle.index) + assert shuffle_data["idx"] == shuffle.index + assert shuffle_data["train_fraction"] == shuffle.train_fraction + assert tuple(shuffle_data["train"]) == shuffle.split.train_indices + assert tuple(shuffle_data["test"]) == shuffle.split.test_indices + print() + + +def test_get_shuffle_engine_warns_when_metadata_get_fails_then_uses_model_folder(caplog): + """ValueError from metadata lookup is logged; engine is inferred from model folders.""" + caplog.set_level(logging.WARNING) + + cfg = { + "project_path": "/tmp/dlc-nonexistent-project-path", + "TrainingFraction": [0.95], + "Task": "t", + "date": "d", + "scorer": "s", + "iteration": 0, + } + + meta_path_mock = MagicMock() + meta_path_mock.exists.return_value = True + + training_meta_mock = MagicMock() + training_meta_mock.get.side_effect = ValueError("no shuffle for this index") + + with ( + patch.object(metadata.TrainingDatasetMetadata, "path", return_value=meta_path_mock), + patch.object(metadata.TrainingDatasetMetadata, "load", return_value=training_meta_mock), + patch.object(metadata, "find_engines_from_model_folders", return_value={Engine.PYTORCH}), + ): + engine = metadata.get_shuffle_engine(cfg, trainingsetindex=0, shuffle=1) + + assert engine == Engine.PYTORCH + assert "no shuffle for this index" in caplog.text + assert "Falling back to detecting the engine from model folders" in caplog.text + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.__post_init__ +# --------------------------------------------------------------------------- + + +def test_training_dataset_metadata_requires_sorted_shuffles(tmpdir): + """Constructor raises RuntimeError when shuffles are not sorted.""" + cfg, *_ = _create_project_with_config(tmpdir) + with pytest.raises(RuntimeError): + metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[2], SHUFFLES[1])) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.get +# --------------------------------------------------------------------------- + + +def test_get_returns_matching_shuffle(tmpdir): + """get() returns the correct ShuffleMetadata.""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + result = trainset_meta.get(trainset_index=0, index=1) + assert result == SHUFFLES[1] + + +def test_get_raises_when_trainset_index_out_of_bounds(tmpdir): + """get() raises ValueError when trainset_index >= len(TrainingFraction).""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + with pytest.raises(ValueError, match="out of bounds"): + trainset_meta.get(trainset_index=1, index=1) + + +def test_get_raises_when_shuffle_not_found(tmpdir): + """get() raises ValueError when no shuffle matches the given index.""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + with pytest.raises(ValueError, match="Could not find"): + trainset_meta.get(trainset_index=0, index=99) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.save — lazy load_split branch +# --------------------------------------------------------------------------- + + +def test_save_loads_split_when_shuffle_has_no_split(tmpdir): + """save() calls load_split for shuffles where split is None.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + + # Build a shuffle with split=None — mimics a load(load_splits=False) result + shuffle_no_split = metadata.ShuffleMetadata( + name=SHUFFLES[1].name, + train_fraction=0.5, + index=1, + engine=Engine.PYTORCH, + split=None, + ) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (shuffle_no_split,)) + # Should not raise; save() must call load_split internally + trainset_meta.save() + + reloaded = metadata.TrainingDatasetMetadata.load(cfg, load_splits=True) + assert len(reloaded.shuffles) == 1 + assert reloaded.shuffles[0].split is not None + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.load +# --------------------------------------------------------------------------- + + +def test_load_raises_when_metadata_file_missing(tmpdir): + """load() raises FileNotFoundError when metadata.yaml does not exist.""" + cfg, *_ = _create_project_with_config(tmpdir) + with pytest.raises(FileNotFoundError): + metadata.TrainingDatasetMetadata.load(cfg) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.create — empty trainset_path branch +# --------------------------------------------------------------------------- + + +def test_create_returns_empty_when_trainset_dir_missing(tmp_path): + """create() returns metadata with no shuffles when the trainset dir is absent.""" + cfg = { + "Task": "t", + "date": "d", + "scorer": "s", + "iteration": 0, + "project_path": str(tmp_path), + } + trainset_meta = metadata.TrainingDatasetMetadata.create(cfg) + assert len(trainset_meta.shuffles) == 0 + + +# --------------------------------------------------------------------------- +# update_metadata +# --------------------------------------------------------------------------- + + +def test_update_metadata_adds_shuffle(tmpdir): + """update_metadata adds a new shuffle and persists it.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + # Seed an existing metadata file with one shuffle + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=2, + engine=Engine.PYTORCH, + train_indices=[0, 1], + test_indices=[2, 3], + ) + + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + indices = [s.index for s in reloaded.shuffles] + assert 2 in indices + + +def test_update_metadata_overwrite(tmpdir): + """update_metadata with overwrite=True replaces an existing shuffle.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=1, + engine=Engine.TF, + train_indices=[0, 1], + test_indices=[2, 3], + overwrite=True, + ) + + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + assert len(reloaded.shuffles) == 1 + assert reloaded.shuffles[0].engine == Engine.TF + + +def test_update_metadata_raises_without_overwrite(tmpdir): + """update_metadata raises RuntimeError when shuffle exists and overwrite=False.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + with pytest.raises(RuntimeError): + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=1, + engine=Engine.PYTORCH, + train_indices=[0, 1], + test_indices=[2, 3], + overwrite=False, + ) + + +def _create_project_with_config( + tmp, + task: str = "example", + date: str = "Feb21", + scorer: str = "wayneRooney", + iteration: int = 0, + engine: str | None = None, +): + project_dir = tmp.mkdir("ex-ample-2024-02-21") + cfg = { + "Task": task, + "date": date, + "scorer": scorer, + "iteration": iteration, + "project_path": str(project_dir), + } + if engine is not None: + cfg["engine"] = engine + + cfg_path = project_dir.join("config.yaml") + with open(cfg_path, "w") as file: + YAML().dump(cfg, file) + + it = f"iteration-{iteration}" + dir_name = "UnaugmentedDataSet_" + task + date + trainset_dir = project_dir.mkdir("training-datasets").mkdir(it).mkdir(dir_name) + + meta_path = trainset_dir.join("metadata.yaml") + return cfg, cfg_path, trainset_dir, meta_path + + +def _create_doc_data( + cfg, + trainset_dir, + train_frac, + shuffle, + train_indices, + test_indices, +) -> None: + _, doc_path = auxiliaryfunctions.get_data_and_metadata_filenames(trainset_dir, train_frac, shuffle, cfg) + auxiliaryfunctions.save_metadata(doc_path, {}, list(train_indices), list(test_indices), train_frac) diff --git a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py new file mode 100644 index 0000000000..253841df54 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py @@ -0,0 +1,470 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from dataclasses import dataclass +from unittest.mock import Mock, patch + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.apis as apis +import deeplabcut.pose_estimation_pytorch.data as data + +PREDICT = Mock() + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize("num_individuals", [1, 2, 5]) +@pytest.mark.parametrize( + "bodyparts, error", + [ + (["nose", "left_ear"], [5, 10]), + (["nose", "left_ear", "right_ear"], [2, 3, 4]), + ], +) +def test_evaluate_basic( + num_individuals: int, + bodyparts: list[str], + error: list[float], +) -> None: + print() + gt, pred = generate_data(1, num_individuals, len(bodyparts), error) + + pose_runner = Mock() + + PREDICT.return_value = {img: {"bodyparts": pose} for img, pose in pred.items()} + loader = build_mock_loader(gt, num_individuals, bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test") + print("results", results) + np.testing.assert_almost_equal(results["rmse"], np.mean(error)) + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize("num_individuals", [1, 2, 5]) +@pytest.mark.parametrize( + "bodyparts, error", + [ + (["nose", "left_ear"], [5, 10]), + (["nose", "left_ear", "right_ear"], [2, 3, 4]), + ], +) +@pytest.mark.parametrize( + "unique_bodyparts, unique_error", + [ + (["top_left"], [2]), + (["top_left", "bottom_right"], [2, 3]), + ], +) +def test_evaluate_with_unique_bodyparts( + num_individuals: int, + bodyparts: list[str], + error: list[float], + unique_bodyparts: list[str], + unique_error: list[float], +) -> None: + print() + num_images = 5 + gt, pred = generate_data(num_images, num_individuals, len(bodyparts), error) + gt_unique, pred_unique = generate_data(num_images, 1, len(unique_bodyparts), unique_error) + + pose_runner = Mock() + PREDICT.return_value = { + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() + } + loader = build_mock_loader(gt, num_individuals, bodyparts, gt_unique=gt_unique, unique=unique_bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test") + idv_errors = np.tile(error, (num_individuals, 1)).reshape(-1) + expected_rmse = np.mean(np.concatenate([idv_errors, unique_error])) + print(num_individuals) + print(error) + print(idv_errors) + print(unique_error) + print(np.concatenate([idv_errors, unique_error])) + print(expected_rmse) + print("results", results) + np.testing.assert_almost_equal(results["rmse"], expected_rmse) + + +@dataclass +class CompTestConfig: + num_individuals: int = 1 + bodyparts: tuple[str, ...] = ("nose", "left_ear") + error: tuple[float, ...] = (5, 10) + unique_bodyparts: tuple[str, ...] = ("top_left",) + unique_error: tuple[float, ...] = (2,) + comparison_bodyparts: str | list[str] | None = None + expected_error: float = (2 + 5 + 10) / 3 + + def num_bpt(self) -> int: + return len(self.bodyparts) + + def num_unique(self) -> int: + return len(self.unique_bodyparts) + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize( + "cfg", + [ + CompTestConfig(comparison_bodyparts=None), + CompTestConfig(comparison_bodyparts="all"), + CompTestConfig(comparison_bodyparts=["nose", "left_ear", "top_left"]), + CompTestConfig(num_individuals=2, expected_error=(2 + 5 + 5 + 10 + 10) / 5), + CompTestConfig(comparison_bodyparts="nose", expected_error=5), + CompTestConfig(comparison_bodyparts=["nose"], expected_error=5), + CompTestConfig(comparison_bodyparts=["left_ear"], expected_error=10), + CompTestConfig(comparison_bodyparts=["nose", "left_ear"], expected_error=7.5), + CompTestConfig(comparison_bodyparts="top_left", expected_error=2), + CompTestConfig(comparison_bodyparts=["top_left"], expected_error=2), + CompTestConfig( + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + comparison_bodyparts=["a", "b", "c"], + expected_error=4, + ), + CompTestConfig( + num_individuals=1, + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + comparison_bodyparts=["nose", "a", "b", "c"], + expected_error=(5.0 + 3.0 + 4.0 + 5.0) / 4, + ), + CompTestConfig( + num_individuals=7, + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + comparison_bodyparts=["nose", "left_ear", "a", "b"], + expected_error=((7 * 5) + (7 * 10) + 3.0 + 4.0) / (7 + 7 + 2), + ), + ], +) +def test_evaluate_with_comparison_bodyparts(cfg: CompTestConfig) -> None: + print() + num_images = 5 + gt, pred = generate_data(num_images, cfg.num_individuals, cfg.num_bpt(), cfg.error) + gt_unique, pred_unique = generate_data(num_images, 1, cfg.num_unique(), cfg.unique_error) + + pose_runner = Mock() + PREDICT.return_value = { + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() + } + loader = build_mock_loader( + gt, + cfg.num_individuals, + cfg.bodyparts, + gt_unique=gt_unique, + unique=cfg.unique_bodyparts, + ) + results, preds = apis.evaluate( + pose_runner, + loader, + mode="test", + comparison_bodyparts=cfg.comparison_bodyparts, + ) + print(cfg) + print("results", results) + np.testing.assert_almost_equal(results["rmse"], cfg.expected_error) + + +@dataclass +class KeypointData: + img: int + idv: int + bodypart: str + gt: tuple[float, float] + pred: tuple[float, float] + score: float + + def image(self) -> str: + return f"image_{self.img:04d}.png" + + def error(self) -> float: + return np.linalg.norm(np.asarray(self.gt, dtype=float) - np.asarray(self.pred, dtype=float)).item() + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize( + "pcutoff", + [0.4, 0.6, 0.8, [0.3, 0.5, 0.7]], +) +@pytest.mark.parametrize( + "keypoints", + [ + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.7), + KeypointData(img=0, idv=0, bodypart="c", gt=(20, 20), pred=(20, 22), score=0.5), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 30), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=1, bodypart="a", gt=(40, 10), pred=(41, 10), score=0.7), + KeypointData(img=0, idv=1, bodypart="b", gt=(50, 20), pred=(49, 20), score=0.5), + KeypointData(img=0, idv=1, bodypart="c", gt=(60, 20), pred=(58, 20), score=0.2), + ], + ], +) +def test_evaluate_with_pcutoff( + pcutoff: float | list[float], + keypoints: list[KeypointData], +) -> None: + print() + + images = {d.image() for d in keypoints} + individuals = list({d.idv for d in keypoints if d.idv != -1}) + bodyparts = list({d.bodypart for d in keypoints if d.idv != -1}) + unique_bodyparts = list({d.bodypart for d in keypoints if d.idv == -1}) + + num_idv = len(individuals) + num_bodyparts = len(bodyparts) + len(unique_bodyparts) + + gt, pred = {}, {} + for img in images: + gt[img] = np.zeros((num_idv, num_bodyparts, 3)) + pred[img] = np.zeros((num_idv, num_bodyparts, 3)) + + errors = [] + errors_cutoff = [] + for kpt in keypoints: + img = kpt.image() + bpt = bodyparts.index(kpt.bodypart) + + gt[img][kpt.idv, bpt, :2] = kpt.gt + gt[img][kpt.idv, bpt, 2] = 2 + pred[img][kpt.idv, bpt, :2] = kpt.pred + pred[img][kpt.idv, bpt, 2] = kpt.score + + if isinstance(pcutoff, list): + bpt_cutoff = pcutoff[bpt] + else: + bpt_cutoff = pcutoff + + errors.append(kpt.error()) + if kpt.score >= bpt_cutoff: + errors_cutoff.append(kpt.error()) + + print(errors) + print(errors_cutoff) + + pose_runner = Mock() + PREDICT.return_value = {img: {"bodyparts": pose} for img, pose in pred.items()} + loader = build_mock_loader(gt, num_idv, bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test", pcutoff=pcutoff) + print("results", results) + np.testing.assert_almost_equal(results["rmse"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_pcutoff"], np.mean(errors_cutoff)) + if "rmse_detections" in results: + np.testing.assert_almost_equal(results["rmse_detections"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_detections_pcutoff"], np.mean(errors_cutoff)) + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize( + "pcutoff", + [ + 0.4, + 0.6, + 0.8, + [0.3, 0.5, 0.7, 0.4, 0.6], + [0.25, 0.43, 0.61, 0.46, 0.92], + [0.12, 0.15, 0.92, 0.97, 0.85], + [0.92, 0.97, 0.85, 0.12, 0.15], + ], +) +@pytest.mark.parametrize( + "keypoints", + [ + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.7), + KeypointData(img=0, idv=0, bodypart="c", gt=(20, 20), pred=(20, 22), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(20, 20), pred=(20, 22), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.3), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 30), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=1, bodypart="a", gt=(40, 10), pred=(41, 10), score=0.7), + KeypointData(img=0, idv=1, bodypart="b", gt=(50, 20), pred=(49, 20), score=0.5), + KeypointData(img=0, idv=1, bodypart="c", gt=(60, 20), pred=(58, 20), score=0.2), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.9), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(8, 13), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 27), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 36), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.9), + KeypointData(img=1, idv=0, bodypart="a", gt=(15, 20), pred=(41, 10), score=0.7), + KeypointData(img=1, idv=0, bodypart="b", gt=(20, 12), pred=(49, 20), score=0.5), + KeypointData(img=1, idv=0, bodypart="c", gt=(17, 32), pred=(58, 20), score=0.2), + KeypointData(img=1, idv=-1, bodypart="u1", gt=(37, 4), pred=(3, 3), score=0.7), + KeypointData(img=1, idv=-1, bodypart="u2", gt=(12, 6), pred=(20, 22), score=0.9), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(8, 13), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 27), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(30, 36), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u3", gt=(20, 20), pred=(20, 22), score=0.9), + KeypointData(img=1, idv=0, bodypart="a", gt=(15, 20), pred=(41, 10), score=0.7), + KeypointData(img=1, idv=0, bodypart="b", gt=(20, 12), pred=(49, 20), score=0.5), + KeypointData(img=1, idv=-1, bodypart="u1", gt=(17, 32), pred=(58, 20), score=0.2), + KeypointData(img=1, idv=-1, bodypart="u2", gt=(37, 4), pred=(3, 3), score=0.7), + KeypointData(img=1, idv=-1, bodypart="u3", gt=(12, 6), pred=(20, 22), score=0.9), + ], + ], +) +def test_evaluate_with_pcutoff_and_unique_bodyparts( + pcutoff: float | list[float], + keypoints: list[KeypointData], +) -> None: + print() + + images = {d.image() for d in keypoints} + individuals = list({d.idv for d in keypoints if d.idv != -1}) + bodyparts = list({d.bodypart for d in keypoints if d.idv != -1}) + unique_bodyparts = list({d.bodypart for d in keypoints if d.idv == -1}) + + num_idv = len(individuals) + num_bodyparts = len(bodyparts) + num_unique = len(unique_bodyparts) + + gt, pred, gt_unique, pred_unique = {}, {}, {}, {} + for img in images: + gt[img] = np.zeros((num_idv, num_bodyparts, 3)) + pred[img] = np.zeros((num_idv, num_bodyparts, 3)) + gt_unique[img] = np.zeros((1, num_unique, 3)) + pred_unique[img] = np.zeros((1, num_unique, 3)) + + errors, errors_cutoff = [], [] + for kpt in keypoints: + img = kpt.image() + if kpt.idv == -1: + idv, bpt = 0, unique_bodyparts.index(kpt.bodypart) + pcutoff_idx = bpt + len(bodyparts) # offset by number of bodyparts + gt_data, pred_data = gt_unique[img], pred_unique[img] + else: + idv, bpt = kpt.idv, bodyparts.index(kpt.bodypart) + pcutoff_idx = bpt + gt_data, pred_data = gt[img], pred[img] + + gt_data[idv, bpt, :2] = kpt.gt + gt_data[idv, bpt, 2] = 2 + pred_data[idv, bpt, :2] = kpt.pred + pred_data[idv, bpt, 2] = kpt.score + + if isinstance(pcutoff, list): + bpt_cutoff = pcutoff[pcutoff_idx] + else: + bpt_cutoff = pcutoff + + errors.append(kpt.error()) + if kpt.score >= bpt_cutoff: + errors_cutoff.append(kpt.error()) + + print(errors) + print(errors_cutoff) + + pose_runner = Mock() + PREDICT.return_value = { + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() + } + loader = build_mock_loader(gt, num_idv, bodyparts, gt_unique, unique_bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test", pcutoff=pcutoff) + + print("results", results) + np.testing.assert_almost_equal(results["rmse"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_pcutoff"], np.mean(errors_cutoff)) + if "rmse_detections" in results: + np.testing.assert_almost_equal(results["rmse_detections"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_detections_pcutoff"], np.mean(errors_cutoff)) + + +def generate_data( + num_images: int, + num_individuals: int, + num_bodyparts: int, + error: list[float] | tuple[float, ...] | np.ndarray, + cutoffs: list[float] | tuple[float, ...] | np.ndarray | None = None, + error_cutoff: list[float] | tuple[float, ...] | np.ndarray | None = None, +) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]: + num_elems = num_individuals * num_bodyparts + shape = num_individuals, num_bodyparts, 3 + error = np.asarray(error) + coord_error = (np.sqrt(2) / 2) * error + + gt, pred = {}, {} + for img in range(num_images): + gt_pose = 100 * np.arange(3 * num_elems, dtype=float).reshape(shape) + gt_pose[..., 2] = 2 + gt[f"img_{img:04d}.png"] = gt_pose + + pred_pose = np.ones(shape, dtype=float) + pred_pose[..., :2] = gt_pose[..., :2] + pred_pose[:, :, 0] += coord_error + pred_pose[:, :, 1] += coord_error + pred[f"img_{img:04d}.png"] = pred_pose + + if error_cutoff is not None and cutoffs is not None: + for img in range(num_images): + gt_pose = 100 * np.arange(3 * num_elems, dtype=float).reshape(shape) + gt_pose[..., 2] = 2 + gt[f"img_{num_images + img:04d}.png"] = gt_pose + + pred_pose = np.ones(shape, dtype=float) + pred_pose[..., :2] = gt_pose[..., :2] + pred_pose[..., 2] = cutoffs + pred_pose[:, :, 0] += coord_error + pred_pose[:, :, 1] += coord_error + pred[f"img_{num_images + img:04d}.png"] = pred_pose + + return gt, pred + + +def build_mock_loader( + gt: dict[str, np.ndarray], + num_individuals: int, + bodyparts: list[str] | tuple[str, ...], + gt_unique: dict[str, np.ndarray] | None = None, + unique: list[str] | tuple[str, ...] | None = None, +) -> Mock: + if unique is None: + unique = [] + + def _gt(mode: str, unique_bodypart: bool = False) -> dict[str, np.ndarray]: + if unique_bodypart: + print("LOADING UNIQUE GT") + return gt_unique + print("LOADING GT") + return gt + + individuals = [f"animal_{i:03d}" for i in range(num_individuals)] + loader = Mock() + loader.get_dataset_parameters.return_value = data.PoseDatasetParameters( + bodyparts=bodyparts, + unique_bpts=unique, + individuals=individuals, + ) + loader.ground_truth_keypoints = _gt + loader.model_cfg = { + "metadata": { + "bodyparts": bodyparts, + "unique_bodyparts": unique, + "individuals": individuals, + "with_identity": False, + }, + "train_settings": {}, + } + return loader diff --git a/tests/pose_estimation_pytorch/apis/test_apis_export.py b/tests/pose_estimation_pytorch/apis/test_apis_export.py new file mode 100644 index 0000000000..021a994f96 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_export.py @@ -0,0 +1,308 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests exporting models.""" + +import copy +import shutil +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +import torch +from ruamel.yaml.scalarstring import SingleQuotedScalarString as SQS + +import deeplabcut.pose_estimation_pytorch.apis.export as export +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.pose_estimation_pytorch import Task +from deeplabcut.pose_estimation_pytorch.runners.snapshots import Snapshot + + +@pytest.fixture() +def project_dir(tmp_path_factory) -> Path: + project_dir = tmp_path_factory.mktemp("tmp-project") + print("\nTemporary project directory:") + print(str(project_dir)) + print("---") + yield project_dir + shutil.rmtree(str(project_dir)) + + +def _mock_multianimal_project(project_dir: Path): + video_dir = project_dir / "videos" + video_dir.mkdir(exist_ok=True) + + cfg_file, yaml_file = af.create_config_template(multianimal=True) + yaml_file.width = 10_000 + + cfg_file["Task"] = "mock" + cfg_file["scorer"] = "mock" + cfg_file["video_sets"] = {SQS((video_dir / "vid.mp4").as_posix()): {"crop": "0, 640, 0, 480"}} + cfg_file["project_path"] = project_dir.as_posix() + cfg_file["individuals"] = ["a", "b"] + cfg_file["uniquebodyparts"] = [] + cfg_file["multianimalbodyparts"] = ["k1", "k2", "k3"] + cfg_file["bodyparts"] = "MULTI!" + + with open(project_dir / "config.yaml", "w", encoding="utf-8") as f: + yaml_file.dump(cfg_file, f) + + +def _make_mock_loader( + project_path: Path, + project_task: str, + project_iteration: int, + model_folder: Path, + net_type: str, + pose_task: Task, + default_snapshot_index: int | str, + default_detector_snapshot_index: int | str, +) -> Mock: + loader = Mock() + loader.project_path = project_path + loader.model_folder = model_folder + loader.pose_task = pose_task + loader.shuffle = 0 + + loader.project_cfg = dict( + project_path=str(project_path), + Task=project_task, + date="Jan12", + TrainingFraction=[0.95], + snapshotindex=default_snapshot_index, + detector_snapshotindex=default_detector_snapshot_index, + iteration=project_iteration, + ) + loader.model_cfg = dict( + net_type=net_type, + metadata=dict( + project_path=str(project_path), + pose_config_path=str(loader.model_folder / "pytorch_config.yaml"), + ), + weight_init=None, + resume_training_from=None, + ) + if pose_task == Task.TOP_DOWN: + loader.model_cfg["detector"] = dict(resume_training_from=None) + + return loader + + +def _get_export_model_data( + project_dir: Path, + num_snapshots: int, + task: Task, + project_iteration: int = 0, +): + _mock_multianimal_project(project_dir) + + model_dir = Path(project_dir) / f"iteration-{project_iteration}" / "fake-shuffle-0" + model_dir.mkdir(exist_ok=True, parents=True) + snapshots = [] + snapshot_data = [] + for i in range(num_snapshots): + snapshot = dict(model=dict(idx=i)) + snapshot_path = model_dir / f"snapshot-{i:03}.pt" + torch.save(snapshot, snapshot_path) + snapshots.append(Snapshot(best=False, epochs=i, path=snapshot_path)) + snapshot_data.append(snapshot) + + detector_snapshots = [] + detector_data = [] + if task == Task.TOP_DOWN: + for i in range(num_snapshots): + snapshot = dict(model=dict(idx=i)) + snapshot_path = model_dir / f"snapshot-detector-{i:03}.pt" + torch.save(snapshot, snapshot_path) + detector_data.append(snapshot) + detector_snapshots.append(Snapshot(best=False, epochs=i, path=snapshot_path)) + + mock_loader = _make_mock_loader( + project_path=project_dir, + project_task="mock", + project_iteration=project_iteration, + model_folder=model_dir, + net_type="fake-net", + pose_task=task, + default_snapshot_index=-1, + default_detector_snapshot_index=-1, + ) + return mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data + + +@pytest.mark.parametrize( + "task, num_snapshots, idx, detector_idx", + [ + (Task.BOTTOM_UP, 10, 0, None), + (Task.BOTTOM_UP, 10, 5, None), + (Task.BOTTOM_UP, 10, -1, None), + (Task.TOP_DOWN, 10, 0, 0), + (Task.TOP_DOWN, 10, -1, 0), + (Task.TOP_DOWN, 10, -1, 5), + (Task.TOP_DOWN, 10, -1, -1), + ], +) +def test_export_model( + project_dir, + task: Task, + num_snapshots: int, + idx: int, + detector_idx: int | None, +): + test_data = _get_export_model_data(project_dir, num_snapshots, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + # export the model + export.export_model( + project_dir / "config.yaml", + snapshotindex=idx, + detector_snapshot_index=detector_idx, + ) + + # check that the correct snapshot was exported + snapshot = snapshots[idx] + detector = None + if task == Task.TOP_DOWN: + detector = detector_snapshots[detector_idx] + + dir_name = export.get_export_folder_name(mock_loader) + filename = export.get_export_filename(mock_loader, snapshot, detector) + expected_export = project_dir / "exported-models-pytorch" / dir_name / filename + assert expected_export.exists() + + # check that content of the exports are correct + exported_data = torch.load(expected_export, weights_only=True) + assert isinstance(exported_data, dict) + assert "config" in exported_data + assert exported_data["config"] == mock_loader.model_cfg + + assert "pose" in exported_data + assert exported_data["pose"] == snapshot_data[idx]["model"] + + if task == Task.TOP_DOWN: + assert "detector" in exported_data + assert exported_data["detector"] == detector_data[detector_idx]["model"] + + +@patch("deeplabcut.pose_estimation_pytorch.apis.export.wipe_paths_from_model_config") +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +def test_export_model_clear_paths(mock_wipe: Mock, project_dir, task: Task): + test_data = _get_export_model_data(project_dir, 1, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + export.export_model(project_dir / "config.yaml", wipe_paths=True) + + # check that wipe_paths_from_model_config was called + assert mock_wipe.call_count == 1 + + +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +@pytest.mark.parametrize("overwrite", [True, False]) +def test_export_overwrite(project_dir, task: Task, overwrite: bool): + test_data = _get_export_model_data(project_dir, 1, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + snapshot = snapshots[0] + detector = None if task == Task.BOTTOM_UP else detector_snapshots[0] + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + dir_name = export.get_export_folder_name(mock_loader) + filename = export.get_export_filename(mock_loader, snapshot, detector) + expected_export = project_dir / "exported-models-pytorch" / dir_name / filename + expected_export.parent.mkdir(exist_ok=False, parents=True) + + # add existing data + assert not expected_export.exists() + existing_data = dict() + torch.save(existing_data, expected_export) + + # export data + export.export_model(project_dir / "config.yaml", overwrite=overwrite) + + exported_data = torch.load(expected_export, weights_only=True) + + if overwrite: + assert existing_data != exported_data + else: + assert existing_data == exported_data + + +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +@pytest.mark.parametrize("iteration", [5, 12]) +def test_export_change_iteration(project_dir, task: Task, iteration: int): + test_data = _get_export_model_data( + project_dir, + 1, + task, + project_iteration=0, + ) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + snapshot = snapshots[0] + detector = None if task == Task.BOTTOM_UP else detector_snapshots[0] + + loader_diff_iter = _get_export_model_data(project_dir, 1, task, project_iteration=iteration)[0] + + def get_mock_loader(config, *args, **kwargs): + _loader = copy.deepcopy(mock_loader) + if isinstance(config, dict): + _loader = copy.deepcopy(mock_loader) + _loader.project_cfg = config + return _loader + + def read_mock_config(*args, **kwargs): + return copy.deepcopy(mock_loader.project_cfg) + + # patch the DLCLoader but also read_config + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.af.read_config", + read_mock_config, + ): + # check no exports exist yet + for loader in [mock_loader, loader_diff_iter]: + dir_name = export.get_export_folder_name(loader) + filename = export.get_export_filename(loader, snapshot, detector) + assert not (project_dir / "exported-models-pytorch" / dir_name / filename).exists() + + # export data + export.export_model(project_dir / "config.yaml", iteration=iteration) + + # check the export exists for the correct iteration + for loader, file_should_exist in [ + (mock_loader, False), + (loader_diff_iter, True), + ]: + dir_name = export.get_export_folder_name(loader) + filename = export.get_export_filename(loader, snapshot, detector) + expected = project_dir / "exported-models-pytorch" / dir_name / filename + expected_exists = expected.exists() + assert expected_exists == file_should_exist diff --git a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py new file mode 100644 index 0000000000..e9ba4996d3 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py @@ -0,0 +1,74 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests method to create the tracking dataset in PyTorch.""" + +from pathlib import Path + +import torch + +import deeplabcut.pose_estimation_pytorch as dlc_torch +import deeplabcut.pose_estimation_pytorch.apis.tracking_dataset as tracking_dataset +import deeplabcut.pose_estimation_pytorch.models as models + + +class MockLoader(dlc_torch.Loader): + """Mock loader for data.""" + + def __init__(self, tmp_folder: Path, bodyparts: list[str] | None = None): + if bodyparts is None: + bodyparts = ["nose", "left_eye", "right_eye", "tail_base"] + self.bodyparts = bodyparts + + model_config_path = tmp_folder / "pytorch_config.yaml" + dlc_torch.config.make_pytorch_pose_config( + project_config=dlc_torch.config.make_basic_project_config( + dataset_path=str(tmp_folder), + bodyparts=self.bodyparts, + max_individuals=3, + ), + pose_config_path=tmp_folder / "pytorch_config.yaml", + net_type="resnet_50", + save=True, + ) + super().__init__( + str(tmp_folder), + str(tmp_folder / "labeled-data"), + model_config_path, + ) + + def load_data(self, mode: str = "train") -> dict[str, list[dict]]: + return { + "annotations": [], + "categories": [], + "images": [], + } + + def get_dataset_parameters(self) -> dlc_torch.PoseDatasetParameters: + return dlc_torch.PoseDatasetParameters( + bodyparts=self.bodyparts, + unique_bpts=[], + individuals=self.model_cfg["metadata"]["individuals"], + ) + + +def test_build_feature_extraction_runner(tmp_path_factory): + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + + loader = MockLoader(tmp_folder=tmp_folder) + model = models.PoseModel.build(loader.model_cfg["model"]) + snapshot_path = loader.model_folder / "snapshot.pt" + torch.save(dict(model=model.state_dict()), snapshot_path) + _ = tracking_dataset.build_feature_extraction_runner( + loader=loader, + snapshot_path=snapshot_path, + device="cpu", + batch_size=1, + ) diff --git a/tests/pose_estimation_pytorch/apis/test_tracklets.py b/tests/pose_estimation_pytorch/apis/test_tracklets.py new file mode 100644 index 0000000000..8e06b4a55e --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_tracklets.py @@ -0,0 +1,100 @@ +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.pose_estimation_pytorch.apis.tracklets import build_tracklets + + +@pytest.mark.parametrize( + "assemblies_data, inference_cfg, joints, scorer, num_frames, unique_bodyparts", + [ + ( + # assemblies_data + { + "single": { + 0: np.array([[1, 2, 0.9]]), + 1: np.array([[1, 3, 0.7]]), + 2: np.array([[0, 1, 0.9]]), + }, + 0: [ + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + ], + 1: [ + np.array([[9, 19, 0.9, -1], [29, 41, 0.8, -1]]), + np.array([[15, 21, 0.9, -1], [35, 45, 0.8, -1]]), + ], + 2: [ + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + ], + }, + # inference_cfg + {"max_age": 3, "min_hits": 1, "topktoretain": 1, "pcutoff": 0.5}, + # joints + ["nose", "ear"], + # scorer + "DLC", + # num_frames + 3, + # unique_bodyparts + ["led"], + ), + ( + # assemblies_data + { + 0: [ + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + ], + 1: [ + np.array([[9, 19, 0.9, -1], [29, 41, 0.8, -1]]), + np.array([[15, 21, 0.9, -1], [35, 45, 0.8, -1]]), + ], + 2: [ + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + ], + }, + # inference_cfg + {"max_age": 3, "min_hits": 1, "topktoretain": 1, "pcutoff": 0.5}, + # joints + ["nose", "ear"], + # scorer + "DLC", + # num_frames + 3, + # unique_bodyparts + None, + ), + ], +) +def test_build_tracklets( + assemblies_data: dict, + inference_cfg: dict, + joints: list, + scorer: str, + num_frames: int, + unique_bodyparts: list, +): + # Run the function + tracklets = build_tracklets( + assemblies_data=assemblies_data, + track_method="box", + inference_cfg=inference_cfg, + joints=joints, + scorer=scorer, + num_frames=num_frames, + unique_bodyparts=unique_bodyparts, + identity_only=False, + ) + + # # Assertions + assert "header" in tracklets + assert isinstance(tracklets["header"], pd.MultiIndex) + if unique_bodyparts: + assert "single" in tracklets + else: + assert "single" not in tracklets + + assert isinstance(tracklets, dict) diff --git a/tests/pose_estimation_pytorch/config/test_config_utils.py b/tests/pose_estimation_pytorch/config/test_config_utils.py new file mode 100644 index 0000000000..4a28ef567a --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_config_utils.py @@ -0,0 +1,67 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Test util functions for config creation.""" + +import pytest + +import deeplabcut.pose_estimation_pytorch.config.utils as utils + + +@pytest.mark.parametrize( + "data", + [ + dict( + config={}, + num_bodyparts=None, + num_individuals=None, + backbone_output_channels=None, + output_config={}, + ), + dict( + config={ + "a": "num_bodyparts", + "b": ["num_bodyparts // 2", "num_bodyparts // 3"], + "c": "num_bodyparts x 2", + "d": "num_bodyparts + 2", + }, + num_bodyparts=10, + num_individuals=None, + backbone_output_channels=None, + output_config={ + "a": 10, + "b": [5, 3], + "c": 20, + "d": 12, + }, + ), + dict( + config={ + "a": [{"b": "num_individuals x 3"}], + "b": [[{"b": "num_bodyparts x 3"}]], + }, + num_bodyparts=10, + num_individuals=1, + backbone_output_channels=None, + output_config={ + "a": [{"b": 3}], + "b": [[{"b": 30}]], + }, + ), + ], +) +def test_replace_default_values_no_extras(data: dict): + output_config = utils.replace_default_values( + config=data["config"], + num_bodyparts=data["num_bodyparts"], + num_individuals=data["num_individuals"], + backbone_output_channels=data["backbone_output_channels"], + ) + assert output_config == data["output_config"] diff --git a/tests/pose_estimation_pytorch/config/test_make_pose_config.py b/tests/pose_estimation_pytorch/config/test_make_pose_config.py new file mode 100644 index 0000000000..a8fbc7b6ff --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_make_pose_config.py @@ -0,0 +1,484 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the pre-processors.""" + +import pytest + +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.core.config import pretty_print +from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_basic_project_config, + make_pytorch_pose_config, +) +from deeplabcut.pose_estimation_pytorch.config.utils import ( + update_config, + update_config_by_dotpath, +) + + +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) +def test_make_single_animal_config(bodyparts: list[str], net_type: str): + # Single animal projects can't have unique bodyparts + project_config = _make_project_config( + project_path="my/little/project", + multianimal=False, + identity=False, + individuals=[], + bodyparts=bodyparts, + unique_bodyparts=[], + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + # check that the bodypart head has locref and heatmaps and the correct output shapes + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + outputs = [("heatmap_config", len(bodyparts))] + if bodypart_head["predictor"]["location_refinement"]: + outputs += [("locref_config", 2 * len(bodyparts))] + + for name, output_channels in outputs: + head = bodypart_head[name] + if "final_conv" in head: + actual_output_channels = head["final_conv"]["out_channels"] + else: + actual_output_channels = head["channels"][-1] + assert name in bodypart_head + assert actual_output_channels == output_channels + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) +def test_backbone_plus_paf_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + # Single animal projects can't have unique bodyparts + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] + num_limbs = len(graph) * 2 + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + # check PAF head + assert bodypart_head["type"] == "DLCRNetHead" + assert bodypart_head["predictor"]["type"] == "PartAffinityFieldPredictor" + + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ("locref_config", len(bodyparts) * 2), + ("paf_config", num_limbs), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize( + "detector", + [ + (None, "SSDLite"), + ("ssdlite", "SSDLite"), + ("fasterrcnn_mobilenet_v3_large_fpn", "FasterRCNN"), + ("fasterrcnn_resnet50_fpn_v2", "FasterRCNN"), + ], +) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) +def test_top_down_config( + detector: tuple[str, str], + individuals: list[str], + bodyparts: list[str], + net_type: str, +): + # Single animal projects can't have unique bodyparts + detector_type, expected_detector_type = detector + project_config = _make_project_config( + project_path="my/little/project", + multianimal=True, + identity=False, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=[], + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + top_down=True, + detector_type=detector_type, + ) + pretty_print(pytorch_pose_config) + + # check no collate function + collate = pytorch_pose_config["data"]["train"].get("collate") + print(f"Collate: {collate}") + assert not collate + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + # check detector is there + assert "detector" in pytorch_pose_config.keys() + assert pytorch_pose_config["detector"]["model"]["type"] == expected_detector_type + + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["final_conv"]["out_channels"] == output_channels + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["dekr_w18", "dekr_w32", "dekr_w48"]) +def test_make_dekr_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + for name, output_channels in [ + ("heatmap_config", len(bodyparts) + 1), + ("offset_config", len(bodyparts)), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose", "ears"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["dlcrnet_stride16_ms5", "dlcrnet_stride32_ms5"]) +def test_make_dlcrnet_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + paf_graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] + num_limbs = len(paf_graph) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ("locref_config", 2 * len(bodyparts)), + ("paf_config", 2 * num_limbs), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose", "eyes"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["animaltokenpose_base"]) +def test_make_tokenpose_config( + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=True, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + + if identity or len(unique_bodyparts) > 0: + with pytest.raises(ValueError) as _: + # Not yet implemented! + _ = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + else: + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check no collate function + collate = pytorch_pose_config["data"]["train"].get("collate") + print(f"Collate: {collate}") + assert not collate + + # check detector is there + assert "detector" in pytorch_pose_config + assert "data" in pytorch_pose_config["detector"] + + +@pytest.mark.parametrize( + "data", + [ + { + "config": {"a": 0, "b": 0}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": {"i0": [1, 2, 3]}}, + "expected_result": {"a": 0, "b": {"i0": [1, 2, 3], "i1": 2}}, + }, + { + "config": {"detector": {"batch_size": 1, "epochs": 10, "save_epochs": 5}}, + "updates": { + "batch_size": 1, + "detector": {"batch_size": 8, "save_epochs": 1}, + }, + "expected_result": { + "batch_size": 1, + "detector": {"batch_size": 8, "epochs": 10, "save_epochs": 1}, + }, + }, + ], +) +def test_update_config(data: dict): + result = update_config(config=data["config"], updates=data["updates"]) + print("\nResult") + pretty_print(result) + assert result == data["expected_result"] + + +@pytest.mark.parametrize( + "data", + [ + { + "config": {"a": 0, "b": 0}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b.i0": [1, 2, 3]}, + "expected_result": {"a": 0, "b": {"i0": [1, 2, 3], "i1": 2}}, + }, + { + "config": {"detector": {"batch_size": 1, "epochs": 10, "save_epochs": 5}}, + "updates": { + "batch_size": 1, + "detector.batch_size": 8, + "detector.save_epochs": 1, + }, + "expected_result": { + "batch_size": 1, + "detector": {"batch_size": 8, "epochs": 10, "save_epochs": 1}, + }, + }, + ], +) +def test_update_config_by_dotpath(data: dict): + result = update_config_by_dotpath(config=data["config"], updates=data["updates"]) + print("\nResult") + pretty_print(result) + assert result == data["expected_result"] + + +def _make_project_config( + project_path: str, + multianimal: bool, + identity: bool, + individuals: list[str], + bodyparts: list[str], + unique_bodyparts: list[str], +) -> dict: + project_config = { + "project_path": project_path, + "multianimalproject": multianimal, + "identity": identity, + "uniquebodyparts": unique_bodyparts, + } + + if multianimal: + project_config["multianimalbodyparts"] = bodyparts + project_config["bodyparts"] = "MULTI!" + project_config["individuals"] = individuals + else: + project_config["bodyparts"] = bodyparts + + return project_config + + +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("max_idv", [1, 12, 20]) +@pytest.mark.parametrize("multi", [True, False]) +def test_make_basic_project_config(bodyparts: list[str], max_idv: int, multi: bool): + if not multi and max_idv > 1: + return + + project_config = make_basic_project_config( + dataset_path="path/dataset", + bodyparts=bodyparts, + max_individuals=max_idv, + multi_animal=multi, + ) + + bpts = af.get_bodyparts(project_config) + assert bodyparts == bpts + + individuals = project_config["individuals"] + assert len(individuals) == max_idv + assert len(set(individuals)) == max_idv diff --git a/tests/pose_estimation_pytorch/data/test_data_ctd.py b/tests/pose_estimation_pytorch/data/test_data_ctd.py new file mode 100644 index 0000000000..37bd834d03 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_data_ctd.py @@ -0,0 +1,184 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import json +import platform +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromFile + +CONDITIONS = [ + np.zeros((4, 3, 3)).tolist(), + np.ones((4, 3, 3)).tolist(), + 2 * np.ones((4, 3, 3)).tolist(), + 3 * np.ones((4, 3, 3)).tolist(), +] + + +@pytest.mark.parametrize("path_prefix", ["/a/b"]) +@pytest.mark.parametrize( + "data", + [ + [("/a/b/c/d.png", "/a/b/c/d.png", CONDITIONS[1])], + [("/a/b/c/d.png", "c/d.png", CONDITIONS[1])], + [ + ("/a/b/c.png", "c.png", CONDITIONS[1]), + ("/a/b/c/d.png", "c/d.png", CONDITIONS[2]), + ("/a/b/c/e.png", "/a/b/c/e.png", CONDITIONS[3]), + ], + ], +) +def test_ctd_load_json_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + data: tuple[list[str], list[str], list], +) -> None: + print("Starting test") + # convert the image paths to Windows format + if platform.system() == "Windows": + print("Converting to windows filesystem") + + print("Path Prefix:", path_prefix) + if isinstance(path_prefix, Path): + print(f" As string: {str(path_prefix)}") + path_prefix = Path(_to_windows_path(str(path_prefix))) + else: + path_prefix = _to_windows_path(path_prefix) + print(f" Converted {path_prefix}") + + data = [(_to_windows_path(img), _to_windows_path(key), cond) for img, key, cond in data] + print(f"Images: {[d[0] for d in data]}") + print(f"Condition keys: {[d[1] for d in data]}") + print("---") + + images = [img for img, _, _ in data] + conditions = {key: cond for _, key, cond in data} + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + conditions_filepath = tmp_folder / "conditions.json" + with open(conditions_filepath, "w") as f: + json.dump(conditions, f) + + conditions = CondFromFile.load_conditions_json( + conditions_filepath, + images, + path_prefix=path_prefix, + ) + for img_path, _, condition in data: + assert img_path in conditions + np.testing.assert_allclose(condition, conditions[img_path]) + + +@pytest.mark.parametrize("path_prefix", ["/p"]) +@pytest.mark.parametrize("num_conditions", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize("num_bodyparts", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize( + "data", + [ + [("/p/data/video0/img0.png", ("data", "video0", "img0.png"))], + [("/p/data/video0/img0.png", "data/video0/img0.png")], + [ + ("/p/b/c/d0.png", ("b", "c", "d0.png")), + ("/p/b/c/d1.png", ("b", "c", "d1.png")), + ("/p/b/c/d2.png", ("b", "c", "d2.png")), + ], + [ + ("/p/b/c/d0.png", "b/c/d0.png"), + ("/p/b/c/d1.png", "b/c/d1.png"), + ("/p/b/c/d2.png", "b/c/d2.png"), + ], + ], +) +def test_ctd_load_hdf_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + num_conditions: int, + num_bodyparts: int, + data: tuple[list[str], list[str]], +) -> None: + print("\nStarting test") + + # convert the image paths to Windows format + if platform.system() == "Windows": + print("Converting to windows filesystem") + + print("Path Prefix:", path_prefix) + if isinstance(path_prefix, Path): + print(f" As string: {str(path_prefix)}") + path_prefix = Path(_to_windows_path(str(path_prefix))) + else: + path_prefix = _to_windows_path(path_prefix) + print(f" Converted {path_prefix}") + + data = [(_to_windows_path(img), idx) for img, idx in data] + print(f"Images: {[d[0] for d in data]}") + print("---") + + num_images = len(data) + images = [img for img, _ in data] + index = [idx for _, idx in data] + if isinstance(index[0], tuple): + index = pd.MultiIndex.from_tuples(index) + + # generate random pose data + size = (num_images, num_conditions, num_bodyparts, 3) + rng = np.random.default_rng(0) + pose = rng.integers(low=0, high=1024, size=size).astype(float) + pose[:, :, :, 2] = rng.random(size=(num_images, num_conditions, num_bodyparts)) + + # set some missing data + is_nans = rng.random(size=size) > 0.8 + pose[is_nans] = np.nan + + # create what the output data will look like + keypoint_mask = np.any(is_nans, axis=3) + output_pose = pose.copy() + output_pose[keypoint_mask] = 0.0 + idv_mask = ~np.all(keypoint_mask, axis=2) + + output_pose = [ + p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) + for p, p_mask in zip(output_pose, idv_mask, strict=False) + ] + + # generate columns for the dataframe + columns = pd.MultiIndex.from_product( + [ + ["scorer"], + [f"idv{i}" for i in range(num_conditions)], + [f"bpt{i}" for i in range(num_bodyparts)], + ["x", "y", "likelihood"], + ], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + df = pd.DataFrame(data=pose.reshape(num_images, -1), index=index, columns=columns) + + print(df.head()) + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + conditions_filepath = tmp_folder / "conditions.h5" + df.to_hdf(conditions_filepath, key="df_with_missing") + + conditions = CondFromFile.load_conditions_h5(conditions_filepath, images, path_prefix=path_prefix) + for idx, (img_path, _img_index) in enumerate(data): + assert img_path in conditions + np.testing.assert_allclose(output_pose[idx], conditions[img_path]) + + +def _to_windows_path(s: str) -> str: + # Convert absolute paths to paths on C: + if s.startswith("/"): + return str(Path("C:\\", *s[1:].split("/"))) + + return s diff --git a/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py b/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py new file mode 100644 index 0000000000..76fe04ca61 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py @@ -0,0 +1,68 @@ +from types import SimpleNamespace + +import numpy as np +import pandas as pd + +import deeplabcut.pose_estimation_pytorch.data.dlcloader as dlcloader_mod +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader + + +def test_to_coco_ignores_likelihood_columns(monkeypatch, tmp_path): + fake_shape = (3, 480, 640) + monkeypatch.setattr( + dlcloader_mod, + "read_image_shape_fast", + lambda _: fake_shape, + ) + + scorer = "testscorer" + bodyparts = ["nose", "tail"] + + index = pd.MultiIndex.from_tuples( + [("labeled-data", "video1", "img0001.png")], + names=["set", "video", "image"], + ) + + # Baseline dataframe: x/y only + columns_xy = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + df_xy = pd.DataFrame( + [[10.0, 20.0, 30.0, 40.0]], + index=index, + columns=columns_xy, + ) + + # Same data, but with likelihood columns added + columns_xyl = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "bodyparts", "coords"], + ) + df_xyl = pd.DataFrame( + [[10.0, 20.0, 0.9, 30.0, 40.0, 0.8]], + index=index, + columns=columns_xyl, + ) + + # to_coco only needs these attributes from parameters + params = SimpleNamespace( + bodyparts=bodyparts, + unique_bpts=[], + individuals=["animal"], + ) + + baseline = DLCLoader.to_coco(tmp_path, df_xy, params) + got = DLCLoader.to_coco(tmp_path, df_xyl, params) + + assert len(got["images"]) == len(baseline["images"]) == 1 + assert len(got["annotations"]) == len(baseline["annotations"]) == 1 + + got_ann = got["annotations"][0] + expected_ann = baseline["annotations"][0] + + assert got_ann["image_id"] == expected_ann["image_id"] + assert got_ann["category_id"] == expected_ann["category_id"] + assert got_ann["num_keypoints"] == expected_ann["num_keypoints"] == 2 + assert np.array_equal(got_ann["keypoints"], expected_ann["keypoints"]) + assert np.allclose(got_ann["bbox"], expected_ann["bbox"]) diff --git a/tests/pose_estimation_pytorch/data/test_postprocessor.py b/tests/pose_estimation_pytorch/data/test_postprocessor.py new file mode 100644 index 0000000000..7148fdf1c2 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_postprocessor.py @@ -0,0 +1,381 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the pre-processors.""" + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data.postprocessor import ( + PredictKeypointIdentities, + PrepareBackboneFeatures, + RemoveLowConfidenceBoxes, + RescaleAndOffset, + TrimOutputs, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "predictions": [[[0, 0, 0.95], [20, 30, 0.5]]], + "offsets": [(0, 0)], + "scales": [(1, 1)], + "rescaled": [[[0, 0, 0.95], [20, 30, 0.5]]], + }, + { + "predictions": [ + [[0, 0, 0.12], [1000, 0, 0.5]], # individual 1 + [[18, 2, 0.24], [0, 1000, 0.6]], # individual 2 + ], + "offsets": [(0, 0), (0, 0)], + "scales": [(1, 1), (0.5, 1.0)], + "rescaled": [ + [[0, 0, 0.12], [1000, 0, 0.5]], # individual 1 + [[9, 2, 0.24], [0, 1000, 0.6]], # individual 2 + ], + }, + { + "predictions": [ + [[0, 0, 0.95], [20, 30, 0.5]], # individual 1 + [[110, 5, 0.95], [60, 1200, 0.5]], # individual 2 + ], + "offsets": [(12, 5), (27, 10)], + "scales": [(0.5, 0.5), (0.2, 0.2)], + "rescaled": [ + [[12, 5, 0.95], [22, 20, 0.5]], # individual 1 + [[49, 11, 0.95], [39, 250, 0.5]], # individual 2 + ], + }, + ], +) +def test_rescale_topdown(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bodyparts"], + mode=RescaleAndOffset.Mode.KEYPOINT_TD, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bodyparts": np.array(data["predictions"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bodyparts"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bodyparts"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[0, 0, 0, 0], [1, 1, 1, 1]], + "bbox_scores": [0, 0], + "max_individuals": {"bboxes": 1, "bbox_scores": 1}, + }, + { + "bboxes": [[0, 0, 0, 0], [1, 1, 1, 1]], + "bbox_scores": [0, 0], + "max_individuals": {"bboxes": 2, "bbox_scores": 2}, + }, + ], +) +def test_trim_outputs(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = TrimOutputs(max_individuals=data["max_individuals"]) + context = {} + predictions = {"bboxes": np.array(data["bboxes"]), "bbox_scores": np.array(data["bbox_scores"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bboxes"].tolist()) + print(predictions["bbox_scores"].tolist()) + assert len(predictions["bboxes"]) == data["max_individuals"]["bboxes"] + assert len(predictions["bbox_scores"]) == data["max_individuals"]["bbox_scores"] + + +@pytest.mark.parametrize( + "data", + [ + { + "predictions": [[[0, 0, 0.95], [20, 30, 0.5]]], + "offsets": (0, 0), + "scales": (1, 1), + "rescaled": [[[0, 0, 0.95], [20, 30, 0.5]]], + }, + { + "predictions": [ + [[0, 0, 0.12], [10, 0, 0.5]], # individual 1 + [[1000, 500, 0.24], [50, 250, 0.6]], # individual 2 + ], + "offsets": (5, 7), + "scales": (0.2, 0.5), + "rescaled": [ + [[5, 7, 0.12], [7, 7, 0.5]], # individual 1 + [[205, 257, 0.24], [15, 132, 0.6]], # individual 2 + ], + }, + ], +) +def test_rescale_bottom_up(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bodyparts"], + mode=RescaleAndOffset.Mode.KEYPOINT, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bodyparts": np.array(data["predictions"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bodyparts"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bodyparts"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[222.0, 562.0, 721.0, 637.0]], + "offsets": (0, 0), + "scales": (1, 1), + "rescaled": [[222.0, 562.0, 721.0, 637.0]], + }, + { + "bboxes": [[386.71875, 219.53125, 281.640625, 248.828125]], + "offsets": (-768, 0), + "scales": (2.56, 2.56), + "rescaled": [[222.0, 562.0, 721.0, 637.0]], + }, + { + "bboxes": [ + [0, 0, 100, 100], + [5, 10, 100, 100], + [5, 10, 10, 20], + ], + "offsets": (3, 7), + "scales": (2, 0.5), + "rescaled": [ + [3, 7, 200, 50], + [13, 12, 200, 50], + [13, 12, 20, 10], + ], + }, + ], +) +def test_rescale_detector(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bboxes"], + mode=RescaleAndOffset.Mode.BBOX_XYWH, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bboxes": np.array(data["bboxes"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bboxes"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bboxes"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bodyparts": [ + [[3.1, 1, 0.8], [1, 0, 0.9]], # assembly 1 (x, y, score) + [[2.2, 1.6, 0.5], [3, 3, 0.4]], # assembly 2 (x, y, score) + ], + "id_heatmap": [ # id1, id2 score for each pixel + [[0.1, 0.1], [0.2, 0.1], [0.3, 0.1], [0.4, 0.1]], + [[0.1, 0.2], [0.2, 0.2], [0.3, 0.2], [0.4, 0.2]], + [[0.1, 0.3], [0.2, 0.3], [0.3, 0.3], [0.4, 0.3]], + [[0.1, 0.4], [0.2, 0.4], [0.3, 0.4], [0.4, 0.4]], + ], + "id_scores": [ # id1, id2 score for each bodypart + [[0.4, 0.2], [0.2, 0.1]], # assembly 1 (id_1 proba, id_2 proba) + [[0.3, 0.3], [0.4, 0.4]], # assembly 2 (id_1 proba, id_2 proba) + ], + }, + ], +) +def test_assign_id_scores(data): + p = PredictKeypointIdentities( + identity_key="keypoint_identity", + identity_map_key="identity_map", + pose_key="bodyparts", + keep_id_maps=True, + ) + bodyparts = np.array(data["bodyparts"]) + id_heatmap = np.array(data["id_heatmap"]) + expected_ids = np.array(data["id_scores"]) + print() + print(bodyparts.shape) + print(id_heatmap.shape) + print(expected_ids.shape) + predictions_in = {"bodyparts": bodyparts, "identity_map": id_heatmap} + predictions, _ = p(predictions_in, {}) + np.testing.assert_array_equal( + predictions["keypoint_identity"], + expected_ids, + ) + + +def test_prepare_backbone_features(): + p = PrepareBackboneFeatures(top_down=False) + + img_w, img_h = 256, 128 + features = np.zeros((1, img_h, img_w)) + + features[0, 15, 10] = 1 + features[0, 25, 20] = 2 + features[0, 35, 30] = 3 + + pose = np.array( + [ + [ + [10.1, 15.1, 0.95], + [20.1, 25.1, 0.95], + [29.9, 34.9, 0.95], + ], + ] + ) + + predictions = [dict(backbone=dict(features=features), bodypart=dict(poses=pose))] + context = dict(image_size=(img_w, img_h)) + predictions_out, context_out = p(predictions, context) + + assert len(predictions_out) == 1 + assert len(context_out) == 1 + preds = predictions_out[0] + + assert "backbone" in preds + assert "bodypart_features" in preds["backbone"] + bodypart_features = preds["backbone"]["bodypart_features"] + print(f"Bodypart features: {bodypart_features.shape}") + print(bodypart_features) + assert bodypart_features.shape == (1, 3, 1) + assert bodypart_features.reshape(-1).tolist() == [1, 2, 3] + + +def test_prepare_top_down_backbone_features(): + p = PrepareBackboneFeatures(top_down=True) + + img_w, img_h = 256, 256 + + features = np.zeros((2, 1, img_h, img_w)) + features[0, 0, 15, 10] = 1 + features[0, 0, 25, 20] = 2 + features[0, 0, 35, 30] = 3 + features[1, 0, 95, 10] = 11 + features[1, 0, 85, 20] = 12 + features[1, 0, 75, 30] = 13 + + pose_idv0 = np.array( + [ + [ + [10.1, 15.1, 0.95], + [20.1, 25.1, 0.95], + [29.9, 34.9, 0.95], + ], + ] + ) + pose_idv1 = np.array( + [ + [ + [10.1, 95.1, 0.95], + [20.1, 85.1, 0.95], + [29.9, 74.9, 0.95], + ], + ] + ) + + predictions = [ + dict(backbone=dict(features=features[0]), bodypart=dict(poses=pose_idv0)), + dict(backbone=dict(features=features[1]), bodypart=dict(poses=pose_idv1)), + ] + context = dict(top_down_crop_size=(img_w, img_h)) + predictions_out, context_out = p(predictions, context) + + assert len(predictions_out) == 2 + assert len(context_out) == 1 + for preds, expected in zip(predictions_out, [[1, 2, 3], [11, 12, 13]], strict=True): + assert "backbone" in preds + assert "bodypart_features" in preds["backbone"] + bodypart_features = preds["backbone"]["bodypart_features"] + print(f"Bodypart features: {bodypart_features.shape}") + print(bodypart_features) + assert bodypart_features.shape == (1, 3, 1) + assert bodypart_features.reshape(-1).tolist() == expected + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30], [40, 40, 50, 50]], + "bbox_scores": [0.1, 0.5, 0.9], + "threshold": 0.3, + "expected_bboxes": [[20, 20, 30, 30], [40, 40, 50, 50]], + "expected_scores": [0.5, 0.9], + }, + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30], [40, 40, 50, 50]], + "bbox_scores": [0.1, 0.2, 0.3], + "threshold": 0.5, + "expected_bboxes": [], + "expected_scores": [], + }, + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30]], + "bbox_scores": [0.3, 0.7], + "threshold": 0.3, + "expected_bboxes": [[0, 0, 10, 10], [20, 20, 30, 30]], + "expected_scores": [0.3, 0.7], + }, + { + "bboxes": [], + "bbox_scores": [], + "threshold": 0.5, + "expected_bboxes": [], + "expected_scores": [], + }, + ], +) +def test_remove_low_confidence_boxes(data): + """Tests that RemoveLowConfidenceBoxes filters boxes below threshold.""" + postprocessor = RemoveLowConfidenceBoxes(bbox_score_thresh=data["threshold"]) + context = {} + + # Handle empty input arrays with proper shape + if len(data["bboxes"]) == 0: + bboxes = np.empty((0, 4)) + else: + bboxes = np.array(data["bboxes"]) + + if len(data["bbox_scores"]) == 0: + bbox_scores = np.empty((0,)) + else: + bbox_scores = np.array(data["bbox_scores"]) + + predictions = { + "bboxes": bboxes, + "bbox_scores": bbox_scores, + } + predictions, context = postprocessor(predictions, context=context) + + # Handle empty expected arrays with proper shape + if len(data["expected_bboxes"]) == 0: + expected_bboxes = np.empty((0, 4)) + else: + expected_bboxes = np.array(data["expected_bboxes"]) + + if len(data["expected_scores"]) == 0: + expected_scores = np.empty((0,)) + else: + expected_scores = np.array(data["expected_scores"]) + + np.testing.assert_array_equal(predictions["bboxes"], expected_bboxes) + np.testing.assert_array_equal(predictions["bbox_scores"], expected_scores) diff --git a/tests/pose_estimation_pytorch/data/test_preprocessor.py b/tests/pose_estimation_pytorch/data/test_preprocessor.py new file mode 100644 index 0000000000..9a68d76fe7 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_preprocessor.py @@ -0,0 +1,158 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the pre-processors.""" + +import albumentations as A +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data.preprocessor import ( + AugmentImage, + build_conditional_top_down_preprocessor, +) +from deeplabcut.pose_estimation_pytorch.data.transforms import build_resize_transforms + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": (2, 4, 4), + "resize_transform": {"height": 5, "width": 4, "keep_ratio": True}, + "output_shape": (2, 4, 4), + "padded_shape": (5, 4, 4), # single offset as not a batch + "output_context": {"offsets": (0, 0), "scales": (1, 1)}, + }, + { + "image_shape": (1, 2, 4, 4), # as batch + "resize_transform": {"height": 10, "width": 4, "keep_ratio": True}, + "output_shape": (1, 2, 4, 4), + "padded_shape": (1, 10, 4, 4), + "output_context": {"offsets": [(0, 0)], "scales": [(1, 1)]}, + }, + { + "image_shape": (2, 4, 3), + "resize_transform": {"height": 10, "width": 8, "keep_ratio": True}, + "output_shape": (4, 8, 3), + "padded_shape": (10, 8, 3), + "output_context": {"offsets": (0, 0), "scales": (0.5, 0.5)}, + }, + ], +) +def test_augment_image_rescaling(data): + resize_transform = build_resize_transforms(data["resize_transform"]) + transform = A.Compose( + resize_transform, + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + preprocessor = AugmentImage(transform) + img = np.ones(data["image_shape"]) + transformed_image, context = preprocessor(img, context={}) + print() + print(transformed_image[:, :, 0]) # first channel + print(context) + assert np.sum(transformed_image) == np.sum(np.ones(data["output_shape"])) + assert context == data["output_context"] + assert transformed_image.shape == data["padded_shape"] + + +ctd_preprocessor = build_conditional_top_down_preprocessor( + color_mode="RGB", + transform=A.Compose( + build_resize_transforms({"height": 100, "width": 100, "keep_ratio": True}), + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ), + bbox_margin=0, + top_down_crop_size=(256, 256), +) + + +@pytest.mark.parametrize( + "data", + [ + # two well-defined individuals + { + "image_shape": (100, 100, 3), + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]])}, + "output_context": { + "cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]]), + "bboxes": [np.array([10, 10, 10, 10]), np.array([60, 60, 10, 10])], + "offsets": [(10, 10), (60, 60)], + "scales": [(0.1, 0.1), (0.1, 0.1)], + }, + }, + # one individual has 0 keypoints + { + "image_shape": (100, 100, 3), + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.0]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + # one individual has only 1 keypoints + { + "image_shape": (100, 100, 3), + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.9]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + # two individuals but one is low confidence + { + "image_shape": (100, 100, 3), + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.01], [70, 70, 0.01]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + ], +) +def test_conditional_top_down_preprocessor(data): + input_img = np.ones(data["image_shape"]) + + output_img, output_context = ctd_preprocessor(input_img, context=data["context"]) + + for context_key in ["cond_kpts", "bboxes", "offsets", "scales"]: + assert deep_equal(output_context[context_key], data["output_context"][context_key]) + + +def deep_equal(a, b): + if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): + return np.array_equal(a, b) + elif isinstance(a, list) and isinstance(b, list): + if len(a) != len(b): + return False + return all(deep_equal(x, y) for x, y in zip(a, b, strict=False)) + else: + return a == b diff --git a/tests/pose_estimation_pytorch/data/test_transforms.py b/tests/pose_estimation_pytorch/data/test_transforms.py new file mode 100644 index 0000000000..f85ce00ffe --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_transforms.py @@ -0,0 +1,302 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the custom transforms.""" + +import random + +import albumentations as A +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data import transforms + + +@pytest.mark.parametrize( + "height, width, image_shapes", + [ + (200, 200, [(300, 300, 3), (1000, 1000, 3), (1024, 1024, 1)]), + (512, 512, [(1024, 1024, 3), (128, 128, 4), (300, 300, 1)]), + (1024, 512, [(600, 300, 3), (4096, 2048, 3), (50, 25, 1)]), + (800, 1300, [(80, 130, 3), (1600, 2600, 4), (1200, 1950, 1)]), + ], +) +def test_dlc_resize_pad_good_aspect_ratio(height, width, image_shapes): + aug = transforms.KeepAspectRatioResize(width=width, height=height, mode="pad") + for image_shape in image_shapes: + fake_image = np.zeros(image_shape) + transformed = aug(image=fake_image, keypoints=[]) + assert transformed["image"].shape[:2] == (height, width) + assert transformed["image"].shape[2] == fake_image.shape[2] + + +@pytest.mark.parametrize( + "data", + [ + { + "height": 200, + "width": 200, + "in_shapes": [(100, 50, 3), (50, 400, 3)], + "out_shapes": [(200, 100, 3), (25, 200, 3)], + }, + { + "height": 128, + "width": 256, + "in_shapes": [(100, 100, 3), (512, 256, 3)], + "out_shapes": [(128, 128, 3), (128, 64, 3)], + }, + ], +) +def test_dlc_resize_pad_bad_aspect_ratio(data): + aug = transforms.KeepAspectRatioResize(width=data["width"], height=data["height"], mode="pad") + for in_shape, out_shape in zip(data["in_shapes"], data["out_shapes"], strict=False): + fake_image = np.zeros(in_shape) + transformed = aug(image=fake_image, keypoints=[]) + assert transformed["image"].shape == out_shape + + +@pytest.mark.parametrize( + "data", + [ + { + "height": 200, + "width": 200, + "in_shape": (100, 50, 3), + "out_shape": (200, 100, 3), + "in_keypoints": [(50.0, 50.0), (25.0, 10.0)], + "out_keypoints": [(100.0, 100.0), (50.0, 20.0)], + }, + { + "height": 512, + "width": 256, + "in_shape": (1024, 1024, 3), + "out_shape": (256, 256, 3), + "in_keypoints": [(512.0, 512.0), (100.0, 10.0)], + "out_keypoints": [(128.0, 128.0), (25.0, 2.5)], + }, + ], +) +def test_dlc_resize_pad_bad_aspect_ratio_with_keypoints(data): + aug = transforms.KeepAspectRatioResize(width=data["width"], height=data["height"], mode="pad") + transform = A.Compose( + [aug], + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + ) + fake_image = np.zeros(data["in_shape"]) + transformed = transform(image=fake_image, keypoints=data["in_keypoints"]) + assert transformed["image"].shape == data["out_shape"] + assert transformed["keypoints"] == data["out_keypoints"] + + +def test_coarse_dropout(): + transforms.CoarseDropout( + max_holes=10, + max_height=0.05, + min_height=0.01, + max_width=0.05, + min_width=0.01, + p=0.5, + ) + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=10.0, + shift_prob=0.0, + scale_factor=[0.1, 2.0], + scale_prob=0.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=1.0, + scale_factor=[1.0, 1.0], + scale_prob=1.0, + sampling="uniform", # truncnorm throws an error if delta is 0 + ), + }, + ], +) +def test_random_bbox_transform_does_not_modify_with_base_config(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + output = t( + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + bboxes_out = np.asarray(output["bboxes"]) + print("bboxes") + print(bboxes_out) + print() + np.testing.assert_array_almost_equal(bboxes, bboxes_out) + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.25, 0.5], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[1.0, 1.5], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.5, 1.25], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.5, 1.5], + scale_prob=0.5, + ), + }, + ], +) +def test_random_bbox_transform_scale(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + output = t( + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + + bboxes_out = np.asarray(output["bboxes"]) + scale_low, scale_high = data["transform_config"]["scale_factor"] + for bbox_in_wh, bbox_out_wh in zip(bboxes[:, 2:], bboxes_out[:, 2:], strict=False): + print("bbox_in_wh", bbox_in_wh) + w, h = bbox_in_wh[0].item(), bbox_in_wh[1].item() + w_low, w_high = w * scale_low, w * scale_high + h_low, h_high = h * scale_low, h * scale_high + print("(w, w_low, w_high)", w, w_low, w_high) + print("(h, h_low, h_high)", h, h_low, h_high) + assert w_low <= bbox_out_wh[0].item() <= w_high + assert h_low <= bbox_out_wh[1].item() <= h_high + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.1, + shift_prob=1.0, + scale_factor=[1.0, 1.0], + scale_prob=0.0, + ), + }, + ], +) +def test_random_bbox_transform_shift(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + output = t( + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + + bboxes_out = np.asarray(output["bboxes"]) + shift = data["transform_config"]["shift_factor"] + for bbox_in, bbox_out in zip(bboxes, bboxes_out, strict=False): + print("bbox_in", bbox_in) + x, y, w, h = bbox_in + x_out, y_out, w_out, h_out = bbox_out + max_shift_x, max_shift_y = w * shift, h * shift + assert x - max_shift_x <= x_out <= x + max_shift_x + assert y - max_shift_y <= y_out <= y + max_shift_y + + +def _set_random_seed(): + np.random.seed(0) + random.seed(0) + + +def _gen_random_bboxes( + gen: np.random.Generator, + num_bboxes: int, + w: int, + h: int, +) -> np.ndarray: + image_wh = np.array([w, h]) + bboxes = np.zeros((num_bboxes, 4)) + # sample x, y in the images + bboxes[:, :2] = image_wh * gen.random((num_bboxes, 2)) + # sample w, h with the space remaining + bboxes[:, 2:] = (image_wh - bboxes[:, :2]) * gen.random((num_bboxes, 2)) + + print() + print("Input bounding boxes") + print(bboxes) + return bboxes diff --git a/tests/pose_estimation_pytorch/data/test_utils.py b/tests/pose_estimation_pytorch/data/test_utils.py new file mode 100644 index 0000000000..1494e01787 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_utils.py @@ -0,0 +1,97 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests data utils.""" + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.data.utils as utils + + +@pytest.mark.parametrize( + "keypoints, expected_bboxes, params", + [ + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 10, 5], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [[-1, -1, 2], [3, 4, 2]], + [0, 0, 3, 4], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 5, 3], + dict(image_w=5, image_h=3, margin=0), + ), + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 5, 3], + dict(image_w=5, image_h=3, margin=10), + ), + ( + [[[0, 0, 2], [10, 5, 2]]], + [[0, 0, 10, 5]], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [4, 1, 6, 4], + [1, 3, 6, 9], + ], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [2, 0, 10, 7], + [0, 1, 9, 13], + ], + dict(image_w=1024, image_h=1024, margin=2), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [2, 0, 8, 7], + [0, 1, 9, 9], + ], + dict(image_w=10, image_h=10, margin=2), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 0], [2, 0, -1], [1, 12, 0]], + ], + [ + [2, 0, 8, 7], + [0, 0, 0, 0], + ], + dict(image_w=10, image_h=10, margin=2), + ), + ], +) +def test_bbox_from_keypoints(keypoints, expected_bboxes, params): + keypoints = np.asarray(keypoints, dtype=float) + bboxes = utils.bbox_from_keypoints(keypoints, **params) + expected_bboxes = np.asarray(expected_bboxes, dtype=float) + np.testing.assert_array_almost_equal(bboxes, expected_bboxes) diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py new file mode 100644 index 0000000000..7f418f4912 --- /dev/null +++ b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py @@ -0,0 +1,131 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the heatmap target generators (plateau and gaussian)""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( + HeatmapGaussianGenerator, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "dist_thresh": 3, + "num_heatmaps": 1, + "in_shape": (3, 3), + "out_shape": (3, 3), + "centers": [(1, 1)], + "expected_output": [ + [0.7788, 0.8825, 0.7788], + [0.8825, 1.0000, 0.8825], + [0.7788, 0.8825, 0.7788], + ], + }, + { + "dist_thresh": 3, + "num_heatmaps": 1, + "in_shape": (5, 5), + "out_shape": (5, 5), + "centers": [[1, 1], [2, 2]], + "expected_output": [ + [0.7788, 0.8825, 0.7788, 0.5353, 0.3679], + [0.8825, 1.0000, 0.8825, 0.7788, 0.5353], + [0.7788, 0.8825, 1.0000, 0.8825, 0.6065], + [0.5353, 0.7788, 0.8825, 0.7788, 0.5353], + [0.3679, 0.5353, 0.6065, 0.5353, 0.3679], + ], + }, + { + "dist_thresh": 1, + "num_heatmaps": 1, + "in_shape": (4, 4), + "out_shape": (4, 4), + "centers": [[1, 1]], + "expected_output": [ + [0.1054, 0.3247, 0.1054, 0.0036], + [0.3247, 1.0, 0.3247, 0.0111], + [0.1054, 0.3247, 0.1054, 0.0036], + [0.0036, 0.0111, 0.0036, 0.0001], + ], + }, + ], +) +def test_gaussian_heatmap_generation_single_keypoint(data): + dist_thresh = data["dist_thresh"] + generator = HeatmapGaussianGenerator( + num_heatmaps=data["num_heatmaps"], + pos_dist_thresh=dist_thresh, + heatmap_mode=HeatmapGaussianGenerator.Mode.KEYPOINT, + generate_locref=False, + ) + stride = data["in_shape"][0] / data["out_shape"][0] + outputs = torch.zeros((1, data["num_heatmaps"], *data["out_shape"])) + ann_shape = (1, len(data["centers"]), data["num_heatmaps"], 2) + annotations = { + "keypoints": torch.tensor(data["centers"]).reshape(ann_shape) # x, y + } + targets = generator(stride, {"heatmap": outputs}, annotations) + + print("Targets") + print(targets["heatmap"]["target"]) + print() + np.testing.assert_almost_equal( + targets["heatmap"]["target"].cpu().numpy().reshape(data["out_shape"]), + np.array(data["expected_output"]), + decimal=3, + ) + + +@pytest.mark.parametrize( + "batch_size, num_keypoints, image_size", + [(2, 2, (64, 64)), (1, 5, (48, 64)), (15, 50, (64, 48))], +) +def test_random_gaussian_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1): + # generate annotations + annotations = { + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) + } # batch size, num animals, num keypoints, 2 for x,y + + # model stride 1 + stride = 1 + + # generate predictions + predicted_heatmaps = {"heatmap": torch.zeros((batch_size, num_keypoints, *image_size))} + + # generate heatmap + generator = HeatmapGaussianGenerator( + num_heatmaps=num_keypoints, + pos_dist_thresh=17, + heatmap_mode=HeatmapGaussianGenerator.Mode.KEYPOINT, + generate_locref=False, + ) + targets = generator(stride, predicted_heatmaps, annotations) + target_heatmap = targets["heatmap"]["target"].reshape(batch_size, num_keypoints, image_size[0] * image_size[1]) + + # get coords of max value of the heatmap + gaus_max = torch.argmax(target_heatmap, dim=2) + + # get unraveled coords + x = gaus_max % image_size[1] + y = gaus_max // image_size[1] + + # get heatmap center tensor + predict_kp = torch.stack((x, y), dim=-1) + # Remove num_animals dimension - only one animal is supported + annotations["keypoints"] = torch.squeeze(annotations["keypoints"], dim=1) + + # compare heatmap center to annotation + assert torch.eq(annotations["keypoints"], predict_kp).all().item() diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py new file mode 100644 index 0000000000..d335fc5524 --- /dev/null +++ b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py @@ -0,0 +1,90 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the heatmap target generators (plateau and gaussian)""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( + HeatmapGenerator, + HeatmapPlateauGenerator, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "dist_thresh": 1, + "num_heatmaps": 1, + "in_shape": (3, 3), + "out_shape": (3, 3), + "centers": [(1, 1)], + "expected_output": [ + [0.0, 1.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 1.0, 0.0], + ], + }, + { + "dist_thresh": 2, + "num_heatmaps": 1, + "in_shape": (5, 5), + "out_shape": (5, 5), + "centers": [[1, 1], [2, 2]], + "expected_output": [ + [1.0, 1.0, 1.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + ], + }, + { + "dist_thresh": 2, + "num_heatmaps": 1, + "in_shape": (4, 4), + "out_shape": (4, 4), + "centers": [[1, 1]], + "expected_output": [ + [1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + ], + }, + ], +) +def test_plateau_heatmap_generation_single_keypoint(data): + dist_thresh = data["dist_thresh"] + generator = HeatmapPlateauGenerator( + num_heatmaps=data["num_heatmaps"], + pos_dist_thresh=dist_thresh, + heatmap_mode=HeatmapGenerator.Mode.KEYPOINT, + generate_locref=False, + ) + stride = data["in_shape"][0] / data["out_shape"][0] + outputs = torch.zeros((1, data["num_heatmaps"], *data["out_shape"])) + ann_shape = (1, len(data["centers"]), data["num_heatmaps"], 2) + annotations = { + "keypoints": torch.tensor(data["centers"]).reshape(ann_shape) # x, y + } + targets = generator(stride, {"heatmap": outputs}, annotations) + + print("Targets") + print(targets["heatmap"]["target"]) + print() + np.testing.assert_almost_equal( + targets["heatmap"]["target"].cpu().numpy().reshape(data["out_shape"]), + np.array(data["expected_output"]), + decimal=3, + ) diff --git a/tests/tests_modelzoo.py b/tests/pose_estimation_pytorch/modelzoo/test_download.py similarity index 91% rename from tests/tests_modelzoo.py rename to tests/pose_estimation_pytorch/modelzoo/test_download.py index 555c590307..06cc9857e1 100644 --- a/tests/tests_modelzoo.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_download.py @@ -4,12 +4,13 @@ # https://github.com/DeepLabCut/DeepLabCut # # Please see AUTHORS for contributors. -# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS # # Licensed under GNU Lesser General Public License v3.0 # -import dlclibrary import os + +import dlclibrary import pytest from dlclibrary.dlcmodelzoo.modelzoo_download import MODELOPTIONS @@ -29,7 +30,7 @@ def test_download_huggingface_wrong_model(): dlclibrary.download_huggingface_model("wrong_model_name") -@pytest.mark.skip +@pytest.mark.skip(reason="slow") @pytest.mark.parametrize("model", MODELOPTIONS) def test_download_all_models(tmp_path_factory, model): test_download_huggingface_model(tmp_path_factory, model) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py new file mode 100644 index 0000000000..e7608625fa --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py @@ -0,0 +1,198 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import pathlib +import socket +from types import SimpleNamespace + +import numpy as np +import pandas as pd +import pytest + +fmpose3d = pytest.importorskip("fmpose3d", reason="fmpose3d not installed") +pytestmark = pytest.mark.fmpose3d + +# DLC fmpose_3d modules import fmpose3d; load only after importorskip above. +from deeplabcut.pose_estimation_pytorch.modelzoo.fmpose_3d import inference as fmp_inf # noqa: E402 +from deeplabcut.pose_estimation_pytorch.modelzoo.fmpose_3d.fmpose3d import ( # noqa: E402 + get_fmpose3d_inference_api, +) + + +def _has_network(host="huggingface.co", port=443, timeout=3) -> bool: + """Return True if we can reach *host* (used to download model weights).""" + try: + socket.create_connection((host, port), timeout=timeout).close() + return True + except OSError: + return False + + +requires_network = pytest.mark.skipif( + not _has_network(), + reason="No network connection (needed to download model weights)", +) + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +_EXAMPLE_IMAGE = ( + _REPO_ROOT / "examples" / "Reaching-Mackenzie-2018-08-30" / "labeled-data" / "reachingvideo1" / "img005.png" +) + + +# --------------------------------------------------------------------------- +# Lightweight: verifies the API object is constructed correctly +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("model_type", ["fmpose3d_humans", "fmpose3d_animals"]) +@pytest.mark.unittest +def test_api_init(model_type): + api = get_fmpose3d_inference_api(model_type, device="cpu") + assert api is not None + assert hasattr(api, "prepare_2d") + assert hasattr(api, "pose_3d") + assert hasattr(api, "predict") + + +# --------------------------------------------------------------------------- +# Integration: downloads weights and runs inference (needs network) +# --------------------------------------------------------------------------- +@requires_network +@pytest.mark.functional +def test_prepare_2d_and_pose_3d(): + """2D detection followed by 3D lifting on a real image.""" + api = get_fmpose3d_inference_api("fmpose3d_animals", device="cpu") + + result_2d = api.prepare_2d(source=str(_EXAMPLE_IMAGE)) + assert isinstance(result_2d.keypoints, np.ndarray) + assert result_2d.keypoints.shape[-1] == 2 + + keypoints_3d = api.pose_3d( + keypoints_2d=result_2d.keypoints, + image_size=result_2d.image_size, + ) + assert isinstance(keypoints_3d.poses_3d, np.ndarray) + assert keypoints_3d.poses_3d.shape[-1] == 3 + + +@requires_network +@pytest.mark.functional +def test_predict_end_to_end(): + """Full pipeline (2D -> 3D) in a single call.""" + api = get_fmpose3d_inference_api("fmpose3d_animals", device="cpu") + predictions_3d = api.predict(source=str(_EXAMPLE_IMAGE)) + + assert isinstance(predictions_3d.poses_3d, np.ndarray) + assert predictions_3d.poses_3d.shape[-1] == 3 + + +@pytest.mark.unittest +def test_pose2d_to_dlc_predictions_shapes(): + pose_2d = SimpleNamespace( + keypoints=np.random.rand(2, 3, 4, 2).astype(np.float32), + scores=np.random.rand(2, 3, 4).astype(np.float32), + ) + preds = fmp_inf._pose2d_to_dlc_predictions( + pose_2d=pose_2d, + max_individuals=1, + num_bodyparts=4, + ) + + assert len(preds) == 3 + assert preds[0]["bodyparts"].shape == (1, 4, 3) + np.testing.assert_allclose(preds[0]["bodyparts"][0, :, :2], pose_2d.keypoints[0, 0]) + + +@pytest.mark.unittest +def test_poses3d_to_dataframe_layout(): + scorer = "DLC_test" + bodyparts = ["bp1", "bp2", "bp3"] + columns_2d = pd.MultiIndex.from_product( + [[scorer], ["individual1"], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + df_2d = pd.DataFrame(np.zeros((2, len(columns_2d))), columns=columns_2d) + + poses_3d = [ + np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]), + np.array([[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]]), + ] + df_3d = fmp_inf._poses3d_to_dataframe(poses_3d, df_2d, f"{scorer}_3d") + + assert df_3d.columns.names == ["scorer", "bodyparts", "coords"] + assert set(df_3d.columns.get_level_values("coords")) == {"x", "y", "z"} + assert df_3d.loc[0, (f"{scorer}_3d", "bp1", "x")] == 1.0 + assert df_3d.loc[1, (f"{scorer}_3d", "bp3", "z")] == 18.0 + + +@pytest.mark.functional +def test_video_inference_fmpose3d_include_3d_return(tmp_path, monkeypatch): + frames = [np.zeros((8, 8, 3), dtype=np.uint8) for _ in range(2)] + + class FakeVideoIterator: + def __init__(self, _path, cropping=None): + self.dimensions = (8, 8) + self.fps = 30 + self._frames = frames + + def __iter__(self): + return iter(self._frames) + + class FakeAPI: + def prepare_2d(self, source): + n_frames = source.shape[0] + return SimpleNamespace( + keypoints=np.zeros((1, n_frames, 26, 2), dtype=np.float32), + scores=np.ones((1, n_frames, 26), dtype=np.float32), + image_size=(8, 8), + ) + + def pose_3d(self, keypoints_2d, image_size): + n_frames = keypoints_2d.shape[1] + return SimpleNamespace( + poses_3d=np.zeros((n_frames, 26, 3), dtype=np.float32), + ) + + def _fake_create_df_from_prediction(predictions, dlc_scorer, multi_animal, model_cfg, output_path, output_prefix): + bodyparts = model_cfg["metadata"]["bodyparts"] + individuals = model_cfg["metadata"]["individuals"] + columns = pd.MultiIndex.from_product( + [[dlc_scorer], individuals, bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + return pd.DataFrame(np.zeros((len(predictions), len(columns))), columns=columns) + + monkeypatch.setattr(fmp_inf, "VideoIterator", FakeVideoIterator) + monkeypatch.setattr( + fmp_inf, + "get_fmpose3d_inference_api", + lambda model_type, device: FakeAPI(), + ) + monkeypatch.setattr(fmp_inf, "create_df_from_prediction", _fake_create_df_from_prediction) + monkeypatch.setattr( + fmp_inf, + "get_superanimal_colormaps", + lambda: { + "superanimal_quadruped": "viridis", + "superanimal_humanbody": "viridis", + }, + ) + + result = fmp_inf._video_inference_fmpose3d( + video_paths=[str(tmp_path / "dummy.mp4")], + model_name="fmpose3d_animals", + dest_folder=tmp_path, + create_labeled_video=False, + include_3d_in_return=True, + ) + + payload = result[str(tmp_path / "dummy.mp4")] + assert "df_2d" in payload + assert "df_3d" in payload + assert isinstance(payload["df_3d"], pd.DataFrame) + assert (tmp_path / "dummy_DLC_fmpose3d_animals_3d.h5").exists() diff --git a/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py new file mode 100644 index 0000000000..50e58e234d --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py @@ -0,0 +1,172 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +from types import SimpleNamespace + +import pytest + +import deeplabcut.pose_estimation_pytorch.modelzoo.inference_helpers as helpers + + +def _dummy_cfg(method: str = "TD") -> dict: + return { + "method": method, + "metadata": {"bodyparts": ["nose"], "unique_bodyparts": []}, + } + + +def test_create_superanimal_inference_runners_uses_custom_config_path(monkeypatch): + cfg = _dummy_cfg("TD") + read_calls = [] + + def fake_read_config_as_dict(path): + read_calls.append(path) + return cfg + + monkeypatch.setattr(helpers, "read_config_as_dict", fake_read_config_as_dict) + monkeypatch.setattr(helpers, "update_config", lambda config, max_individuals, device: config) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", "det_runner"), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path="det.pt", + ), + ) + + pose_runner, detector_runner, model_cfg = helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config="/tmp/custom_model_cfg.yaml", + ) + + assert read_calls == ["/tmp/custom_model_cfg.yaml"] + assert pose_runner == "pose_runner" + assert detector_runner == "det_runner" + assert model_cfg is cfg + + +def test_create_superanimal_inference_runners_uses_deepcopy_for_custom_dict(monkeypatch): + custom_cfg = _dummy_cfg("TD") + monkeypatch.setattr( + helpers, + "read_config_as_dict", + lambda path: pytest.fail("read_config_as_dict should not be called for dict input"), + ) + + def fake_update_config(config, max_individuals, device): + # Mutate nested structure; caller-owned dict should stay unchanged. + config["metadata"]["bodyparts"].append("tail") + return config + + monkeypatch.setattr(helpers, "update_config", fake_update_config) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", None), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path=None, + ), + ) + + _, _, model_cfg = helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name=None, + customized_model_config=custom_cfg, + ) + + assert custom_cfg["metadata"]["bodyparts"] == ["nose"] + assert model_cfg["metadata"]["bodyparts"] == ["nose", "tail"] + + +@pytest.mark.parametrize("input_device", ["auto", None]) +def test_create_superanimal_inference_runners_auto_device_selection(monkeypatch, input_device): + cfg = _dummy_cfg("TD") + captured = {} + + monkeypatch.setattr(helpers, "read_config_as_dict", lambda path: cfg) + + def fake_update_config(config, max_individuals, device): + captured["device"] = device + return config + + monkeypatch.setattr(helpers, "update_config", fake_update_config) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", "det_runner"), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path="det.pt", + ), + ) + + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config="/tmp/custom_model_cfg.yaml", + device=input_device, + ) + assert captured["device"] == "auto" + + +def test_create_superanimal_inference_runners_raises_for_fmpose3d(): + with pytest.raises(NotImplementedError, match="FMPose3D"): + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="FMPose3D_resnet", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config=_dummy_cfg("TD"), + ) + + +def test_create_superanimal_inference_runners_propagates_unsupported_dataset_error( + monkeypatch, +): + monkeypatch.setattr( + helpers, + "load_super_animal_config", + lambda **kwargs: (_ for _ in ()).throw(ValueError("Unsupported dataset for model zoo config")), + ) + + with pytest.raises(ValueError, match="Unsupported dataset"): + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_unknown", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config=None, + ) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py new file mode 100644 index 0000000000..fab8e2fb3f --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py @@ -0,0 +1,31 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import dlclibrary +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.modelzoo import get_super_animal_snapshot_path + + +@pytest.mark.skip(reason="require-models") +def test_load_superanimal_models_weights_only(): + super_animal_names = dlclibrary.get_available_datasets() + for super_animal in super_animal_names: + print(f"\nTesting {super_animal}") + for detector in dlclibrary.get_available_detectors(super_animal): + print(super_animal, detector) + path = get_super_animal_snapshot_path(super_animal, detector) + _snapshot = torch.load(path, map_location="cpu", weights_only=True) + + for pose_model in dlclibrary.get_available_models(super_animal): + print(super_animal, pose_model) + path = get_super_animal_snapshot_path(super_animal, pose_model) + _snapshot = torch.load(path, map_location="cpu", weights_only=True) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py new file mode 100644 index 0000000000..571dc9f5cd --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py @@ -0,0 +1,35 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +import pytest + +import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo + +# TODO: make a proper test incl. human model, bird model and that skips the require... at least once per week. + + +@pytest.mark.parametrize("super_animal", ["superanimal_quadruped", "superanimal_topviewmouse"]) +@pytest.mark.parametrize("model_name", ["hrnet_w32"]) +@pytest.mark.parametrize("detector_name", [None, "fasterrcnn_resnet50_fpn_v2"]) +def test_get_config_model_paths(super_animal, model_name, detector_name): + model_config = modelzoo.load_super_animal_config( + super_animal=super_animal, + model_name=model_name, + detector_name=detector_name, + ) + + assert isinstance(model_config, dict) + if detector_name is None: + assert model_config["method"].lower() == "bu" + assert "detector" not in model_config + else: + assert model_config["method"].lower() == "td" + assert "detector" in model_config diff --git a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py new file mode 100644 index 0000000000..34a210b462 --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py @@ -0,0 +1,69 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +import numpy as np +import pytest + +from deeplabcut.modelzoo.webapp.inference import SuperanimalPyTorchInference +from deeplabcut.utils import auxiliaryfunctions + +# TODO: make a proper test incl. human model, bird model and that skips the require... at least once per week. + + +@pytest.mark.parametrize("max_individuals", [1, 3]) +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse"]) +@pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) +def test_class_init(project_name, pose_model_type, max_individuals): + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=max_individuals) + + assert isinstance(inference_pipeline.config, dict) + assert inference_pipeline.config["metadata"]["bodyparts"] + assert len(inference_pipeline.config["metadata"]["bodyparts"]) > 0 + + +@pytest.mark.skip(reason="require-models") +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse"]) +@pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) +def test_runner_init(project_name, pose_model_type): + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=1) + weight_folder = f"{auxiliaryfunctions.get_deeplabcut_path()}/modelzoo/checkpoints" + snapshot_path = f"{weight_folder}/{project_name}_{pose_model_type}.pth" + detector_path = f"{weight_folder}/{project_name}_fasterrcnn.pt" + + inference_pipeline.initialize_models(snapshot_path, detector_path) + + assert inference_pipeline.models.pose_runner + assert inference_pipeline.models.detector_runner + + +@pytest.mark.skip(reason="require-models") +@pytest.mark.parametrize("max_individuals", [10, 4, 1]) +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse", "superanimal_humanbody"]) +@pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) +def test_predict(project_name, pose_model_type, max_individuals): + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=max_individuals) + image_path = "img0001.png" + weight_folder = f"{auxiliaryfunctions.get_deeplabcut_path()}/modelzoo/checkpoints" + snapshot_path = f"{weight_folder}/{project_name}_{pose_model_type}.pth" + detector_path = f"{weight_folder}/{project_name}_fasterrcnn.pt" + + inference_pipeline.initialize_models(snapshot_path, detector_path) + frame = {image_path: np.random.rand(100, 100, 3)} + response = inference_pipeline.predict(frame) + + assert isinstance(response, dict) + assert response["joint_names"] == inference_pipeline.config["bodyparts"] + assert response["predictions"][0]["markers"].shape == ( + max_individuals, + len(inference_pipeline.config["bodyparts"]), + 3, + ) + assert response["predictions"][0]["image_path"] == image_path diff --git a/tests/pose_estimation_pytorch/other/test_api_utils.py b/tests/pose_estimation_pytorch/other/test_api_utils.py new file mode 100644 index 0000000000..0efbfbb52c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_api_utils.py @@ -0,0 +1,94 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import random + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.data.transforms as transforms + +transform_dicts = [ + {"auto_padding": {"pad_height_divisor": 64, "pad_width_divisor": 27}}, + {"resize": {"height": 512, "width": 256, "keep_ration": True}}, + { + "covering": True, + "gaussian_noise": 12.75, + "hist_eq": True, + "motion_blur": True, + "normalize_images": True, + "rotation": 30, + "scale_jitter": [0.5, 1.25], + "auto_padding": {"pad_width_divisor": 64, "pad_height_divisor": 27}, + }, + { + "covering": True, + "gaussian_noise": 100, + "hist_eq": True, + "motion_blur": True, + "normalize_images": True, + "rotation": 180, + "scale_jitter": [0.03, 20], + "auto_padding": {"pad_width_divisor": 64, "pad_height_divisor": 27}, + }, +] + + +def _get_random_params(transform_idx): + return ( + transform_dicts[transform_idx], + (random.randint(100, 1000), random.randint(100, 1000)), + random.randint(1, 100), + random.randint(1, 100), + ) + + +@pytest.mark.parametrize( + "transform_dict, size_image, num_keypoints, num_animals", + [_get_random_params(i) for i in range(4)], +) +def test_build_transforms(transform_dict, size_image, num_keypoints, num_animals): + transform_bbox_aug = transforms.build_transforms(transform_dict) + w, h = size_image + for i in range(10): + test_image = np.random.randint(0, 255, (h, w, 3), dtype=np.uint8) + bboxes = np.random.randint(0, min(w - 1, h - 1), (num_animals, 4)) + bboxes[:, 2] = w - bboxes[:, 0] + bboxes[:, 3] = h - bboxes[:, 1] + keypoints = np.random.randint(0, min(w, h), (num_keypoints, 2)) + + with pytest.raises(ValueError) as _err_info: + _ = transform_bbox_aug(image=test_image) + _ = transform_bbox_aug(image=test_image, bboxes=bboxes.copy()) + _ = transform_bbox_aug(image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy()) + + transformed_with_bbox = transform_bbox_aug( + image=test_image, + keypoints=keypoints.copy(), + bboxes=bboxes.copy(), + bbox_labels=np.arange(num_animals), + class_labels=[0 for _ in range(len(keypoints))], + ) + + if "resize" in transform_dict.keys(): + assert transformed_with_bbox["image"].shape[:2] == ( + transform_dict["resize"]["height"], + transform_dict["resize"]["width"], + ) + + if "auto_padding" in transform_dict.keys(): + modh, modw = ( + transform_dict["auto_padding"]["pad_height_divisor"], + transform_dict["auto_padding"]["pad_width_divisor"], + ) + assert transformed_with_bbox["image"].shape[0] % modh == 0 + assert transformed_with_bbox["image"].shape[1] % modw == 0 + + assert len(transformed_with_bbox["keypoints"]) == len(keypoints) diff --git a/tests/pose_estimation_pytorch/other/test_configs/config.yaml b/tests/pose_estimation_pytorch/other/test_configs/config.yaml new file mode 100644 index 0000000000..15ad6f4678 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/config.yaml @@ -0,0 +1,106 @@ + # Project definitions (do not edit) +Task: openfield +scorer: Pranav +date: Aug20 +multianimalproject: false +identity: + + # Project path (change when moving around) +project_path: /home/quentin/datasets/Openfield_pytorch + + # Annotation data set configuration (and individual video cropping parameters) +video_sets: + /Data/openfield-Pranav-2018-08-20/videos/m1s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m1s2.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m2s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m3s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m3s2.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m4s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m5s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m6s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m6s2.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s2.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s3.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m8s1.mp4: + crop: 0, 800, 0, 800 + + /Users/mwmathis/Downloads/ARCricket1.avi: + crop: 0, 720, 0, 540 +bodyparts: +- snout +- leftear +- rightear +- tailbase + +flipped_keypoints: +- 0 +- 2 +- 1 +- 3 + + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement +start: 0 +stop: 1 +numframes2pick: 20 + + # Plotting configuration +skeleton: [] +skeleton_color: black +pcutoff: 0.4 +dotsize: 8 +alphavalue: 0.7 +colormap: jet + + # Training,Evaluation and Analysis configuration +TrainingFraction: +- 0.95 +iteration: 1 +default_net_type: resnet_50 +default_augmenter: default +snapshotindex: -1 +batch_size: 1 + + # Cropping Parameters (for analysis and outlier frame detection) +cropping: false + #if cropping is true for analysis, then set the values here: +x1: 0 +x2: 640 +y1: 277 +y2: 624 + + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +- 50 +- 50 +move2corner: true +croppedtraining: diff --git a/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml b/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml new file mode 100644 index 0000000000..ec41492bd4 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml @@ -0,0 +1,115 @@ + # Project definitions (do not edit) +Task: +scorer: +date: +multianimalproject: +identity: + + # Project path (change when moving around) +project_path: /home/quentin/datasets/Openfield_pytorch/dlc-models/iteration-1/openfieldAug20-trainset95shuffle1/train + + # Annotation data set configuration (and individual video cropping parameters) +video_sets: +bodyparts: + + # Fraction of video to start/stop when extracting frames for labeling/refinement +start: +stop: +numframes2pick: + + # Plotting configuration +skeleton: [] +skeleton_color: black +pcutoff: +dotsize: +alphavalue: +colormap: + + # Training,Evaluation and Analysis configuration +TrainingFraction: +iteration: +default_net_type: +default_augmenter: +snapshotindex: +batch_size: 1 + + # Cropping Parameters (for analysis and outlier frame detection) +cropping: + #if cropping is true for analysis, then set the values here: +x1: +x2: +y1: +y2: + + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +move2corner: +all_joints: +- - 0 +- - 1 +- - 2 +- - 3 +all_joints_names: +- snout +- leftear +- rightear +- tailbase +alpha_r: 0.02 +apply_prob: 0.5 +contrast: + clahe: true + claheratio: 0.1 + histeq: true + histeqratio: 0.1 +convolution: + edge: false + emboss: + alpha: + - 0.0 + - 1.0 + strength: + - 0.5 + - 1.5 + embossratio: 0.1 + sharpen: false + sharpenratio: 0.3 +cropratio: 0.4 +dataset: training-datasets/iteration-1/UnaugmentedDataSet_openfieldAug20/openfield_Pranav95shuffle1.mat +dataset_type: default +decay_steps: 30000 +display_iters: 1000 +global_scale: 0.8 +init_weights: /home/quentin/miniconda/envs/DEEPLABCUT/lib/python3.8/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt +intermediate_supervision: false +intermediate_supervision_layer: 12 +location_refinement: true +locref_huber_loss: true +locref_loss_weight: 0.05 +locref_stdev: 7.2801 +lr_init: 0.0005 +max_input_size: 1500 +metadataset: training-datasets/iteration-1/UnaugmentedDataSet_openfieldAug20/Documentation_data-openfield_95shuffle1.pickle +min_input_size: 64 +mirror: false +multi_stage: false +multi_step: +- - 0.005 + - 10000 +- - 0.02 + - 430000 +- - 0.002 + - 730000 +- - 0.001 + - 1030000 +net_type: resnet_50 +num_joints: 4 +pairwise_huber_loss: false +pairwise_predict: false +partaffinityfield_predict: false +pos_dist_thresh: 17 +rotation: 25 +rotratio: 0.4 +save_iters: 50000 +scale_jitter_lo: 0.5 +scale_jitter_up: 1.25 +scmap_type: plateau diff --git a/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml b/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml new file mode 100644 index 0000000000..0be2ca0ed8 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml @@ -0,0 +1,45 @@ +project_root: /home/quentin/datasets/Openfield_pytorch +pose_cfg_path: /home/quentin/datasets/Openfield_pytorch/dlc-models/iteration-1/openfieldAug20-trainset95shuffle1/train/pose_cfg.yaml +cfg_path: /home/quentin/datasets/Openfield_pytorch/config.yaml + +seed: 42 +device: 'cuda:2' #needs to be updated dynamically; some users might have CPUs +model: + backbone: + type: 'ResNet' + pretrained: 'https://download.pytorch.org/models/resnet50-19c8e357.pth' + heatmap_head: + type: 'SimpleHead' + channels: [ 2048, 1024, 4 ] + kernel_size: [ 2, 2 ] + strides: [ 2, 2 ] + locref_head: + type: 'SimpleHead' + channels: [ 2048, 1024, 8 ] + kernel_size: [ 2, 2 ] + strides: [ 2, 2 ] + pose_model: + stride: 8 + heatmap_type: 'plateau' +optimizer: + type: 'SGD' + params: + lr: 0.005 +scheduler: + type: "LRListScheduler" + params: + milestones : [10, 430] + lr_list : [[0.02], [0.002]] +criterion: + type: 'PoseLoss' + loss_weight_locref: 0.1 + locref_huber_loss: True +#logger: +# type: 'WandbLogger' +# project_name: 'deeplabcut' +# run_name: 'tmp' +solver: + type: 'BottomUpSingleAnimalSolver' +pos_dist_thresh : 17 +batch_size: 1 +epochs: 600 diff --git a/tests/pose_estimation_pytorch/other/test_custom_transforms.py b/tests/pose_estimation_pytorch/other/test_custom_transforms.py new file mode 100644 index 0000000000..28ddb2ffc7 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_custom_transforms.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data import transforms + + +@pytest.mark.parametrize("width, height", [(200, 200), (300, 300), (400, 400)]) +def test_keypoint_aware_cropping(width, height): + fake_image = np.empty((600, 600, 3)) + fake_keypoints = [(i * 100, i * 100, 0, 0) for i in range(1, 6)] + aug = transforms.KeypointAwareCrop(width=width, height=height, crop_sampling="density") + transformed = aug(image=fake_image, keypoints=fake_keypoints) + assert transformed["image"].shape[:2] == (height, width) + # Ensure at least a keypoint is visible in each crop + assert len(transformed["keypoints"]) + + +def test_grayscale(): + fake_image = np.ones((600, 600, 3)) + fake_image *= np.random.uniform(0, 255, size=fake_image.shape) + fake_image = fake_image.astype(np.uint8) + gray = transforms.Grayscale(alpha=1, p=1) + aug_image = gray(image=fake_image)["image"] + assert aug_image.shape == fake_image.shape + + gray = transforms.Grayscale(alpha=0, p=1) + aug_image = gray(image=fake_image)["image"] + assert np.allclose(fake_image, aug_image) + + with pytest.warns(UserWarning, match="clipped"): + gray = transforms.Grayscale(alpha=1.5) + assert gray.alpha == 1 + + +def test_coarse_dropout(): + fake_image = np.ones((300, 300, 3)) + fake_image *= np.random.uniform(0, 255, size=fake_image.shape) + fake_image = fake_image.astype(np.uint8) + cd = transforms.CoarseDropout(max_height=0.9999, max_width=0.9999, p=1) + kpts = np.random.rand(10, 2) * 298 + 1 + aug_kpts = cd(image=fake_image, keypoints=kpts)["keypoints"] + assert len(aug_kpts) == kpts.shape[0] + assert np.isnan([c for kpt in aug_kpts for c in kpt]).all() diff --git a/tests/pose_estimation_pytorch/other/test_data_helper.py b/tests/pose_estimation_pytorch/other/test_data_helper.py new file mode 100644 index 0000000000..73db76316d --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_data_helper.py @@ -0,0 +1,94 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +import os +from unittest.mock import Mock, patch +from zipfile import Path + +import numpy as np +import pytest + +from deeplabcut.generate_training_dataset import create_training_dataset +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader +from deeplabcut.pose_estimation_pytorch.data.utils import merge_list_of_dicts + + +def mock_aux() -> Mock: + aux_functions = Mock() + aux_functions.read_plainconfig = Mock() + aux_functions.read_plainconfig.return_value = {} + return aux_functions + + +@patch("deeplabcut.pose_estimation_pytorch.data.base.auxiliaryfunctions", mock_aux()) +def _get_loader(project_root): + if not (Path(project_root) / "training-datasets").exists(): + create_training_dataset(config=str(Path(project_root) / "config.yaml")) + return DLCLoader(Path(project_root) / "config.yaml", shuffle=1) + + +@pytest.mark.skip +@pytest.mark.parametrize("repo_path", ["/home/anastasiia/DLCdev"]) +def test_propertymeta_project(repo_path): + project_root = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + dlc_loader = _get_loader(project_root) + + for prop in dlc_loader.properties: + print(prop, getattr(dlc_loader, prop)) + + +@pytest.mark.skip +@pytest.mark.parametrize( + "repo_path, mode", + [("/home/anastasiia/DLCdev", "train"), ("/home/anastasiia/DLCdev", "test")], +) +def test_propertymeta_dataset(repo_path, mode): + repo_path = "/home/anastasiia/DLCdev" + mode = "train" + project_root = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + dlc_loader = _get_loader(project_root) + dataset = dlc_loader.create_dataset(transform=None, mode=mode) + + for prop in dataset.properties: + print(prop, getattr(dataset, prop)) + + +@pytest.mark.parametrize( + "list_dicts, keys_to_include", + [ + ([{"a": 1, "b": 2}, {"a": 3, "b": 4}], ["a"]), + ( + [ + *[ + { + "keypoints": np.random.randn(27, 3), + "images": np.random.randn(256, 192), + } + ] + * 10 + ], + [*["keypoints", "images"] * 10], + ), + ], +) +def test_merge_list_of_dicts(list_dicts, keys_to_include): + result_dict = merge_list_of_dicts(list_dicts, keys_to_include) + expected_result_dict = {} + for dictionary in list_dicts: + for key in dictionary: + if key not in keys_to_include: + continue + else: + if key not in expected_result_dict: + expected_result_dict[key] = [] + expected_result_dict[key].append(dictionary[key]) + assert result_dict == expected_result_dict diff --git a/tests/pose_estimation_pytorch/other/test_dataset.py b/tests/pose_estimation_pytorch/other/test_dataset.py new file mode 100644 index 0000000000..221ec64f19 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_dataset.py @@ -0,0 +1,186 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import os +import random +from pathlib import Path +from unittest.mock import Mock, patch + +import albumentations as A +import pytest +from torch.utils.data import DataLoader + +import deeplabcut.pose_estimation_pytorch as dlc +import deeplabcut.utils.auxiliaryfunctions as dlc_auxfun +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset import create_training_dataset + + +def mock_config() -> Mock: + aux_functions = Mock() + aux_functions.read_config_as_dict = Mock() + aux_functions.read_config_as_dict.return_value = { + "data": {"train": {}, "inference": {}}, + "metadata": { + "project_path": "", + "pose_config_path": "", + "bodyparts": ["snout", "leftear", "rightear", "tailbase"], + "unique_bodyparts": [], + "individuals": ["animal"], + "with_identity": False, + }, + "method": "bu", + } + return aux_functions + + +@patch("deeplabcut.pose_estimation_pytorch.data.base.config", mock_config()) +def _get_dataset(path, transform, mode="train"): + project_root = Path(path) + if not (project_root / "training-datasets").exists(): + print(str(project_root / "config.yaml")) + create_training_dataset( + config=str(project_root / "config.yaml"), + net_type="resnet_50", + engine=Engine.PYTORCH, + ) + + loader = dlc.DLCLoader(Path(project_root) / "config.yaml", shuffle=1) + dataset = loader.create_dataset(transform=transform, mode=mode) + return dataset + + +def _get_openfield_dataset(transform=None): + dlc_path = dlc_auxfun.get_deeplabcut_path() + repo_path = os.path.dirname(dlc_path) + openfield_path = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + + return _get_dataset(openfield_path, transform=transform) + + +key_set = { + "offsets", + "path", + "scales", + "image", + "original_size", + "annotations", + "image_id", + "context", +} +anno_key_set = { + "keypoints", + "keypoints_unique", + "with_center_keypoints", + "area", + "boxes", + "is_crowd", + "labels", + "individual_ids", +} + + +@pytest.mark.parametrize("batch_size", [1, 2, random.randint(2, 20)]) +def test_iter_all_dataset_no_transform(batch_size): + if batch_size > 1: # if batched, all images need to be the same size + transform = A.Compose( + [A.Resize(512, 512)], + keypoint_params=A.KeypointParams(format="xy"), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + else: + transform = A.Compose( + [A.Normalize()], + keypoint_params=A.KeypointParams(format="xy"), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + dataset = _get_openfield_dataset(transform=transform) + dataloader = DataLoader(dataset, batch_size=batch_size) + max_num_animals = dataset.parameters.max_num_animals + num_keypoints = dataset.parameters.num_joints + for i, item in enumerate(dataloader): + is_last_batch = i == (len(dataloader) - 1) + assert set(item.keys()) == key_set, ( + f"the key returned don't match the required ones: {item.keys()} != {key_set}" + ) + + anno = item["annotations"] + assert set(anno.keys()) == anno_key_set, "the annotation keys returned don't match the required ones" + + assert (len(item["image"].shape) == 4) and ((item["image"].shape[:2] == (batch_size, 3)) or is_last_batch), ( + "image shape is not (batch_size, 3, h, w)" + ) + + b, _, h, w = item["image"].shape + kpts, bboxes = anno["keypoints"], anno["boxes"] + assert kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) or is_last_batch, ( + "keypoints have the wrong shape" + ) + assert bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch, "boxes have the wrong shape" + assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ((bboxes[:, :, 1] + bboxes[:, :, 3]) <= h).all(), ( + "boxes don't seem to be un the format (x, y, w, h)" + ) + + +def _generate_random_test_values_aug(min_exa): + batch_size = random.randint(1, 20) + x_size = random.randint(50, 600) + y_size = random.randint(50, 600) + exaggeration = random.randint(min_exa, 99) + + return batch_size, x_size, y_size, exaggeration + + +@pytest.mark.parametrize( + "batch_size, x_size, y_size, exaggeration", + [ + (1, 512, 512, 1), + _generate_random_test_values_aug(1), + _generate_random_test_values_aug(50), + ], +) +def test_iter_all_augmented_dataset(batch_size, x_size, y_size, exaggeration): + transform = A.Compose( + [ + A.Affine( + scale=(1 - exaggeration * 0.01, 1 + exaggeration), + rotate=(-exaggeration * 2, exaggeration * 2), + translate_px=(-exaggeration * 10, exaggeration * 10), + ), + A.Resize(y_size, x_size), + ], + keypoint_params=A.KeypointParams(format="xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + dataset = _get_openfield_dataset(transform=transform) + dataloader = DataLoader(dataset, batch_size=batch_size) + max_num_animals = dataset.parameters.max_num_animals + num_keypoints = dataset.parameters.num_joints + for i, item in enumerate(dataloader): + is_last_batch = i == (len(dataloader) - 1) + assert set(item.keys()) == key_set, ( + f"the key returned don't match the required ones: {item.keys()} != {key_set}" + ) + + anno = item["annotations"] + assert set(anno.keys()) == anno_key_set, "the annotation keys returned don't match the required ones" + + assert (len(item["image"].shape) == 4) and ((item["image"].shape[:2] == (batch_size, 3)) or is_last_batch), ( + "image shape is not (batch_size, 3, h, w)" + ) + + kpts, bboxes = anno["keypoints"], anno["boxes"] + b, _, h, w = item["image"].shape + assert (h == y_size) and (w == x_size) + assert kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) or is_last_batch, ( + "keypoints have the wrong shape" + ) + assert bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch, "boxes have the wrong shape" + assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ((bboxes[:, :, 1] + bboxes[:, :, 3]) <= h).all() diff --git a/tests/pose_estimation_pytorch/other/test_gaussian_targets.py b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py new file mode 100644 index 0000000000..1c202b1902 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py @@ -0,0 +1,56 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import HeatmapGaussianGenerator + + +@pytest.mark.parametrize( + "batch_size, num_keypoints, image_size", + [(2, 2, (64, 64)), (1, 5, (48, 64)), (15, 50, (64, 48))], +) +def test_gaussian_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1): + # generate annotations + labels = { + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) + } # batch size, num animals, num keypoints, 2 for x,y + # generate predictions + stride = 1 + prediction = { + "heatmap": torch.rand((batch_size, num_keypoints, *image_size[:2])), + "locref": torch.rand((batch_size, 2 * num_keypoints, *image_size[:2])), + } + + # generate heatmap + output = HeatmapGaussianGenerator( + num_heatmaps=num_keypoints, + pos_dist_thresh=17, + locref_std=5.0, + ) + output = output(stride, prediction, labels)["heatmap"]["target"].reshape( + batch_size, num_keypoints, image_size[0] * image_size[1] + ) + + # get coords of max value of the heatmap + gaus_max = torch.argmax(output, dim=2) + + # get unraveled coords + x = gaus_max % image_size[1] + y = gaus_max // image_size[1] + + # get heatmap center tensor + predict_kp = torch.stack((x, y), dim=-1) + # Remove num_animals dimension - only one animal is supported + labels["keypoints"] = torch.squeeze(labels["keypoints"], dim=1) + + # compare heatmap center to annotation + assert torch.eq(labels["keypoints"], predict_kp).all().item() diff --git a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py new file mode 100644 index 0000000000..b44dc3f39f --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py @@ -0,0 +1,204 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + + +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import HeatmapPlateauGenerator + + +def get_target( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + locref_std: float, + pos_dist_thresh: int, +): + """Summary Getting the target generator for certain annotations, predictions and + image size. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_std (float): scaling factor + pos_dist_thresh (int): radius plateau on the heatmap + + Returns: + target_output (dict): containing the heatmaps, locref_maps and locref_masks. + annotations (dict): containing input keypoint annotations. + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + output: + """ + labels = { + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_joints, 2)) + } # 2 for x,y coords + stride = 1 + prediction = { + "heatmap": torch.rand((batch_size, num_joints, image_size[0], image_size[1])), + "locref": torch.rand((batch_size, 2 * num_joints, image_size[0], image_size[1])), + } + generator = HeatmapPlateauGenerator( + num_heatmaps=num_joints, + pos_dist_thresh=pos_dist_thresh, + locref_std=locref_std, + generate_locref=True, + ) + + targets_output = generator(stride, prediction, labels) + return targets_output, labels + + +data = [(1, 1, 10, (256, 256), 7.2801, 17)] + + +@pytest.mark.parametrize( + "batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh", + data, +) +def test_expected_output( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + locref_stdev: float, + pos_dist_thresh: int, +): + """Summary: + Testing if plateau targets return the expected output. We take a target generator from + get_target function. Given a sequence of random numbers for batch_size, num_animals etc., we assert if + it returns the expected heatmaps and locrefmaps, as well as checking if the output has the expected shape. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_stdev (float): scaling factor + pos_dist_thresh (int): radius plateau on heatmap + + Returns: + None + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + """ + targets_output, annotations = get_target( + batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh + ) + + assert "heatmap" in targets_output + assert "locref" in targets_output + assert targets_output["heatmap"]["target"].shape == ( + batch_size, + num_joints, + image_size[0], + image_size[1], + ) # heatmaps score output + assert targets_output["locref"]["weights"].shape == ( + batch_size, + num_joints * 2, + image_size[0], + image_size[1], + ) + assert targets_output["locref"]["target"].shape == ( + batch_size, + num_joints * 2, + image_size[0], + image_size[1], + ) + + +data = [(1, 1, 10, (256, 256), 7.2801, 17)] + + +@pytest.mark.parametrize( + "batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh", + data, +) +def test_single_animal( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + locref_stdev: float, + pos_dist_thresh: int, +): + """Summary Testing, for single animals experiments (num_animals=1) if the distance + between the expected keypoints and the annotations keypoints is smaller than the + radius plateau. + + 'argmax' function returns the indices of the max values of all elements in the input tensor. + If there are multiple maximal values, such as in our case because it's a plateau, then the + indices of the first maximal value are returned. From this tensor we exctact x,y coords + and then concatenate these new tensors along a new dimension. Then, we assert if the distance between + each x,y element in annotations and predicted keypoints is smaller or equal to the 'pos_dist_thresh', + which represents the radius of the plateau heatmap. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_stdev (float): scaling factor + pos_dist_thresh (int): radius plateau on heatmap + + Returns: + None + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + """ + targets_output, annotations = get_target( + batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh + ) + + targets_output = torch.tensor( + targets_output["heatmap"]["target"].reshape(1, 10, image_size[0] * image_size[1]) + ) # converting from dict to tensor. 'argmax' works on tensors. + + plt_max = torch.argmax(targets_output, dim=2) + # get unraveled coords + x = plt_max % image_size[1] + y = plt_max // image_size[1] + + predict_kp = torch.stack((x, y), dim=-1) + + predict_kp = predict_kp.float() + + annotations["keypoints"] = torch.squeeze(annotations["keypoints"], dim=1) + annotations["keypoints"] = annotations["keypoints"].float() + + dist = torch.norm(annotations["keypoints"] - predict_kp, p=2, dim=-1) + assert (dist <= pos_dist_thresh).all() diff --git a/tests/pose_estimation_pytorch/other/test_helper.py b/tests/pose_estimation_pytorch/other/test_helper.py new file mode 100644 index 0000000000..1dfa250109 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_helper.py @@ -0,0 +1,21 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import torch + + +def test_train_valid_call(): + tmp_model = torch.nn.Linear(3, 10) + to_train_mode = tmp_model.train + to_train_mode() + assert tmp_model.training + to_valid_mode = tmp_model.eval + to_valid_mode() + assert not tmp_model.training diff --git a/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py new file mode 100644 index 0000000000..1ee4073fc8 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py @@ -0,0 +1,132 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.post_processing import ( + match_predictions_to_gt as deeplabcut_torch_match_predictions_gt, +) + + +@pytest.fixture +def animals_and_keypoints_invalid(): + """Summary: + Fixture with invalid pred_kpts and gt_kpts shapes that will raise ValueErrors. + + Returns: + tuple containing: + predicted keypoints(pred_kpts), of shape num_animals, num_keypoints, (x,y,score) + ground truth keypoints (gt_kpts), of shape num_animals, num_keypoints, (x,y) + individual names (indv_names) + """ + gt_kpts = 2 * np.ones((6, 6, 3)) # num animals, num keypoints, (x,y,vis) + gt_kpts[:, :, :2] = np.random.rand(6, 6, 2) + pred_kpts = np.random.rand(6, 8, 3) # num animals, num keypoints, (x,y,score) + indv_names = ["indv1", "indv2"] + return pred_kpts, gt_kpts, indv_names + + +@pytest.fixture +def animals_and_keypoints(): + """Summary: + Fixture with pred_kpts, gt_kpts shapes and indv_names. + + Returns: + tuple containing: + predicted keypoints(pred_kpts), of shape num_animals, num_keypoints, (x,y,score) + ground truth keypoints (gt_kpts), of shape num_animals, num_keypoints, (x,y) + individual names (indv_names) + """ + gt_kpts = 2 * np.ones((6, 6, 3)) # num animals, num keypoints, (x,y,vis) + gt_kpts[:, :, :2] = np.random.rand(6, 6, 2) + + # adding score value because the shape of pred_kpts should be (6,6,3) + score = np.full((gt_kpts.shape[0], gt_kpts.shape[1], 1), 0.5) + pred_kpts = np.concatenate((gt_kpts, score), axis=2) + np.random.shuffle(pred_kpts) # shuffle predicted keypoints + + indv_names = ["indv1", "indv2"] + return pred_kpts, gt_kpts, indv_names + + +def test_invalid_rmse(animals_and_keypoints_invalid: tuple) -> None: + """Summary: + Tets if an invalid output really returns a ValueError in the rmse function. + + Args: + animals_and_keypoints_invalid (tuple): containing predicted keypoints (pred_kpts), + ground truth keypoints (gt_kpts) and individual names (indv_names). + """ + pred_kpts, gt_kpts, indv_names = animals_and_keypoints_invalid + + with pytest.raises(ValueError): + deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + + +def test_invalid_oks(animals_and_keypoints_invalid: tuple) -> None: + """Summary: + Test if an invalid output really returns a ValueError in the oks function. + + Args: + animals_and_keypoints_invalid (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints + (gt_kpts) + and individual names (indv_names) + """ + pred_kpts, gt_kpts, indv_names = animals_and_keypoints_invalid + + with pytest.raises(ValueError): + deeplabcut_torch_match_predictions_gt.oks_match_prediction_to_gt(pred_kpts, gt_kpts, indv_names) + + +def test_rmse_match_predictions_to_gt(animals_and_keypoints: tuple, num_animals: int = 6) -> None: + """Summary: + Test if rmse_match_prediction_to_gt function returns the expected shape output. + + Args: + animals_and_keypoints (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints (gt_kpts) + and individual names (indv_names) + """ + pred_kpts, gt_kpts, indv_names = animals_and_keypoints + + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + assert isinstance(col_ind, np.ndarray) + assert col_ind.shape == (num_animals,) + + +def test_oks_match_predictions_to_gt(animals_and_keypoints: tuple, num_animals: int = 6) -> None: + """Summary: + Test if oks_match_predictions_to_gt function returns the expected shape output. + + Args: + animals_and_keypoints (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints (gt_kpts) + and individual names (indv_names) + """ + pred_kpts, gt_kpts, indv_names = animals_and_keypoints + + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + assert isinstance(col_ind, np.ndarray) + assert col_ind.shape == (num_animals,) + + +def test_extend_col_ind(animals_and_keypoints: tuple, num_animals: int = 6) -> None: + """Summary: + Test if the column indices have the expected shape. + + Args: + animals_and_keypoints (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints (gt_kpts) + and individual names (indv_names) + """ + pred_kpts, gt_kpts, indv_names = animals_and_keypoints + + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + extended_array = deeplabcut_torch_match_predictions_gt.extend_col_ind(col_ind, num_animals) + assert extended_array.shape == (num_animals,) diff --git a/tests/pose_estimation_pytorch/other/test_modelzoo.py b/tests/pose_estimation_pytorch/other/test_modelzoo.py new file mode 100644 index 0000000000..18fddfd931 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_modelzoo.py @@ -0,0 +1,50 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import os + +import pytest + +from deeplabcut.modelzoo.video_inference import video_inference_superanimal +from deeplabcut.utils import auxiliaryfunctions + +examples_folder = os.path.join( + auxiliaryfunctions.get_deeplabcut_path(), + "modelzoo", + "examples", +) + + +# requires videos to be in the examples folder +@pytest.mark.skip +@pytest.mark.parametrize( + "video_paths, superanimal_name", + [ + (f"{examples_folder}/black_dog.mp4", "superanimal_quadruped"), + (f"{examples_folder}/black_dog.mp4", "superanimal_quadruped_hrnetw32"), + (f"{examples_folder}/swear_mouse_tiny.mp4", "superanimal_topviewmouse"), + ( + f"{examples_folder}/swear_mouse_tiny.mp4", + "superanimal_topviewmouse_hrnetw32", + ), + ], +) +def test_video_inference_saves_file(video_paths, superanimal_name): + video_inference_superanimal( + video_paths, + superanimal_name=superanimal_name, + ) + if isinstance(video_paths, str): + video_paths = [video_paths] + for video_path in video_paths: + output_path = video_path.replace(".mp4", "_labeled.mp4") + assert os.path.exists(output_path), "Output video file does not exist" + + assert os.stat(output_path).st_size > 0, "Output video file is empty" diff --git a/tests/pose_estimation_pytorch/other/test_paf_targets.py b/tests/pose_estimation_pytorch/other/test_paf_targets.py new file mode 100644 index 0000000000..9865cf732c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_paf_targets.py @@ -0,0 +1,37 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import pafs_targets + + +@pytest.mark.parametrize( + "batch_size, num_keypoints, image_size", + [(2, 2, (64, 64)), (1, 5, (48, 64)), (8, 50, (64, 48))], +) +def test_paf_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=2): + labels = { + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) + } # 2 for x,y coords + graph = [(i, j) for i in range(num_keypoints) for j in range(i + 1, num_keypoints)] + prediction = { + "heatmap": torch.rand((batch_size, num_keypoints, image_size[0], image_size[1])), + "paf": torch.rand((batch_size, len(graph) * 2, image_size[0], image_size[1])), + } + generator = pafs_targets.PartAffinityFieldGenerator(graph=graph, width=20) + targets_output = generator(1, prediction, labels) + assert targets_output["paf"]["target"].shape == ( + batch_size, + len(graph) * 2, + image_size[0], + image_size[1], + ) diff --git a/tests/pose_estimation_pytorch/other/test_pose_model.py b/tests/pose_estimation_pytorch/other/test_pose_model.py new file mode 100644 index 0000000000..de36678c8c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_pose_model.py @@ -0,0 +1,300 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import copy +import random + +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.models as dlc_models +from deeplabcut.pose_estimation_pytorch.models import CRITERIONS, PREDICTORS, TARGET_GENERATORS +from deeplabcut.pose_estimation_pytorch.models.criterions import LOSS_AGGREGATORS +from deeplabcut.pose_estimation_pytorch.models.modules import AdaptBlock, BasicBlock + +backbones_dicts = [ + { + "type": "HRNet", + "model_name": "hrnet_w32", + "output_channels": 480, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w18", + "output_channels": 270, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w48", + "output_channels": 720, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w32", + "output_channels": 32, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + { + "type": "HRNet", + "model_name": "hrnet_w18", + "output_channels": 18, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + { + "type": "HRNet", + "model_name": "hrnet_w48", + "output_channels": 48, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + {"type": "ResNet", "model_name": "resnet50_gn", "output_channels": 2048, "stride": 32}, +] + +heads_dicts = [ + { + "type": "HeatmapHead", + "predictor": { + "type": "HeatmapPredictor", + "location_refinement": True, + "locref_std": 7.2801, + }, + "target_generator": { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": "num_bodyparts", + "pos_dist_thresh": 17, + "heatmap_mode": "KEYPOINT", + "generate_locref": True, + "locref_std": 7.2801, + }, + "criterion": { + "heatmap": { + "type": "WeightedBCECriterion", + "weight": 1.0, + }, + "locref": { + "type": "WeightedHuberCriterion", + "weight": 0.05, + }, + }, + "heatmap_config": { + "channels": [2048, 1024, -1], + "kernel_size": [2, 2], + "strides": [2, 2], + }, + "locref_config": { + "channels": [2048, 1024, -1], + "kernel_size": [2, 2], + "strides": [2, 2], + }, + "output_channels": -1, + "input_channels": 2048, + "total_stride": 4, + }, + { + "type": "TransformerHead", + "predictor": { + "type": "HeatmapPredictor", + "location_refinement": False, + }, + "target_generator": { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": "num_bodyparts", + "pos_dist_thresh": 17, + "heatmap_mode": "KEYPOINT", + "generate_locref": False, + }, + "criterion": {"type": "WeightedBCECriterion"}, + "dim": 192, + "hidden_heatmap_dim": 384, + "heatmap_dim": -1, + "apply_multi": True, + "heatmap_size": [-1, -1], + "apply_init": True, + "total_stride": 1, + "input_channels": -1, + "output_channels": -1, + "head_stride": 1, + }, + { + "type": "DEKRHead", + "predictor": { + "type": "DEKRPredictor", + "num_animals": 1, + "keypoint_score_type": "heatmap", + "max_absorb_distance": 75, + }, + "target_generator": { + "type": "DEKRGenerator", + "num_joints": "num_bodyparts", + "pos_dist_thresh": 17, + "bg_weight": 0.1, + }, + "criterion": { + "heatmap": { + "type": "WeightedBCECriterion", + "weight": 1.0, + }, + "offset": { + "type": "WeightedHuberCriterion", + "weight": 0.03, + }, + }, + "heatmap_config": { + "channels": [480, 64, -1], + "num_blocks": 1, + "dilation_rate": 1, + "final_conv_kernel": 1, + "block": BasicBlock, + }, + "offset_config": { + "channels": [480, -1, -1], + "num_offset_per_kpt": 15, + "num_blocks": 1, + "dilation_rate": 1, + "final_conv_kernel": 1, + "block": AdaptBlock, + }, + "total_stride": 1, + "input_channels": 480, + "output_channels": -1, + }, +] + + +def _generate_random_backbone_inputs(i): + # Returns sizes that are divisible by 64to be able to predict consistently output size + # (and be able to do the forward pass of HRNet) + x_size_tmp, y_size_tmp = random.randint(100, 1000), random.randint(100, 1000) + return ( + backbones_dicts[i], + (x_size_tmp - x_size_tmp % 64, y_size_tmp - y_size_tmp % 64), + ) + + +@pytest.mark.parametrize( + "backbone_dict, input_size", + [_generate_random_backbone_inputs(i) for i in range(len(backbones_dicts))], +) +def test_backbone(backbone_dict, input_size): + input_tensor = torch.Tensor(1, 3, input_size[1], input_size[0]) + + stride = backbone_dict.pop("stride") + output_channels = backbone_dict.pop("output_channels") + backbone = dlc_models.BACKBONES.build(backbone_dict) + + features = backbone(input_tensor) + _, c, h, w = features.shape + assert c == output_channels + assert h == input_size[1] // stride + assert w == input_size[0] // stride + + +def _generate_random_head_inputs(i): + # Returns sizes that are divisible by 64to be able to predict consistently output size + # (and be able to do the forward pass of HRNet) + x_size_tmp, y_size_tmp = random.randint(8, 500), random.randint(8, 500) + num_kpts = random.randint(2, 50) + return ( + heads_dicts[i], + (x_size_tmp - x_size_tmp % 4, y_size_tmp - y_size_tmp % 4), + num_kpts, + ) + + +@pytest.mark.parametrize( + "head_dict, input_shape, num_keypoints", + [_generate_random_head_inputs(i) for i in range(len(heads_dicts))], +) +def test_head(head_dict, input_shape, num_keypoints): + w, h = input_shape + head_dict = copy.deepcopy(head_dict) + + head_type = head_dict["type"] + input_channels = head_dict.pop("input_channels") + output_channels = head_dict.pop("output_channels") + total_stride = head_dict.pop("total_stride") + if head_type == "HeatmapHead": + output_channels = num_keypoints + head_dict["heatmap_config"]["channels"][2] = output_channels + head_dict["locref_config"]["channels"][2] = 2 * output_channels + head_dict["target_generator"]["num_heatmaps"] = output_channels + input_tensor = torch.zeros((1, input_channels, h, w)) + + elif head_type == "TransformerHead": + output_channels = num_keypoints + input_channels = num_keypoints + head_dict["heatmap_dim"] = h * w + head_dict["heatmap_size"] = [h, w] + head_dict["target_generator"]["num_heatmaps"] = output_channels + input_tensor = torch.zeros((1, input_channels, head_dict["dim"] * 3)) + + elif head_type == "DEKRHead": + output_channels = num_keypoints + 1 + head_dict["target_generator"]["num_joints"] = num_keypoints + head_dict["heatmap_config"]["channels"][2] = num_keypoints + 1 + head_dict["offset_config"]["channels"][1] = num_keypoints * head_dict["offset_config"]["num_offset_per_kpt"] + head_dict["offset_config"]["channels"][2] = num_keypoints + input_tensor = torch.zeros((1, input_channels, h, w)) + + if "type" in head_dict["criterion"]: + head_dict["criterion"] = CRITERIONS.build(head_dict["criterion"]) + else: + weights = {} + criterions = {} + for loss_name, criterion_cfg in head_dict["criterion"].items(): + weights[loss_name] = criterion_cfg.get("weight", 1.0) + criterion_cfg = {k: v for k, v in criterion_cfg.items() if k != "weight"} + criterions[loss_name] = CRITERIONS.build(criterion_cfg) + + aggregator_cfg = {"type": "WeightedLossAggregator", "weights": weights} + head_dict["aggregator"] = LOSS_AGGREGATORS.build(aggregator_cfg) + head_dict["criterion"] = criterions + + head_dict["target_generator"] = TARGET_GENERATORS.build(head_dict["target_generator"]) + head_dict["predictor"] = PREDICTORS.build(head_dict["predictor"]) + head = dlc_models.HEADS.build(head_dict) + + output = head(input_tensor)["heatmap"] + _, c_out, h_out, w_out = output.shape + assert (h_out == h * total_stride) and (w_out == w * total_stride) + assert c_out == output_channels + + +def test_msa_hrnet(): + # TODO: build microsoft asia hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + pass + + +def test_msa_tokenpose(): + # TODO: build microsoft asia hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + # cf https://github.com/amathislab/BUCTDdev/blob/main/lib/models/transpose_h.py#L1 + pass + + +def test_msa_hrnetCOAM(): + # TODO: build BUCTD COAM hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + pass + + +# TODO: add other model variants our pipeline can build ;) diff --git a/tests/pose_estimation_pytorch/other/test_seq_targets.py b/tests/pose_estimation_pytorch/other/test_seq_targets.py new file mode 100644 index 0000000000..c2816c8650 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_seq_targets.py @@ -0,0 +1,51 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from itertools import combinations + +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import ( + TARGET_GENERATORS, +) + + +def test_sequential_generator(): + batch_size = 4 + image_size = 256, 256 + num_keypoints = 12 + num_animals = 2 + graph = [list(edge) for edge in combinations(range(num_keypoints), 2)] + num_limbs = len(graph) + cfg = { + "type": "SequentialGenerator", + "generators": [ + { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": num_keypoints, + "pos_dist_thresh": 17, + "generate_locref": True, + "locref_std": 7.2801, + }, + {"type": "PartAffinityFieldGenerator", "graph": graph, "width": 20}, + ], + } + gen = TARGET_GENERATORS.build(cfg) + + annotations = {"keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2))} + head_outputs = { + "heatmap": torch.rand(batch_size, num_keypoints, 32, 32), + "locref": torch.rand(batch_size, num_keypoints * 2, 32, 32), + "paf": torch.rand(batch_size, num_limbs * 2, 32, 32), + } + out = gen(stride=1, outputs=head_outputs, labels=annotations) + assert all(s in out for s in list(head_outputs)) + for k, v in head_outputs.items(): + assert out[k]["target"].shape == v.shape diff --git a/tests/pose_estimation_pytorch/post_processing/test_identity.py b/tests/pose_estimation_pytorch/post_processing/test_identity.py new file mode 100644 index 0000000000..3f16eade5f --- /dev/null +++ b/tests/pose_estimation_pytorch/post_processing/test_identity.py @@ -0,0 +1,59 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests identity matching.""" + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.post_processing.identity import assign_identity + + +@pytest.mark.parametrize( + "prediction, identity_scores, output_order", + [ + ( + [ + [[0, 0, 1.0], [0, 0, 1.0]], # assembly 1 + [[5, 5, 1.0], [5, 5, 1.0]], # assembly 2 + [[9, 9, 1.0], [9, 9, 1.0]], # assembly 3 + ], + [ # a0 -> idv1, a1 -> idv2, a2 -> idv0 + [[0.1, 0.8, 0.3], [0.1, 0.7, 0.3]], # assembly 1 ID scores + [[0.2, 0.1, 0.6], [0.3, 0.1, 0.5]], # assembly 2 ID scores + [[0.7, 0.1, 0.1], [0.6, 0.2, 0.2]], # assembly 3 ID scores + ], + [2, 0, 1], + ), + ( + [ + [[0, 0, 1.0], [0, 0, 1.0]], # assembly 1 + [[1, 1, 1.0], [5, 5, 1.0]], # assembly 2 + [[0, 0, 1.0], [9, 9, 1.0]], # assembly 3 + ], + [ # a0 -> idv0, a1 -> idv1, a2 -> idv2 + [[0.4, 0.4, 0.3], [0.5, 0.3, 0.3]], # assembly 1 ID scores + [[0.4, 0.4, 0.3], [0.3, 0.5, 0.4]], # assembly 2 ID scores + [[0.2, 0.2, 0.4], [0.2, 0.2, 0.3]], # assembly 3 ID scores + ], + [0, 1, 2], + ), + ], +) +def test_single_identity_assignment(prediction, identity_scores, output_order): + predictions = np.array(prediction) + identity_scores = np.array(identity_scores) + new_order = assign_identity(predictions, identity_scores) + predictions_with_id = predictions[new_order] + + print() + print(predictions.shape) + print(identity_scores.shape) + np.testing.assert_equal(predictions[output_order], predictions_with_id) diff --git a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py new file mode 100644 index 0000000000..48f65fd5e1 --- /dev/null +++ b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py @@ -0,0 +1,108 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests pose NMS.""" + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.post_processing.nms as nms + + +@pytest.mark.parametrize( + "poses, score_threshold, expected_kept", + [ + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [True], # a single pose should be kept + ), + ( + [ + [[0.0, np.nan, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [True], # a single pose should be kept + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [False, False], # no valid poses + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [False, True, False, False], # a single valid pose + ), + ( + [ + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[100.0, 100, 0.89], [110, 110, 0.89], [120, 120, 0.89]], + ], + 0.1, + [True, True], # two valid poses, far apart + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + ], + 0.1, + [False, True, True], # two valid poses, far apart + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 0.1, + [False, True, True], # two valid poses, far apart, sorted by score + ), + ( + [ + [[0.0, 0, 0.89], [10, 10, 0.89], [20, 20, 0.89]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 0.1, + [False, True, True], # two valid poses, far apart, sorted by score, one suppressed + ), + ( + [ + [[1.0, 0, 0.89], [11, 10, 0.89], [21, 20, 0.89]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 0.1, + [False, True, True], # two valid poses, far apart, sorted by score, one suppressed + ), + ], +) +def test_oks_nms_post_processing(poses, score_threshold, expected_kept): + """Tests pose NMS.""" + kept = nms.nms_oks( + predictions=np.asarray(poses), + oks_threshold=0.9, + oks_sigmas=0.1, + score_threshold=0.1, + ) + assert kept.tolist() == expected_kept diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py new file mode 100644 index 0000000000..ae6de38298 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -0,0 +1,77 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for the bottom-up pytorch runner.""" + +from pathlib import Path +from typing import Any + +import pytest + +from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config +from deeplabcut.pose_estimation_pytorch.models import PoseModel +from deeplabcut.pose_estimation_pytorch.runners.train import build_training_runner +from deeplabcut.pose_estimation_pytorch.task import Task + +SINGLE_ANIMAL_NETS = ["resnet_50"] +MULTI_ANIMAL_NETS = ["dekr_w18"] +NETS = [(n, False) for n in SINGLE_ANIMAL_NETS] + [(n, True) for n in MULTI_ANIMAL_NETS] + + +def print_dict(data: dict, indent: int = 0): + for k, v in data.items(): + if isinstance(v, dict): + print_dict(v, indent=indent + 2) + else: + print(f"{indent * ' '}{k}: {v}") + + +# @pytest.mark.skip(reason="This test is outdated and needs to be updated to reflect changes in the codebase.") + + +@pytest.mark.parametrize("net_type, multianimal", NETS) +def test_build_bottom_up_runner( + net_type: str, + multianimal: bool, + tmp_path: Path, +) -> None: + project_cfg: dict[str, Any] = { + "multianimalproject": multianimal, + "project_path": str(tmp_path), + } + if multianimal: + project_cfg["bodyparts"] = "MULTI!" + project_cfg["multianimalbodyparts"] = ["head", "shoulder", "knee", "toe"] + project_cfg["uniquebodyparts"] = [] + project_cfg["individuals"] = ["tom", "jerry"] + else: + project_cfg["bodyparts"] = ["head", "shoulder", "knee", "toe"] + project_cfg["uniquebodyparts"] = [] + project_cfg["individuals"] = ["tom"] + + root_path = Path(__file__).parent.parent + template_path = (root_path / "other/test_configs/pytorch_config.yaml").resolve() + assert template_path.is_file(), f"Template config not found at {template_path}" + + pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) + pose_model = PoseModel.build(pytorch_cfg["model"]) + + # NOTE: @C-Achard 2026-03-18 This file was not named with test_* as a prefix, + # so it never ran in CI. A lot of imports are outdated and non-existent + # FIX: replace RUNNERS registry with build_training_runner and remove unused imports + runner = build_training_runner( + runner_config=pytorch_cfg["runner"], + model_folder=tmp_path, + task=Task.BOTTOM_UP, + model=pose_model, + device=pytorch_cfg["device"], + logger=None, + ) + assert runner is not None diff --git a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py new file mode 100644 index 0000000000..7c1ebb162f --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py @@ -0,0 +1,194 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the dynamic cropper.""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping import ( + DynamicCropper, + TopDownDynamicCropper, +) + + +@pytest.mark.parametrize("dynamic", [(False, 0.5, 10)]) +def test_build_dynamic_cropper(dynamic: tuple[bool, float, int]): + cropper = DynamicCropper.build(*dynamic) + should_be_built, threshold, margin = dynamic + if should_be_built: + assert isinstance(cropper, DynamicCropper) + assert cropper.threshold == threshold + assert cropper.margin == margin + else: + assert cropper is None + + +@pytest.mark.parametrize("batch_size", [0, 2, 8]) +def test_dynamic_fails_with_image_batch(batch_size: int): + cropper = DynamicCropper(threshold=0.6, margin=10) + with pytest.raises(RuntimeError): + cropper.crop(torch.zeros(batch_size, 3, 128, 128)) + + +def test_dynamic_fails_with_variable_frame_size(): + cropper = DynamicCropper(threshold=0.6, margin=10) + cropper.crop(torch.zeros(1, 3, 64, 64)) + with pytest.raises(RuntimeError): + cropper.crop(torch.zeros(1, 3, 128, 128)) + + +def test_dynamic_fails_with_update_before_crop(): + cropper = DynamicCropper(threshold=0.6, margin=10) + with pytest.raises(RuntimeError): + cropper.update(torch.ones(5, 17, 3)) + + +@pytest.mark.parametrize("threshold", [0.25, 0.5, 0.8]) +def test_dynamic_cropper_does_nothing_with_low_quality(threshold: float): + cropper = DynamicCropper(threshold=threshold, margin=10) + image_in = torch.ones((1, 3, 32, 32)) + cropper.crop(image_in) + for i in range(10): + pose = _generate_random_pose( + (32, 64), + min_score=0.0, + max_score=threshold - 0.001, + seed=i, + ) + cropper.update(pose) + image_out = cropper.crop(image_in) + assert torch.equal(image_in, image_out) + + +@pytest.mark.parametrize( + "pose, threshold, margin, expected_crop", + [ + ([[float("nan"), float("nan"), float("nan")]], 0.1, 10, [0, 0, 64, 64]), + ([[float("nan"), 30, 0.0]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.0]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.49]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.8]], 0.5, 10, [10, 20, 30, 40]), + ([[20, 30, 0.8], [float("nan"), float("nan"), 0.2]], 0.5, 15, [5, 15, 35, 45]), + ([[20, 30, 0.8], [5, 5, 0.2]], 0.5, 15, [0, 0, 35, 45]), + ([[20, 30, 0.8], [35, 30, 0.79]], 0.8, 5, [15, 25, 40, 35]), + ([[40, 10, 0.2], [35, 15, 0.79]], 0.3, 8, [27, 2, 48, 23]), + ( + [ + [[float("nan"), float("nan"), float("nan")]], + [[float("nan"), float("nan"), float("nan")]], + ], + 0.15, + 10, + [0, 0, 64, 64], + ), + ( + [ + [[20, 30, 0.8], [5, 12, 0.2]], + [[40, 10, 0.2], [35, 15, 0.79]], + ], + 0.15, + 5, + [0, 5, 45, 35], + ), + ], +) +def test_dynamic_cropper_basic_crop( + pose: list[list[float]], threshold: float, margin: int, expected_crop: tuple[int, int, int, int] +) -> None: + x0, y0, x1, y1 = expected_crop + crop_w, crop_h = x1 - x0, y1 - y0 + + image_in = torch.zeros((1, 3, 64, 64)) + image_in[:, :, y0:y1, x0:x1] = 1 + expected_image_out = torch.ones((1, 3, crop_h, crop_w)) + + cropper = DynamicCropper(threshold=threshold, margin=margin) + image_out = cropper.crop(image_in) + assert torch.equal(image_out, image_in) + + cropper.update(torch.tensor(pose)) + image_out = cropper.crop(image_in) + assert image_out.shape == expected_image_out.shape + assert torch.equal(image_out, expected_image_out) + + pose_out = torch.tensor(pose) + print("\nPose in") + print(pose_out.numpy()) + pose_out[..., 0] -= x0 + pose_out[..., 1] -= y0 + print("Pose out before update") + print(pose_out.numpy()) + cropper.update(pose_out) + print("Pose out after update") + print(pose_out.numpy()) + np.testing.assert_allclose(pose_out.numpy(), np.array(pose)) + + +@pytest.mark.parametrize("size", [128, 256, 291, 320, 480, 500, 640, 800]) +@pytest.mark.parametrize("n", [1, 2, 3, 4, 5]) +@pytest.mark.parametrize("overlap", [0, 1, 5, 10, 100]) +def test_tddc_array_split(size: int, n: int, overlap: int) -> None: + print("\nTesting TopDownDynamicCropper array split") + print("Size:", size) + print("N:", n) + print("Overlap:", overlap) + sections = TopDownDynamicCropper.split_array(size, n, overlap) + print("Sections:") + for section in sections: + print(f" {section}") + + # check that we have the desired number of sections + assert len(sections) == n + + # check that the sections start at 0 and end at the array size + start, end = sections[0][0], sections[-1][1] + assert start == 0 + assert end == size + + # check all sections have size at least 1 + for start, end in sections: + assert start < end + + # check that all sections have the same size + sizes = [end - start for start, end in sections] + assert len(set(sizes)) == 1 + + # check the overlap is big enough for each section + for (_start_1, end_1), (start_2, _end_2) in zip(sections[:-1], sections[1:], strict=False): + assert end_1 >= start_2 + assert end_1 - start_2 >= overlap + + # check that the difference between overlaps is at most 1 + # FIXME(niels) - auto-correct the overlap to spread it out more evenly + # if n > 1: + # overlaps = [ + # end_1 - start_2 + # for (start_1, end_1), (start_2, end_2) in zip(sections[:-1], sections[1:]) + # ] + # + # assert max(overlaps) - min(overlaps) <= 1 + + +def _generate_random_pose( + image_shape: tuple[int, int], + min_score: float, + max_score: float, + num_animals: int = 3, + num_keypoints: int = 7, + seed: int = 0, +) -> torch.Tensor: + gen = np.random.default_rng(seed) + pose = gen.random((num_animals, num_keypoints, 3)) + pose[..., 0] *= image_shape[0] + pose[..., 1] *= image_shape[1] + pose[..., 2] = (pose[..., 2] * (max_score - min_score)) + min_score + return torch.from_numpy(pose) diff --git a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py new file mode 100644 index 0000000000..4eaa6ad543 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Test script for superanimal_humanbody with torchvision detector.""" + +from deeplabcut.pose_estimation_pytorch.apis.utils import ( + TORCHVISION_DETECTORS, + get_filtered_coco_detector_inference_runner, +) +from deeplabcut.pose_estimation_pytorch.models.detectors.filtered_detector import ( + FilteredDetector, +) +from deeplabcut.pose_estimation_pytorch.modelzoo import load_super_animal_config +from deeplabcut.pose_estimation_pytorch.modelzoo.utils import COCO_PERSON_CATEGORY_ID + + +def test_torchvision_detector(): + """Test that the torchvision detector works with superanimal_humanbody.""" + for detector_name in TORCHVISION_DETECTORS: + # Load the superanimal_humanbody config + superanimal_config = load_super_animal_config( + super_animal="superanimal_humanbody", + model_name="rtmpose_x", + detector_name=detector_name, + ) + print("Config loaded successfully!") + + # Test loading the torchvision detector directly + print("\nTesting torchvision detector loading...") + entry = TORCHVISION_DETECTORS[detector_name] + weights = entry["weights"] + coco_detector = entry["fn"](weights=weights, box_score_thresh=0.6) + coco_detector.eval() + print("Torchvision detector loaded successfully!") + + # Test loading the FilteredDetector + person_detector = FilteredDetector(coco_detector, class_id=COCO_PERSON_CATEGORY_ID) + person_detector.eval() + print("Filtered detector loaded successfully!") + + _ = get_filtered_coco_detector_inference_runner( + model_name=detector_name, + category_id=COCO_PERSON_CATEGORY_ID, + batch_size=1, + model_config=superanimal_config, + ) + print("Filtered detector runner created successfully!") + + print("\n✅ All tests passed! The torchvision detector integration is working correctly.") + return True + + +if __name__ == "__main__": + print("Testing superanimal_humanbody with torchvision detector...") + success = test_torchvision_detector() + if success: + print("\n✅ Test passed! The torchvision detector works with superanimal_humanbody") + else: + print("\n❌ Test failed! There's an issue with the torchvision detector integration") diff --git a/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py b/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py new file mode 100644 index 0000000000..318c909e3f --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py @@ -0,0 +1,67 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests DLC_DIRECTML_NO_GRAD toggles inference_mode vs no_grad (AMD DirectML).""" + +from __future__ import annotations + +import importlib +import os +from unittest.mock import Mock + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.inference as inference + + +def _reload_with_env(env_value: str | None): + if env_value is None: + os.environ.pop("DLC_DIRECTML_NO_GRAD", None) + else: + os.environ["DLC_DIRECTML_NO_GRAD"] = env_value + importlib.reload(inference) + + +@pytest.fixture(autouse=True) +def _restore_env(): + yield + _reload_with_env(None) # always restore defaults after each test + + +@pytest.mark.parametrize( + ("env_value", "directml_no_grad"), + [(None, False), ("false", False), ("true", True)], +) +def test_directml_no_grad_env(env_value, directml_no_grad): + """env var sets _directml_no_grad and selects the correct torch grad context.""" + _reload_with_env(env_value) + assert inference._directml_no_grad is directml_no_grad + + class _SniffRunner(inference.InferenceRunner): + def __init__(self): + super().__init__( + model=Mock(), + batch_size=1, + inference_cfg=inference.InferenceConfig( + multithreading=inference.MultithreadingConfig(enabled=False), + ), + ) + self.saw_inference_mode: bool | None = None + + def predict(self, inputs: torch.Tensor, **kwargs): + self.saw_inference_mode = torch.is_inference_mode_enabled() + return [{"mock": {"poses": np.zeros((1,), dtype=np.float32)}}] + + runner = _SniffRunner() + runner.inference([np.zeros((1, 3, 8, 8), dtype=np.float32)]) + + assert runner.saw_inference_mode is not directml_no_grad diff --git a/tests/pose_estimation_pytorch/runners/test_logger.py b/tests/pose_estimation_pytorch/runners/test_logger.py new file mode 100644 index 0000000000..9b27314302 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_logger.py @@ -0,0 +1,102 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests loggers.""" + +from pathlib import Path +from typing import Any + +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.logger as logging + + +class MockImageLogger(logging.ImageLoggerMixin): + """Mock image logger.""" + + def log_images( + self, + inputs: dict[str, Any], + outputs: dict[str, torch.Tensor], + targets: dict[str, dict[str, torch.Tensor]], + step: int, + ) -> None: + pass + + +@pytest.mark.parametrize( + "keypoints", + [ + [ + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + ], + [ + [[float("nan"), float("nan")], [float("nan"), float("nan")]], + ], + [ + [[0.0, 0.0], [1, 1], [2, 2]], + ], + [[[float("nan"), 0.0], [1, 1], [2, 2]]], + [[[-1.0, -1.0], [1, 1], [2, 2]]], + [ + [[-1.0, -1.0], [-1.0, -1.0]], + ], + [ + [[-1.0, -1.0], [-1.0, -1.0]], + [[1.0, 1.0], [1.0, 1.0]], + ], + ], +) +@pytest.mark.parametrize("denormalize", [True, False]) +def test_prepare_image(keypoints: list[list[float]], denormalize: bool) -> None: + image = torch.ones((3, 256, 256)) + keypoints = torch.tensor(keypoints) + + print() + print(f"IMAGE: {image.shape}") + print(f"KEYPOINTS: {keypoints.shape}") + for k in keypoints: + print(k) + print() + print() + + logger = MockImageLogger() + logger._prepare_image( + image=image, + denormalize=denormalize, + keypoints=keypoints, + bboxes=None, + ) + + +def test_csv_logger_resume(tmp_path: Path) -> None: + """Test CSVLogger preserves data when resuming from snapshot.""" + log_file = tmp_path / "learning_stats.csv" + + # Initial training: log some metrics + logger1 = logging.CSVLogger(str(tmp_path), "learning_stats.csv") + logger1.log({"loss": 0.5, "accuracy": 0.8}, step=1) + logger1.log({"loss": 0.4, "accuracy": 0.9}, step=2) + + assert log_file.exists() + assert len(logger1._steps) == 2 + + # Resume training: should load existing data + logger2 = logging.CSVLogger(str(tmp_path), "learning_stats.csv") + assert len(logger2._steps) == 2 + assert logger2._steps == [1, 2] + assert logger2._metric_store[0]["loss"] == 0.5 + assert logger2._metric_store[1]["accuracy"] == 0.9 + + # Log new data: should append, not overwrite + logger2.log({"loss": 0.3, "accuracy": 0.95}, step=3) + assert len(logger2._steps) == 3 + assert logger2._steps == [1, 2, 3] diff --git a/tests/pose_estimation_pytorch/runners/test_runners.py b/tests/pose_estimation_pytorch/runners/test_runners.py new file mode 100644 index 0000000000..c3afa93db5 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners.py @@ -0,0 +1,38 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import pickle +from pathlib import Path +from unittest.mock import Mock + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners as runners + + +@pytest.mark.parametrize("value", [True, False]) +def test_set_load_weights_only(value: bool): + print(f"\nget_load_weights_only: {runners.get_load_weights_only()}") + print(f"setting value to {value}") + runners.set_load_weights_only(value) + print(f"get_load_weights_only: {runners.get_load_weights_only()}\n") + assert runners.get_load_weights_only() == value + + +def test_load_snapshot_weights_only_error(tmpdir_factory): + snapshot_dir = Path(tmpdir_factory.mktemp("snapshot-dir")) + snapshot_path = snapshot_dir / "snapshot.pt" + torch.save(dict(content=np.zeros(10)), str(snapshot_path)) + + runners.set_load_weights_only(False) + with pytest.raises(pickle.UnpicklingError): + runners.Runner.load_snapshot(snapshot_path, device="cpu", model=Mock(), weights_only=True) diff --git a/tests/pose_estimation_pytorch/runners/test_runners_inference.py b/tests/pose_estimation_pytorch/runners/test_runners_inference.py new file mode 100644 index 0000000000..f59831ca4e --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners_inference.py @@ -0,0 +1,195 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests inference runners.""" + +from unittest.mock import Mock, patch + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.data.postprocessor as post +import deeplabcut.pose_estimation_pytorch.data.preprocessor as prep +import deeplabcut.pose_estimation_pytorch.runners.inference as inference +from deeplabcut.pose_estimation_pytorch import get_load_weights_only +from deeplabcut.pose_estimation_pytorch.task import Task + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.build_optimizer", Mock()) +@pytest.mark.parametrize("task", [Task.DETECT, Task.TOP_DOWN, Task.BOTTOM_UP]) +@pytest.mark.parametrize("weights_only", [None, True, False]) +def test_load_weights_only_with_build_training_runner(task: Task, weights_only: bool): + with patch("deeplabcut.pose_estimation_pytorch.runners.base.torch.load") as load: + snapshot = "snapshot.pt" + inference.build_inference_runner( + task=task, + model=Mock(), + device="cpu", + snapshot_path=snapshot, + load_weights_only=weights_only, + ) + if weights_only is None: + weights_only = get_load_weights_only() + load.assert_called_once_with(snapshot, map_location="cpu", weights_only=weights_only) + + +class MockInferenceRunner(inference.InferenceRunner): + """Mocks the predict function for an inference runner.""" + + def __init__( + self, + batch_size: int = 1, + preprocessor: prep.Preprocessor | None = None, + postprocessor: post.Postprocessor | None = None, + ) -> None: + super().__init__( + model=Mock(), + batch_size=batch_size, + preprocessor=preprocessor, + postprocessor=postprocessor, + ) + self.batch_shapes = [] + + def predict(self, inputs: torch.Tensor) -> list[dict[str, dict[str, np.ndarray]]]: + self.batch_shapes.append(tuple(inputs.shape)) + return [ # return first elem of input + {"mock": {"index": i[0, 0, 0].detach().numpy()}} for i in inputs + ] + + +@pytest.mark.parametrize("batch_size", [1, 2, 4, 8]) +def test_mock_bottom_up(batch_size): + h, w = 640, 480 + images = [i * np.ones((1, 3, h, w)) for i in range(10)] + + runner = MockInferenceRunner(batch_size=batch_size) + predictions = runner.inference(images) + + print() + print(f"Num images: {len(predictions)}") + print(f"Num predictions: {len(predictions)}") + print(f"Batch shapes: {runner.batch_shapes}") + print(80 * "-") + for i in images: + print(i[0, 0, 0, 0]) + print("----") + print(80 * "-") + for p in predictions: + print(p) + print("----") + + _check_batch_shapes(batch_size, h, w, runner.batch_shapes) + assert len(images) == len(predictions) + for i, p in zip(images, predictions, strict=True): + assert len(p) == 1 # only 1 output per image + assert i[0, 0, 0, 0] == p[0]["mock"]["index"] + + +@pytest.mark.parametrize("batch_size", [1, 2, 4, 8]) +@pytest.mark.parametrize( + "detections_per_image", + [ + [1, 1, 1, 1, 1], + [0, 1, 0, 1, 1], # some frames might not have predictions + [0, 0, 0, 5, 2], + [1, 2, 3, 4], + [3, 4, 2, 1, 4], + [4, 23, 5, 20, 64, 100], + ], +) +def test_mock_top_down(batch_size, detections_per_image): + h, w = 8, 8 + images = [] + for index, num_detections in enumerate(detections_per_image): + if num_detections == 0: + detections = np.zeros((0, 3, 1, 1)) # random shape when no detections + else: + detections = np.concatenate( + [(1_000_000 * (index + 1) + i) * np.ones((1, 3, h, w)) for i in range(num_detections)], + axis=0, + ) + + images.append(detections) + + runner = MockInferenceRunner(batch_size=batch_size) + predictions = runner.inference(images) + + print() + print(f"Num images: {len(predictions)}") + print(f"Num predictions: {len(predictions)}") + print(80 * "-") + for i in images: + for i_det in i: + print(i_det.shape) + print(i_det[0, 0, 0]) + print("----") + + print(80 * "-") + for p in predictions: + print(p) + print("----") + + _check_batch_shapes(batch_size, h, w, runner.batch_shapes) + + assert len(images) == len(predictions) + for i, p in zip(images, predictions, strict=True): + assert len(p) == len(i) # one prediction per input + for i_det, p_det in zip(i, p, strict=True): + print(i_det.shape) + print(p_det["mock"]["index"]) + assert i_det[0, 0, 0] == p_det["mock"]["index"] + + +def test_dynamic_pose_inference_calls_dynamic(): + pose_batch = torch.zeros((1, 1, 1, 3)) + pose_batch_updated = torch.ones((1, 1, 1, 3)) + + image_crop = Mock() + image_crop.__len__ = Mock(return_value=1) + + model = Mock() + model.get_predictions = Mock() + model.get_predictions.return_value = dict(bodypart=dict(poses=pose_batch)) + + dynamic = Mock() + dynamic.crop = Mock() + dynamic.crop.return_value = image_crop + dynamic.update = Mock() + dynamic.update.return_value = pose_batch_updated + + runner = inference.PoseInferenceRunner( + model=model, + dynamic=dynamic, + batch_size=1, + ) + image = torch.zeros((1, 3, 64, 64)) + updated_pose = runner.predict(image) + dynamic.crop.assert_called_once_with(image) + dynamic.update.assert_called_once_with(pose_batch) + + assert len(updated_pose) == 1 + np.testing.assert_allclose( + updated_pose[0]["bodypart"]["poses"], + pose_batch_updated[0].cpu().numpy(), + ) + + +def _check_batch_shapes(batch_size, h, w, batch_shapes) -> None: + for b in batch_shapes[:-1]: + assert b[0] == batch_size + assert b[1] == 3 + assert b[2] == h + assert b[3] == w + + assert batch_shapes[-1][0] <= batch_size + assert batch_shapes[-1][1] <= 3 + assert batch_shapes[-1][2] <= h + assert batch_shapes[-1][3] <= w diff --git a/tests/pose_estimation_pytorch/runners/test_runners_train.py b/tests/pose_estimation_pytorch/runners/test_runners_train.py new file mode 100644 index 0000000000..3fa0994217 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners_train.py @@ -0,0 +1,320 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from dataclasses import dataclass +from unittest.mock import Mock, patch + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.schedulers as schedulers +import deeplabcut.pose_estimation_pytorch.runners.train as train_runners +from deeplabcut.pose_estimation_pytorch.models import PoseModel +from deeplabcut.pose_estimation_pytorch.models.backbones import ResNet +from deeplabcut.pose_estimation_pytorch.models.heads import HeatmapHead +from deeplabcut.pose_estimation_pytorch.task import Task + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.build_optimizer", Mock()) +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize("task", [Task.DETECT, Task.TOP_DOWN, Task.BOTTOM_UP]) +@pytest.mark.parametrize("weights_only", [True, False]) +def test_load_weights_only_with_build_training_runner(task: Task, weights_only: bool): + runner_config = dict( + optimizer=dict(), + snapshots=dict(max_snapshots=1, save_epochs=5, save_optimizer_state=False), + load_weights_only=weights_only, + ) + with patch("deeplabcut.pose_estimation_pytorch.runners.base.torch.load") as load: + train_runners.build_training_runner( + runner_config=runner_config, + model_folder=Mock(), + task=task, + model=Mock(), + device="cpu", + snapshot_path="snapshot.pt", + ) + load.assert_called_once_with("snapshot.pt", map_location="cpu", weights_only=weights_only) + + +@dataclass +class SchedulerTestConfig: + cfg: dict + init_lr: float + expected_lrs: list[float] + + +TEST_SCHEDULERS = [ + SchedulerTestConfig( + cfg=dict( + type="LRListScheduler", + params=dict(milestones=[2, 5], lr_list=[[0.5], [0.1]]), + ), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 0.5, 0.5, 0.5, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.1]])), + init_lr=0.1, + expected_lrs=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.5]])), + init_lr=0.1, + expected_lrs=[0.1, 0.5, 0.5, 0.5], + ), + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=3, gamma=0.1)), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 1.0, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01, 0.001], + ), +] + + +@pytest.mark.parametrize("load_head_weights", [True, False]) +def test_load_head_weights(tmp_path_factory, load_head_weights): + model_folder = tmp_path_factory.mktemp("model_folder") + runner_config = dict( + optimizer=dict(type="SGD", params=dict(lr=1)), + snapshots=dict(max_snapshots=1, save_epochs=1, save_optimizer_state=False), + ) + + model = PoseModel( + cfg=dict(), + backbone=ResNet(), + heads=dict( + bodyparts=HeatmapHead( + predictor=Mock(), + target_generator=Mock(), + criterion=Mock(), + aggregator=None, + heatmap_config=dict(channels=[2048, 10], kernel_size=[3], strides=[2]), + ), + ), + ) + + original_state_dict = model.state_dict() + zero_state_dict = {k: torch.zeros_like(v) for k, v in original_state_dict.items()} + + load = Mock() + load.return_value = dict(model=zero_state_dict) + + with patch("deeplabcut.pose_estimation_pytorch.runners.train.torch.load", load): + r = train_runners.build_training_runner( + runner_config, + model_folder=model_folder, + task=Task.BOTTOM_UP, + model=model, + device="cpu", + snapshot_path=model_folder / "snapshot.pt", + load_head_weights=load_head_weights, + ) + loaded_state_dict = r.model.state_dict() + for k, v in loaded_state_dict.items(): + if load_head_weights or k.startswith("backbone."): + assert torch.equal(v, zero_state_dict[k]) + else: + assert torch.equal(v, original_state_dict[k]) + + +@pytest.mark.parametrize("load_head_weights", [True, False]) +def test_mocked_load_head_weights(tmp_path_factory, load_head_weights): + model_folder = tmp_path_factory.mktemp("model_folder") + snapshot_manager = Mock() + snapshot_manager.model_folder = model_folder + + model = Mock() + model.backbone = Mock() + state_dict = {"backbone.test": 0, "head.test": 1} + state_dict_backbone = {"test": 0} + load = Mock() + load.return_value = dict(model=state_dict) + + with patch("deeplabcut.pose_estimation_pytorch.runners.train.torch.load", load): + _ = train_runners.PoseTrainingRunner( + model=model, + optimizer=Mock(), + snapshot_manager=snapshot_manager, + device="cpu", + snapshot_path="snapshot.pt", + load_head_weights=load_head_weights, + ) + if load_head_weights: + model.load_state_dict.assert_called_once_with(state_dict) + else: + model.backbone.load_state_dict.assert_called_once_with(state_dict_backbone) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_training_with_scheduler(runner_cls, test_cfg: SchedulerTestConfig) -> None: + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + test_cfg.expected_lrs, + ) + assert runner.current_epoch == len(test_cfg.expected_lrs) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_resuming_training_scheduler_every_epoch( + runner_cls, + test_cfg: SchedulerTestConfig, +): + snapshot_to_load = None + for epoch, expected_lr in enumerate(test_cfg.expected_lrs): + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + [expected_lr], # trains for 1 epoch + snapshot_to_load=snapshot_to_load, + ) + snapshot_to_load = dict(metadata=dict(epoch=epoch + 1), scheduler=runner.scheduler.state_dict()) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize( + "test_cfg, resume_epoch", + [ + ( + SchedulerTestConfig( + cfg=dict( + type="LRListScheduler", + params=dict(milestones=[2, 5], lr_list=[[0.5], [0.1]]), + ), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 0.5, 1.0, 1.0, 0.1, 0.1, 0.1], + ), + 3, # cut after the 3rd epoch - restart at LR=1 until epoch 5 + ), + ( + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=4, gamma=0.1)), + init_lr=1.0, + expected_lrs=(4 * [1.0]) + (4 * [0.1]) + (4 * [0.01]) + (4 * [0.001]), + ), + 3, # cut after the 3rd epoch - restart at LR=1 and update at 4 correctly + ), + ( + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=4, gamma=0.1)), + init_lr=1.0, + expected_lrs=(4 * [1.0]) + [0.1, 1, 1, 1] + (4 * [0.1]), + ), + 5, # cut after the 5th epoch - restart at LR=1 and update again at 8 + ), + ], +) +def test_resuming_training_with_no_scheduler_state(runner_cls, test_cfg: SchedulerTestConfig, resume_epoch: int): + """Without a scheduler config, there is no way to set the initial LR. + + All we can do is set the last_epoch value, and adjust correctly at milestones going + forward. + """ + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + test_cfg.expected_lrs[:resume_epoch], + ) + assert runner.current_epoch == resume_epoch + + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + expected_lrs=test_cfg.expected_lrs[resume_epoch:], + snapshot_to_load=dict(metadata=dict(epoch=resume_epoch)), + ) + assert runner.current_epoch == len(test_cfg.expected_lrs) + + +def _fit_runner_and_check_lrs( + runner_cls, + init_lr: float, + scheduler_cfg: dict, + expected_lrs: list[float], + snapshot_to_load: dict | None = None, +) -> train_runners.TrainingRunner: + runner_kwargs = dict(device="cpu", eval_interval=1_000_000) + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=init_lr) + scheduler = schedulers.build_scheduler(scheduler_cfg, optimizer) + num_epochs = len(expected_lrs) + + base_path = "deeplabcut.pose_estimation_pytorch.runners" + with patch(f"{base_path}.base.Runner.load_snapshot") as base_mock_load: + with patch(f"{base_path}.train.PoseTrainingRunner.load_snapshot") as mock_load: + snapshot_path = None + base_mock_load.return_value = dict() + mock_load.return_value = dict() + if snapshot_to_load is not None: + snapshot_path = "fake_snapshot.pt" + base_mock_load.return_value = snapshot_to_load + mock_load.return_value = snapshot_to_load + + print() + print(f"Scheduler: {scheduler}") + print(f"Starting training for {num_epochs} epochs") + runner = runner_cls( + model=Mock(), + optimizer=optimizer, + snapshot_manager=Mock(), + scheduler=scheduler, + snapshot_path=snapshot_path, + **runner_kwargs, + ) + + # Mock the step call; check that the learning rate is correct for the epoch + def step(*args, **kwargs): + # the current_epoch value is indexed at 1 + total_epoch = runner.current_epoch - 1 + epoch = total_epoch - runner.starting_epoch + _assert_learning_rates_match(total_epoch, optimizer, expected_lrs[epoch]) + optimizer.step() + return dict(total_loss=0) + + train_loader, val_loader = [Mock()], [Mock()] + runner.step = step + runner.fit(train_loader, val_loader, epochs=num_epochs, display_iters=1000) + + return runner + + +def _assert_learning_rates_match(e, optimizer, expected): + current_lrs = [g["lr"] for g in optimizer.param_groups] + print(f"Epoch {e}: LR={current_lrs}, expected={expected}") + for lr in current_lrs: + assert isinstance(lr, float) + np.testing.assert_almost_equal(lr, expected) diff --git a/tests/pose_estimation_pytorch/runners/test_schedulers.py b/tests/pose_estimation_pytorch/runners/test_schedulers.py new file mode 100644 index 0000000000..37e98ea6a1 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_schedulers.py @@ -0,0 +1,271 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests building schedulers from config.""" + +import random +from dataclasses import dataclass + +import numpy as np +import pytest +import torch +import torch.nn as nn + +import deeplabcut.pose_estimation_pytorch.runners.schedulers as schedulers + + +def generate_random_lr_list(num_floats: int): + """Generate list of lists including random numbers. + + Args: + num_floats: number of floats we want to include in our list + + Returns: + ran_list: random list of sorted numbers, being first number bigger than the last + """ + ran_list = [] + for i in range(num_floats): + random_floats = [random.random()] + ran_list.append(random_floats) + return sorted(ran_list, reverse=True) + + +@pytest.mark.parametrize( + "milestones, lr_list", + [([10, 430], [[0.05], [0.005]]), (list(sorted(random.sample(range(0, 999), 2))), generate_random_lr_list(2))], +) +def test_scheduler(milestones, lr_list): + """Testing schedulers.py. + + Given a list of milestones and a list of learning rates, this function tests + if the length of each list is the same. Furthermore, it will assess if + the current learning rate (output from the function we are testing) is a float + and corresponds to the expected learning rate given the milestones. + + Args: + milestones: list of epochs indices (number of epochs) + lr_list: learning rates list + + Returns: + None + + Examples: + input: + milestones = [10,25,50] + lr_list = [[0.00001],[0.000005],[0.000001]] + """ + + assert len(milestones) == len(lr_list) + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=0.01) + s = schedulers.LRListScheduler(optimizer, milestones=milestones, lr_list=lr_list) + + index_rng = range(milestones[0], milestones[1]) + for i in range((milestones[-1]) + 1): + if i < milestones[0]: + expected_lr = [0.01] + elif i in index_rng: + expected_lr = lr_list[0] + else: + expected_lr = lr_list[1] + + current_lr = s.get_lr()[0] + assert s.get_lr() == expected_lr + assert isinstance(current_lr, float) + optimizer.step() + s.step() + + +@dataclass +class SchedulerTestConfig: + cfg: dict + init_lr: float + expected_lrs: list[float] + + +TEST_SCHEDULERS = [ + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[2, 5], lr_list=[[0.5], [0.1]])), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 0.5, 0.5, 0.5, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.1]])), + init_lr=0.1, + expected_lrs=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.5]])), + init_lr=0.1, + expected_lrs=[0.1, 0.5, 0.5, 0.5], + ), + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=3, gamma=0.1)), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 1.0, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01, 0.001], + ), +] + + +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_build_scheduler(test_cfg: SchedulerTestConfig) -> None: + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + print() + print(f"Scheduler: {s}") + num_epochs = len(test_cfg.expected_lrs) + for e in range(num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_resume_scheduler_after_each_epoch(test_cfg: SchedulerTestConfig) -> None: + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + print() + print(f"Scheduler: {s}") + num_epochs = len(test_cfg.expected_lrs) + for e in range(num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + new_scheduler = schedulers.build_scheduler(test_cfg.cfg, optimizer) + schedulers.load_scheduler_state(new_scheduler, s.state_dict()) + s = new_scheduler + + +@pytest.mark.parametrize( + "test_cfg, middle_epoch", + [ + (TEST_SCHEDULERS[0], 3), + (TEST_SCHEDULERS[1], 5), + (TEST_SCHEDULERS[2], 2), + (TEST_SCHEDULERS[3], 2), + (TEST_SCHEDULERS[3], 3), + (TEST_SCHEDULERS[3], 4), + ], +) +def test_two_stage_training(test_cfg: SchedulerTestConfig, middle_epoch: int) -> None: + num_epochs = len(test_cfg.expected_lrs) + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + + print() + print(f"Scheduler: {s}") + for e in range(middle_epoch): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + new_scheduler = schedulers.build_scheduler(test_cfg.cfg, optimizer) + schedulers.load_scheduler_state(new_scheduler, s.state_dict()) + s = new_scheduler + for e in range(middle_epoch, num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + s.step() + + +@pytest.mark.parametrize( + "data", + [ + dict( # example with 3 warm-up epochs + config=dict( + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=3), + ), + ), + start_lr=1.0, + expected_lrs=[[0.1], [0.1], [0.1], [1.0], [1.0]], + ), + dict( # example from torch.optim.lr_scheduler.SequentialLR + config=dict( + type="SequentialLR", + params=dict( + schedulers=[ + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=2), + ), + dict(type="ExponentialLR", params=dict(gamma=0.9)), + ], + milestones=[2], + ), + ), + start_lr=1.0, + expected_lrs=[[0.1], [0.1], [1.0], [0.9], [0.81], [0.729]], + ), + dict( # example from torch.optim.lr_scheduler.SequentialLR + config=dict( + type="SequentialLR", + params=dict( + schedulers=[ + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=2), + ), + dict(type="StepLR", params=dict(step_size=2, gamma=0.1)), + ], + milestones=[5], + ), + ), + start_lr=1.0, + expected_lrs=[ + [0.1], + [0.1], + [1.0], + [1.0], + [1.0], # ConstantLR + [1.0], + [1.0], + [0.1], + [0.1], + [0.01], # StepLR + ], + ), + ], +) +def test_build_sequential_lr(data): + print("\nTESTING") + start_lr = data["start_lr"] + print(f"Start LR: {start_lr}") + model = nn.Linear(in_features=1, out_features=1) + optimizer = torch.optim.SGD(params=model.parameters(), lr=start_lr) + + print("BUILDING") + scheduler = schedulers.build_scheduler(data["config"], optimizer) + + print("RUNNING") + lrs = [] + for epoch in range(len(data["expected_lrs"])): + lrs.append(scheduler.get_last_lr()) + print(scheduler.get_last_lr()) + scheduler.step() + + print(f"Expected: {data['expected_lrs']}") + print(f"Actual: {lrs}") + np.testing.assert_allclose( + np.asarray(data["expected_lrs"]), + np.asarray(lrs), + atol=1e-10, + ) + + +def _assert_learning_rates_match(e, optimizer, expected): + current_lrs = [g["lr"] for g in optimizer.param_groups] + print(f"Epoch {e}: LR={current_lrs}, expected={expected}") + for lr in current_lrs: + assert isinstance(lr, float) + np.testing.assert_almost_equal(lr, expected) diff --git a/tests/pose_estimation_pytorch/runners/test_shelving.py b/tests/pose_estimation_pytorch/runners/test_shelving.py new file mode 100644 index 0000000000..741c3c53c4 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_shelving.py @@ -0,0 +1,160 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for ShelfWriter / ShelfReader.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.runners.shelving import ( + ShelfReader, + ShelfWriter, +) + +POSE_CFG = { + "all_joints": [[0], [1], [2]], + "all_joints_names": ["snout", "leftear", "rightear"], + "nmsradius": 5, + "minconfidence": 0.1, + "sigma": 1, +} + + +def _make_bodyparts(num_assemblies: int = 2, num_bpts: int = 3) -> np.ndarray: + """(num_assemblies, num_bpts, 3) — x, y, score.""" + rng = np.random.default_rng(0) + return rng.random((num_assemblies, num_bpts, 3)).astype(np.float32) + + +# -- lifecycle ---------------------------------------------------------------- + + +def test_write_before_open_raises(tmp_path): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf") + with pytest.raises(ValueError, match="open"): + writer.add_prediction(_make_bodyparts()) + + +def test_open_close_roundtrip(tmp_path): + path = tmp_path / "shelf" + writer = ShelfWriter(POSE_CFG, path) + writer.open() + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(path) + reader.open() + assert "metadata" in reader.keys() + assert "frame00000" in reader.keys() + reader.close() + + +# -- key formatting ----------------------------------------------------------- + + +@pytest.mark.parametrize("num_frames,width", [(9, 1), (100, 2), (1000, 3)]) +def test_key_str_width(tmp_path, num_frames, width): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=num_frames) + writer.open() + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + expected_key = "frame" + "0".zfill(width) + assert expected_key in reader.keys() + reader.close() + + +# -- data shape --------------------------------------------------------------- + + +def test_add_prediction_stores_correct_shapes(tmp_path): + num_assemblies, num_bpts = 2, 3 + bp = _make_bodyparts(num_assemblies, num_bpts) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=10) + writer.open() + writer.add_prediction(bp) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + + coords = data["coordinates"][0] + assert len(coords) == num_bpts + assert coords[0].shape == (num_assemblies, 2) + + scores = data["confidence"] + assert len(scores) == num_bpts + assert scores[0].shape == (num_assemblies, 1) + reader.close() + + +# -- metadata on close -------------------------------------------------------- + + +def test_metadata_nframes_updated_on_close(tmp_path): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=100) + writer.open() + for _ in range(3): + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + assert reader["metadata"]["nframes"] == 3 + reader.close() + + +# -- unique bodyparts --------------------------------------------------------- + + +def test_unique_bodyparts_appended(tmp_path): + num_assemblies, num_bpts, num_unique = 2, 3, 1 + bp = _make_bodyparts(num_assemblies, num_bpts) + ubp = np.random.default_rng(1).random((num_assemblies, num_unique, 3)).astype(np.float32) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) + writer.open() + writer.add_prediction(bp, unique_bodyparts=ubp) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + assert len(data["coordinates"][0]) == num_bpts + num_unique + assert len(data["confidence"]) == num_bpts + num_unique + reader.close() + + +# -- identity scores ---------------------------------------------------------- + + +def test_identity_scores_stored(tmp_path): + num_assemblies, num_bpts, num_individuals = 2, 3, 2 + bp = _make_bodyparts(num_assemblies, num_bpts) + ids = np.random.default_rng(2).random((num_assemblies, num_bpts, num_individuals)).astype(np.float32) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) + writer.open() + writer.add_prediction(bp, identity_scores=ids) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + assert "identity" in data + assert len(data["identity"]) == num_bpts + assert data["identity"][0].shape == (num_assemblies, num_individuals) + reader.close() diff --git a/tests/pose_estimation_pytorch/runners/test_task.py b/tests/pose_estimation_pytorch/runners/test_task.py new file mode 100644 index 0000000000..2f821d0aa3 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_task.py @@ -0,0 +1,28 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests the Task enum.""" + +import pytest + +from deeplabcut.pose_estimation_pytorch.task import Task + + +@pytest.mark.parametrize( + "task, task_strings", + [ + (Task.BOTTOM_UP, ["bu", "BU", "bU", "Bu"]), + (Task.TOP_DOWN, ["TD", "tD"]), + (Task.DETECT, ["dt", "DT"]), + ], +) +def test_build_task(task: Task, task_strings: list[str]): + for s in task_strings: + assert task == Task(s) diff --git a/tests/test_auxfun_models.py b/tests/test_auxfun_models.py index c7830b1b41..0684da7651 100644 --- a/tests/test_auxfun_models.py +++ b/tests/test_auxfun_models.py @@ -4,15 +4,15 @@ # https://github.com/DeepLabCut/DeepLabCut # # Please see AUTHORS for contributors. -# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS # # Licensed under GNU Lesser General Public License v3.0 # +import unittest from pathlib import Path from tempfile import TemporaryDirectory -import unittest from unittest.mock import patch from deeplabcut.utils.auxfun_models import MODELTYPE_FILEPATH_MAP, check_for_weights @@ -21,23 +21,15 @@ class CheckForWeightsTestCase(unittest.TestCase): def test_filepaths_for_modeltypes(self): with TemporaryDirectory() as tmpdir: - with patch( - "deeplabcut.utils.auxfun_models.download_weights" - ) as mocked_download: + with patch("deeplabcut.utils.auxfun_models.download_weights") as mocked_download: for modeltype, expected_path in MODELTYPE_FILEPATH_MAP.items(): actual_path = check_for_weights(modeltype, Path(tmpdir)) self.assertIn(str(expected_path), actual_path) if "efficientnet" in modeltype: - mocked_download.assert_called_with( - modeltype, tmpdir / expected_path.parent - ) + mocked_download.assert_called_with(modeltype, tmpdir / expected_path.parent) else: - mocked_download.assert_called_with( - modeltype, tmpdir / expected_path - ) + mocked_download.assert_called_with(modeltype, tmpdir / expected_path) def test_bad_modeltype(self): - actual_path = check_for_weights( - "dummymodel", "nonexistentpath" - ) + actual_path = check_for_weights("dummymodel", "nonexistentpath") self.assertEqual(actual_path, "nonexistentpath") diff --git a/tests/test_auxfun_multianimal.py b/tests/test_auxfun_multianimal.py index 1dbd67d2f6..8e8b7d28dc 100644 --- a/tests/test_auxfun_multianimal.py +++ b/tests/test_auxfun_multianimal.py @@ -8,12 +8,14 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from itertools import combinations + import networkx as nx import numpy as np import pandas as pd import pytest + from deeplabcut.utils import auxfun_multianimal -from itertools import combinations def test_prune_paf_graph(): @@ -44,9 +46,7 @@ def test_reorder_individuals_in_df(): individuals = df.columns.get_level_values("individuals").unique().to_list() # Generate a random permutation and reorder data. Ignore the unique bodypart - permutation_indices = random.sample( - range(len(individuals[:-1])), k=len(individuals[:-1]) - ) + permutation_indices = random.sample(range(len(individuals[:-1])), k=len(individuals[:-1])) permutation = [individuals[i] for i in permutation_indices] permutation.append("single") df_reordered = auxfun_multianimal.reorder_individuals_in_df(df, permutation) @@ -56,9 +56,7 @@ def test_reorder_individuals_in_df(): inverse_permutation_indices = np.argsort(permutation_indices).tolist() inverse_permutation = [individuals[i] for i in inverse_permutation_indices] inverse_permutation.append("single") - df_inverse_reordering = auxfun_multianimal.reorder_individuals_in_df( - df_reordered, inverse_permutation - ) + df_inverse_reordering = auxfun_multianimal.reorder_individuals_in_df(df_reordered, inverse_permutation) # Check pd.testing.assert_frame_equal(df, df_inverse_reordering) diff --git a/tests/test_auxiliaryfunctions.py b/tests/test_auxiliaryfunctions.py index 7397060897..ff16dc1c1e 100644 --- a/tests/test_auxiliaryfunctions.py +++ b/tests/test_auxiliaryfunctions.py @@ -9,7 +9,9 @@ # Licensed under GNU Lesser General Public License v3.0 # from pathlib import Path + import pytest + from deeplabcut.utils import auxiliaryfunctions from deeplabcut.utils.auxfun_videos import SUPPORTED_VIDEOS @@ -17,7 +19,7 @@ def test_find_analyzed_data(tmpdir_factory): fake_folder = tmpdir_factory.mktemp("videos") SUPPORTED_VIDEOS = ["avi"] - n_ext = len(SUPPORTED_VIDEOS) + len(SUPPORTED_VIDEOS) SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle1_5" WRONG_SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle3_5" @@ -36,22 +38,18 @@ def _create_fake_file(filename): for ind, ext in enumerate(SUPPORTED_VIDEOS): # test if existing models are found: - assert auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), SCORER - ) + assert auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), SCORER) # Test if nonexisting models are not found with pytest.raises(FileNotFoundError): - auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), WRONG_SCORER - ) + auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), WRONG_SCORER) with pytest.raises(FileNotFoundError): - auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), SCORER, filtered=True - ) + auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), SCORER, filtered=True) +@pytest.mark.deprecated +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_get_list_of_videos(tmpdir_factory): fake_folder = tmpdir_factory.mktemp("videos") n_ext = len(SUPPORTED_VIDEOS) @@ -121,7 +119,7 @@ def _create_fake_file(filename): def test_write_config_has_skeleton(tmpdir_factory): - """Required for backward compatibility""" + """Required for backward compatibility.""" fake_folder = tmpdir_factory.mktemp("fakeConfigs") fake_config_file = fake_folder / Path("fakeConfig") auxiliaryfunctions.write_config(fake_config_file, {}) @@ -165,21 +163,15 @@ def test_intersection_of_body_parts_and_ones_given_by_user( else: all_bodyparts = bodyparts - filtered_bpts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts="all" - ) - ) + filtered_bpts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts="all") print(all_bodyparts) print(filtered_bpts) assert len(all_bodyparts) == len(filtered_bpts) assert all([bpt in all_bodyparts for bpt in filtered_bpts]) - filtered_bpts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, - comparisonbodyparts=comparison_bpts, - ) + filtered_bpts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( + cfg, + comparisonbodyparts=comparison_bpts, ) print(filtered_bpts) assert len(expected_bpts) == len(filtered_bpts) @@ -230,3 +222,50 @@ def get_rglob_results(*args, **kwargs): monkeypatch.setattr(Path, "rglob", get_rglob_results) next_folder = auxiliaryfunctions.find_next_unlabeled_folder(fake_cfg) assert str(next_folder) == str(Path(data_folder / next_folder_name)) + + +@pytest.fixture +def mock_snapshot_folder(tmp_path): + """Mock folder with snapshots.""" + folder = tmp_path / "train" + folder.mkdir() + + # mock files + snapshot_files = [ + "snapshot-4.index", + "snapshot-5.index", + "snapshot-6.index", + "snapshot-3.data-00000-of-00001", + "snapshot-3.index", + "snapshot-3.meta", + ] + for file_name in snapshot_files: + (folder / file_name).touch() + + return folder + + +@pytest.fixture +def mock_no_snapshots_folder(tmp_path): + """Mock folder with no snapshots.""" + folder = tmp_path / "train" + folder.mkdir() + + # mock files + snapshot_files = ["log.txt", "pose_cfg.yaml"] + for file_name in snapshot_files: + (folder / file_name).touch() + + return folder + + +def test_get_snapshots_from_folder(mock_snapshot_folder): + """Test returns expected snapshots in order.""" + snapshot_names = auxiliaryfunctions.get_snapshots_from_folder(mock_snapshot_folder) + assert snapshot_names == ["snapshot-3", "snapshot-4", "snapshot-5", "snapshot-6"] + + +def test_get_snapshots_from_folder_none(mock_no_snapshots_folder): + """Test raises ValueError if no snapshots are found.""" + with pytest.raises(FileNotFoundError): + auxiliaryfunctions.get_snapshots_from_folder(mock_no_snapshots_folder) diff --git a/tests/test_conversioncode.py b/tests/test_conversioncode.py index 3ec57e0197..ca287ba861 100644 --- a/tests/test_conversioncode.py +++ b/tests/test_conversioncode.py @@ -9,8 +9,10 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + import pandas as pd from conftest import TEST_DATA_DIR + from deeplabcut.utils import conversioncode diff --git a/tests/test_crossvalutils.py b/tests/test_crossvalutils.py index b501a7f9a6..5c821bd03e 100644 --- a/tests/test_crossvalutils.py +++ b/tests/test_crossvalutils.py @@ -8,10 +8,11 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import pickle -from deeplabcut.pose_estimation_tensorflow.lib import crossvalutils +import numpy as np + +from deeplabcut.core import crossvalutils BEST_GRAPH = [14, 15, 16, 11, 22, 31, 61, 7, 59, 62, 64] BEST_GRAPH_MONTBLANC = [1, 0, 2, 5, 4, 3] @@ -21,9 +22,7 @@ def test_get_n_best_paf_graphs(evaluation_data_and_metadata): data, metadata = evaluation_data_and_metadata params = crossvalutils._set_up_evaluation(data) n_graphs = 5 - paf_inds, dict_ = crossvalutils._get_n_best_paf_graphs( - data, metadata, params["paf_graph"], n_graphs=n_graphs - ) + paf_inds, dict_ = crossvalutils._get_n_best_paf_graphs(data, metadata, params["paf_graph"], n_graphs=n_graphs) assert len(paf_inds) == n_graphs assert len(dict_) == len(params["paf_graph"]) assert len(paf_inds[0]) == 11 @@ -67,9 +66,7 @@ def test_benchmark_paf_graphs(evaluation_data_and_metadata): ], } inference_cfg = {"topktoretain": 3, "pcutoff": 0.1, "pafthreshold": 0.1} - results = crossvalutils._benchmark_paf_graphs( - cfg, inference_cfg, data, [BEST_GRAPH] - ) + results = crossvalutils._benchmark_paf_graphs(cfg, inference_cfg, data, [BEST_GRAPH]) all_scores = results[0] assert len(all_scores) == 1 assert all_scores[0][1] == BEST_GRAPH @@ -103,8 +100,8 @@ def test_benchmark_paf_graphs_montblanc(evaluation_data_and_metadata_montblanc): np.testing.assert_equal( results[1].loc["purity"].to_numpy().squeeze(), [ - results_gt[0][6][('purity', 'mean')], - results_gt[0][6][('purity', 'std')], + results_gt[0][6][("purity", "mean")], + results_gt[0][6][("purity", "std")], ], ) vals = [ @@ -116,9 +113,9 @@ def test_benchmark_paf_graphs_montblanc(evaluation_data_and_metadata_montblanc): np.testing.assert_equal( vals, [ - results_gt[0][6][('mAP_train', 'mean')], - results_gt[0][6][('mAR_train', 'mean')], - results_gt[0][6][('mAP_test', 'mean')], - results_gt[0][6][('mAR_test', 'mean')], + results_gt[0][6][("mAP_train", "mean")], + results_gt[0][6][("mAR_train", "mean")], + results_gt[0][6][("mAP_test", "mean")], + results_gt[0][6][("mAR_test", "mean")], ], ) diff --git a/tests/test_dataset_augmentation.py b/tests/test_dataset_augmentation.py index a11dd148d0..9e935c7259 100644 --- a/tests/test_dataset_augmentation.py +++ b/tests/test_dataset_augmentation.py @@ -11,8 +11,14 @@ import imgaug.augmenters as iaa import numpy as np import pytest + from deeplabcut.pose_estimation_tensorflow.datasets import augmentation +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + @pytest.mark.parametrize( "width, height", @@ -98,9 +104,10 @@ def test_keypoint_horizontal_flip( keypoints=list(map(str, range(12))), symmetric_pairs=pairs, ) - keypoints_aug = aug(images=[sample_image], keypoints=[sample_keypoints],)[ - 1 - ][0] + keypoints_aug = aug( + images=[sample_image], + keypoints=[sample_keypoints], + )[1][0] temp = keypoints_aug.reshape((3, 12, 2)) for pair in pairs: temp[:, pair] = temp[:, pair[::-1]] diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py index e37e4822e0..69ea13d7c6 100644 --- a/tests/test_evaluate.py +++ b/tests/test_evaluate.py @@ -13,6 +13,15 @@ import pytest import deeplabcut.pose_estimation_tensorflow as pet +from deeplabcut.pose_estimation_tensorflow.core.evaluate import ( + get_available_requested_snapshots, + get_snapshots_by_index, +) + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) def make_single_animal_rmse_df( @@ -39,9 +48,7 @@ def make_multi_animal_rmse_df( names=["scorer", "individuals", "bodyparts"], ) if error_data is None: - error_data = np.ones( - (len(train_indices) + len(test_indices), len(individuals) * len(bodyparts)) - ) + error_data = np.ones((len(train_indices) + len(test_indices), len(individuals) * len(bodyparts))) return pd.DataFrame(error_data, columns=columns) @@ -152,3 +159,75 @@ def test_evaluate_keypoint_error(inputs, expected_values): mean_error = mean_errors[1] assert keypoint_error.loc[error_name, bodypart] == mean_error + + +def test_get_available_requested_snapshots_ok(): + """Test that the correct snapshots are returned.""" + available = ["snapshot-1", "snapshot-2"] + requested = ["snapshot-2", "snapshot-3"] + + snapshots = get_available_requested_snapshots( + requested_snapshots=requested, + available_snapshots=available, + ) + assert snapshots == ["snapshot-2"] + + +def test_get_available_requested_snapshots_error(): + """Test that a ValueError is raised when requested snapshots are not available.""" + with pytest.raises(ValueError): + get_available_requested_snapshots( + requested_snapshots=["snapshot-2"], + available_snapshots=["snapshot-1", "snapshot-3"], + ) + + +def test_get_snapshots_by_index_int_ok(): + """Test that the correct snapshots are returned.""" + available = ["snapshot-1", "snapshot-2", "snapshot-3"] + + # positive int + snapshots = get_snapshots_by_index( + idx=2, + available_snapshots=available, + ) + assert snapshots == ["snapshot-3"] + + # negative int + snapshots = get_snapshots_by_index( + idx=-2, + available_snapshots=available, + ) + assert snapshots == ["snapshot-2"] + + # all snapshots + snapshots = get_snapshots_by_index( + idx="all", + available_snapshots=available, + ) + assert snapshots == ["snapshot-1", "snapshot-2", "snapshot-3"] + + +def test_get_snapshots_by_index_error(): + """Test that a ValueError is raised when the index is out of range or invalid + str.""" + available = ["snapshot-1", "snapshot-2", "snapshot-3"] + + # positive int + with pytest.raises(IndexError): + get_snapshots_by_index( + idx=5, + available_snapshots=available, + ) + # negative int + with pytest.raises(IndexError): + get_snapshots_by_index( + idx=-4, + available_snapshots=available, + ) + # invalid str + with pytest.raises(IndexError): + get_snapshots_by_index( + idx="1", + available_snapshots=available, + ) diff --git a/tests/test_frame_selection_tools.py b/tests/test_frame_selection_tools.py index ddee2346c7..17b615ad61 100644 --- a/tests/test_frame_selection_tools.py +++ b/tests/test_frame_selection_tools.py @@ -8,10 +8,13 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Tests for frame selection tools """ +"""Tests for frame selection tools.""" + import math from unittest.mock import Mock + import pytest + import deeplabcut.utils.frameselectiontools as fst diff --git a/tests/test_inferenceutils.py b/tests/test_inferenceutils.py index f44aad610f..cd52c8cd36 100644 --- a/tests/test_inferenceutils.py +++ b/tests/test_inferenceutils.py @@ -8,15 +8,17 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os import pickle +from copy import deepcopy + +import numpy as np import pytest from conftest import TEST_DATA_DIR -from copy import deepcopy -from deeplabcut.pose_estimation_tensorflow.lib import inferenceutils from scipy.spatial.distance import squareform +from deeplabcut.core import inferenceutils + def test_conv_square_to_condensed_indices(): n = 5 @@ -25,7 +27,7 @@ def test_conv_square_to_condensed_indices(): mat[rows, cols] = mat[cols, rows] = np.arange(1, len(rows) + 1) vec = squareform(mat) vals = [] - for i, j in zip(rows, cols): + for i, j in zip(rows, cols, strict=False): ind = inferenceutils._conv_square_to_condensed_indices(i, j, n) vals.append(vec[ind]) np.testing.assert_equal(vec, vals) @@ -36,9 +38,7 @@ def test_calc_object_keypoint_similarity(real_assemblies): xy1 = real_assemblies[0][0].xy xy2 = real_assemblies[0][1].xy assert inferenceutils.calc_object_keypoint_similarity(xy1, xy1, sigma) == 1 - assert np.isclose( - inferenceutils.calc_object_keypoint_similarity(xy1, xy2, sigma), 0 - ) + assert np.isclose(inferenceutils.calc_object_keypoint_similarity(xy1, xy2, sigma), 0) xy3 = xy1.copy() xy3[: len(xy3) // 2] = np.nan assert inferenceutils.calc_object_keypoint_similarity(xy3, xy1, sigma) == 0.5 @@ -51,36 +51,27 @@ def test_calc_object_keypoint_similarity(real_assemblies): symmetric_pair = [0, 11] xy4[symmetric_pair] = xy4[symmetric_pair[::-1]] assert inferenceutils.calc_object_keypoint_similarity(xy1, xy4, sigma) != 1 - assert ( - inferenceutils.calc_object_keypoint_similarity( - xy1, xy4, sigma, symmetric_kpts=[symmetric_pair] - ) - == 1 - ) + assert inferenceutils.calc_object_keypoint_similarity(xy1, xy4, sigma, symmetric_kpts=[symmetric_pair]) == 1 def test_match_assemblies(real_assemblies): assemblies = real_assemblies[0] - matched, unmatched = inferenceutils.match_assemblies( - assemblies, assemblies[::-1], 0.01 - ) - assert not unmatched - for ass1, ass2, oks in matched: - assert ass1 is ass2 - assert oks == 1 + num_gt, matches = inferenceutils.match_assemblies(assemblies, assemblies[::-1], 0.01) + assert len(assemblies) == len(matches) + for m in matches: + assert m.prediction is m.ground_truth + assert m.oks == 1 - matched, unmatched = inferenceutils.match_assemblies([], assemblies, 0.01) - assert not matched - assert all(ass1 is ass2 for ass1, ass2 in zip(unmatched, assemblies)) + num_gt, matches = inferenceutils.match_assemblies([], assemblies, 0.01) + assert len(matches) == 0 + assert num_gt == len(assemblies) def test_evaluate_assemblies(real_assemblies): assemblies = {i: real_assemblies[i] for i in range(3)} n_thresholds = 5 thresholds = np.linspace(0.5, 0.95, n_thresholds) - dict_ = inferenceutils.evaluate_assembly( - assemblies, assemblies, oks_thresholds=thresholds - ) + dict_ = inferenceutils.evaluate_assembly(assemblies, assemblies, oks_thresholds=thresholds) assert dict_["mAP"] == dict_["mAR"] == 1 assert len(dict_["precisions"]) == len(dict_["recalls"]) == n_thresholds assert dict_["precisions"].shape[1] == 101 @@ -107,7 +98,7 @@ def test_link(): j1 = inferenceutils.Joint(pos1, conf, idx=idx1) j2 = inferenceutils.Joint(pos2, conf, idx=idx2) link = inferenceutils.Link(j1, j2) - assert link.confidence == conf ** 2 + assert link.confidence == conf**2 assert link.idx == (idx1, idx2) assert link.to_vector() == [*pos1, *pos2] @@ -172,13 +163,11 @@ def test_assembler(tmpdir_factory, real_assemblies): ass.assemble() assert not ass.unique assert len(ass.assemblies) == len(real_assemblies) - assert sum(1 for a in ass.assemblies.values() for _ in a) == sum( - 1 for a in real_assemblies.values() for _ in a - ) + assert sum(1 for a in ass.assemblies.values() for _ in a) == sum(1 for a in real_assemblies.values() for _ in a) - output_name = tmpdir_factory.mktemp("data").join("fake.h5") - ass.to_h5(output_name) - ass.to_pickle(str(output_name).replace("h5", "pickle")) + output_dir = tmpdir_factory.mktemp("data") + ass.to_h5(output_dir.join("fake.h5")) + ass.to_pickle(output_dir.join("fake.pickle")) def test_assembler_with_single_bodypart(real_assemblies): @@ -223,15 +212,9 @@ def test_assembler_with_unique_bodypart(real_assemblies_montblanc): ass.assemble(chunk_size=0) assert len(ass.assemblies) == len(real_assemblies_montblanc[0]) assert len(ass.unique) == len(real_assemblies_montblanc[1]) - assemblies = np.concatenate( - [ass.xy for assemblies in ass.assemblies.values() for ass in assemblies] - ) + assemblies = np.concatenate([ass.xy for assemblies in ass.assemblies.values() for ass in assemblies]) assemblies_gt = np.concatenate( - [ - ass.xy - for assemblies in real_assemblies_montblanc[0].values() - for ass in assemblies - ] + [ass.xy for assemblies in real_assemblies_montblanc[0].values() for ass in assemblies] ) np.testing.assert_equal(assemblies, assemblies_gt) @@ -270,9 +253,7 @@ def test_assembler_with_identity(tmpdir_factory, real_assemblies): ass.assemble() assert not ass.unique assert len(ass.assemblies) == len(real_assemblies) - assert sum(1 for a in ass.assemblies.values() for _ in a) == sum( - 1 for a in real_assemblies.values() for _ in a - ) + assert sum(1 for a in ass.assemblies.values() for _ in a) == sum(1 for a in real_assemblies.values() for _ in a) assert all(np.all(_.data[:, -1] != -1) for a in ass.assemblies.values() for _ in a) # Test now with identity only and ensure assemblies @@ -288,9 +269,9 @@ def test_assembler_with_identity(tmpdir_factory, real_assemblies): eq.append(np.all(ids == ids[0])) assert all(eq) - output_name = tmpdir_factory.mktemp("data").join("fake.h5") - ass.to_h5(output_name) - ass.to_pickle(str(output_name).replace("h5", "pickle")) + output_dir = tmpdir_factory.mktemp("data") + ass.to_h5(output_dir.join("fake.h5")) + ass.to_pickle(output_dir.join("fake.pickle")) def test_assembler_calibration(real_assemblies): diff --git a/tests/test_pose_multianimal_imgaug.py b/tests/test_pose_multianimal_imgaug.py index c0c8a8c5b0..d0f1debab8 100644 --- a/tests/test_pose_multianimal_imgaug.py +++ b/tests/test_pose_multianimal_imgaug.py @@ -8,17 +8,24 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os + +import numpy as np import pytest from conftest import TEST_DATA_DIR + from deeplabcut.pose_estimation_tensorflow.datasets import ( Batch, - pose_multianimal_imgaug, PoseDatasetFactory, + pose_multianimal_imgaug, ) from deeplabcut.utils import read_plainconfig +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + def mock_imread(path, mode): return (np.random.rand(400, 400, 3) * 255).astype(np.uint8) @@ -67,19 +74,13 @@ def test_get_batch(ma_dataset): for batch_size in 1, 4, 8, 16: ma_dataset.batch_size = batch_size batch_images, joint_ids, batch_joints, data_items = ma_dataset.get_batch() - assert ( - len(batch_images) - == len(joint_ids) - == len(batch_joints) - == len(data_items) - == batch_size - ) - for data_item, joint_id, batch_joint in zip(data_items, joint_ids, batch_joints): + assert len(batch_images) == len(joint_ids) == len(batch_joints) == len(data_items) == batch_size + for data_item, joint_id, batch_joint in zip(data_items, joint_ids, batch_joints, strict=False): assert len(data_item.joints) == len(joint_id) assert len(batch_joint) == len(np.concatenate(joint_id)) start = 0 mask = ~np.isnan(batch_joint).any(axis=1) - for joints, id_ in zip(data_item.joints.values(), joint_id): + for joints, id_ in zip(data_item.joints.values(), joint_id, strict=False): inds = id_ + start mask_ = mask[inds] np.testing.assert_equal(joints[:, 0], id_[mask_]) @@ -100,22 +101,14 @@ def test_get_targetmaps(ma_dataset, num_idchannel): scale = np.mean(target_size / ma_dataset.default_size) maps = ma_dataset.get_targetmaps_update(*batch, sm_size, scale) assert all(len(map_) == ma_dataset.batch_size for map_ in maps.values()) - assert ( - maps[Batch.part_score_targets][0].shape - == maps[Batch.part_score_weights][0].shape - ) - assert ( - maps[Batch.part_score_targets][0].shape[2] - == ma_dataset.cfg["num_joints"] + num_idchannel - ) + assert maps[Batch.part_score_targets][0].shape == maps[Batch.part_score_weights][0].shape + assert maps[Batch.part_score_targets][0].shape[2] == ma_dataset.cfg["num_joints"] + num_idchannel assert maps[Batch.locref_targets][0].shape == maps[Batch.locref_mask][0].shape assert maps[Batch.locref_targets][0].shape[2] == 2 * ma_dataset.cfg["num_joints"] - assert ( - maps[Batch.pairwise_targets][0].shape == maps[Batch.pairwise_targets][0].shape - ) + assert maps[Batch.pairwise_targets][0].shape == maps[Batch.pairwise_targets][0].shape assert maps[Batch.pairwise_targets][0].shape[2] == 2 * ma_dataset.cfg["num_limbs"] def test_batching(ma_dataset): for _ in range(10): - batch = ma_dataset.next_batch() + ma_dataset.next_batch() diff --git a/tests/test_predict_multianimal.py b/tests/test_predict_multianimal.py index eb9bbd7d16..4646a6ca93 100644 --- a/tests/test_predict_multianimal.py +++ b/tests/test_predict_multianimal.py @@ -9,9 +9,14 @@ # Licensed under GNU Lesser General Public License v3.0 # import numpy as np -import tensorflow as tf +import pytest + from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) RADIUS = 5 THRESHOLD = 0.01 @@ -66,14 +71,10 @@ def test_association_costs(model_outputs, ground_truth_detections): costs_pred = preds["costs"] assert len(costs_pred) == len(costs_gt) eq = [ - np.array_equal(np.argmax(v["m1"], axis=0), np.argmax(costs_gt[k]["m1"], axis=0)) - for k, v in costs_pred.items() + np.array_equal(np.argmax(v["m1"], axis=0), np.argmax(costs_gt[k]["m1"], axis=0)) for k, v in costs_pred.items() ] assert sum(eq) == 60 # 6 arrays are unequal as cost computation was corrected - assert all( - np.allclose(v["distance"], costs_gt[k]["distance"], atol=1.5) - for k, v in costs_pred.items() - ) + assert all(np.allclose(v["distance"], costs_gt[k]["distance"], atol=1.5) for k, v in costs_pred.items()) def test_compute_peaks_and_costs_no_graph(model_outputs): diff --git a/tests/test_predict_supermodel.py b/tests/test_predict_supermodel.py index 767e2739a8..1453984620 100644 --- a/tests/test_predict_supermodel.py +++ b/tests/test_predict_supermodel.py @@ -10,7 +10,8 @@ # import numpy as np import pytest -from deeplabcut.modelzoo.api import superanimal_inference + +from deeplabcut.pose_estimation_tensorflow.modelzoo.api import superanimal_inference def test_get_multi_scale_frames(): @@ -22,7 +23,7 @@ def test_get_multi_scale_frames(): heights, ) assert len(frames) == len(shapes) == len(heights) - assert all(shape[0] == h for shape, h in zip(shapes, heights)) + assert all(shape[0] == h for shape, h in zip(shapes, heights, strict=False)) assert all(round(shape[0] * ar) == shape[1] for shape in shapes) @@ -44,4 +45,4 @@ def test_project_pred_to_original_size(scale): ) coords_orig = preds_orig["coordinates"][0] assert len(coords_orig) == len(xs) - assert all([round(x * scale) == round(xy[0]) for xy, x in zip(coords_orig, xs)]) + assert all([round(x * scale) == round(xy[0]) for xy, x in zip(coords_orig, xs, strict=False)]) diff --git a/tests/test_refine_train_dataset/test_outlierframes.py b/tests/test_refine_train_dataset/test_outlierframes.py new file mode 100644 index 0000000000..a0d5f229ba --- /dev/null +++ b/tests/test_refine_train_dataset/test_outlierframes.py @@ -0,0 +1,255 @@ +from unittest.mock import MagicMock + +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.refine_training_dataset import outlier_frames + +# ---------------------------- +# Helpers / fixtures +# ---------------------------- + +STATS = [ + "distance", + "sig", + "meanx", + "meany", + "lowerCIx", + "higherCIx", + "lowerCIy", + "higherCIy", +] + + +@pytest.fixture +def patch_hdf_write(monkeypatch): + """ + Avoid filesystem / pytables dependency when storeoutput='full' is used. + Also lets us assert that the write path is still exercised. + """ + mock = MagicMock() + monkeypatch.setattr(pd.DataFrame, "to_hdf", mock) + return mock + + +@pytest.fixture +def patch_fit_sarimax(monkeypatch): + def fake_fit_sarimax_model(x, p, p_bound, alpha, ARdegree, MAdegree): + x = np.asarray(x, dtype=float) + mean = x.copy() + ci = np.c_[mean - 1.0, mean + 1.0] + return mean, ci + + mock = MagicMock(side_effect=fake_fit_sarimax_model) + monkeypatch.setattr(outlier_frames, "FitSARIMAXModel", mock) + return mock + + +@pytest.fixture +def sparse_multianimal_df(): + """ + maDLC-like sparse layout: + - 2 individuals with shared bodyparts + - unique bodyparts present only under a special 'single' bucket + This breaks if reconstructed with the full Cartesian product of the non-'coords' levels, + e.g. multi-animal projects with unique bodyparts were previously + producing many extra columns for the non-existent combinations of individual x unique bodypart. + """ + n_frames = 7 + scorer = "DLC_scorer" + individuals = ["ind1", "ind2"] + shared_bodyparts = [f"shared_{i}" for i in range(18)] + unique_bodyparts = [f"unique_{i}" for i in range(4)] + coords = ["x", "y", "likelihood"] + + tuples = [] + + # Shared bodyparts for each real individual + for ind in individuals: + for bp in shared_bodyparts: + for c in coords: + tuples.append((scorer, ind, bp, c)) + + # Unique bodyparts only under a special bucket + for bp in unique_bodyparts: + for c in coords: + tuples.append((scorer, "single", bp, c)) + + columns = pd.MultiIndex.from_tuples(tuples, names=["scorer", "individuals", "bodyparts", "coords"]) + + # 18 shared * 2 + 4 unique = 40 streams, each with x/y/likelihood + assert len(columns) == 40 * 3 + + rng = np.random.default_rng(42) + values = rng.normal(size=(n_frames, len(columns))) + + # Keep likelihood valid / boring + likelihood_mask = columns.get_level_values("coords") == "likelihood" + values[:, likelihood_mask] = 0.9 + + df = pd.DataFrame(values, columns=columns) + return df + + +@pytest.fixture +def dense_multianimal_df(): + """ + Dense/full-combination layout: + every individual x bodypart combination exists. + For this topology, the old from_product(...) logic and the new "preserve + actual tuples" logic should produce the same output columns (assuming the + dataframe is created in canonical product order, which we do here). + """ + n_frames = 5 + scorer = "DLC_scorer" + individuals = ["ind1", "ind2"] + bodyparts = ["nose", "tail", "paw"] + coords = ["x", "y", "likelihood"] + + tuples = [(scorer, ind, bp, c) for ind in individuals for bp in bodyparts for c in coords] + + columns = pd.MultiIndex.from_tuples(tuples, names=["scorer", "individuals", "bodyparts", "coords"]) + + rng = np.random.default_rng(42) + values = rng.normal(size=(n_frames, len(columns))) + likelihood_mask = columns.get_level_values("coords") == "likelihood" + values[:, likelihood_mask] = 0.95 + + df = pd.DataFrame(values, columns=columns) + return df + + +def _expected_output_columns_from_actual_streams(df): + """ + Expected output columns preserve actual non-'coords' tuples and append the 8 derived stats. + """ + base_cols = df.xs("x", axis=1, level="coords", drop_level=True).columns + return pd.MultiIndex.from_tuples( + [(tuple(col) if isinstance(col, tuple) else (col,)) + (stat,) for col in base_cols for stat in STATS], + names=df.columns.names, + ) + + +def _expected_output_columns_from_dense_product(df): + """ + Expected output columns for the previous implementation: + full Cartesian product of all non-'coords' levels, then the 8 derived stats. + This is only correct / behavior-preserving for dense layouts. + """ + columns = df.columns + prod = [] + for i in range(columns.nlevels - 1): + prod.append(columns.get_level_values(i).unique()) + prod.append(STATS) + return pd.MultiIndex.from_product(prod, names=columns.names) + + +# ---------------------------- +# Tests +# ---------------------------- + + +def test_compute_deviations_regression_sparse_unique_bodyparts( + sparse_multianimal_df, + patch_fit_sarimax, + patch_hdf_write, +): + """ + Regression test for the following maDLC unique-bodypart bug: + output columns must match the actual sparse stream layout rather than an + inflated Cartesian product of all non-'coords' level values. + """ + df = sparse_multianimal_df + n_frames = len(df) + + d, o, data = outlier_frames.compute_deviations( + df, + dataname="dummy.h5", + p_bound=0.01, + alpha=0.01, + ARdegree=3, + MAdegree=1, + storeoutput="full", + ) + + # There are 40 real streams in the sparse fixture + n_streams = 40 + + # Shape sanity checks + assert d.shape == (n_frames,) + assert o.shape == (n_frames,) + assert data.shape == (n_frames, n_streams * 8) + + # Column layout must preserve only the actual streams + expected_columns = _expected_output_columns_from_actual_streams(df) + assert data.columns.equals(expected_columns) + + # xs(...) on the last level should still work exactly as before + distance = data.xs("distance", axis=1, level=-1) + sig = data.xs("sig", axis=1, level=-1) + assert distance.shape == (n_frames, n_streams) + assert sig.shape == (n_frames, n_streams) + + # With the fake fitter, predictions equal observations => zero distances and sig + np.testing.assert_allclose(d, 0.0) + np.testing.assert_allclose(o, 0.0) + + # FitSARIMAXModel should be called twice per stream (x and y) + assert patch_fit_sarimax.call_count == 2 * n_streams + + # "full" path should still try to persist the result + patch_hdf_write.assert_called_once() + + +def test_compute_deviations_behavior_preserved_for_dense_layout( + dense_multianimal_df, + patch_fit_sarimax, + patch_hdf_write, +): + """ + Behavior-preserved check: + for a dense layout where every combination exists, the fixed implementation + should produce the same columns that the old from_product(...) logic would + have produced. + """ + df = dense_multianimal_df + n_frames = len(df) + n_streams = len(df.xs("x", axis=1, level="coords", drop_level=True).columns) + + d, o, data = outlier_frames.compute_deviations( + df, + dataname="dummy.h5", + p_bound=0.01, + alpha=0.01, + ARdegree=3, + MAdegree=1, + storeoutput="full", + ) + + # Basic shape / output checks + assert d.shape == (n_frames,) + assert o.shape == (n_frames,) + assert data.shape == (n_frames, n_streams * 8) + + # For dense data, new behavior should match old dense-product behavior exactly + expected_old_dense_columns = _expected_output_columns_from_dense_product(df) + expected_new_columns = _expected_output_columns_from_actual_streams(df) + + # Sanity check of the fixture assumption: + # in the dense case, these should indeed be identical. + assert expected_new_columns.equals(expected_old_dense_columns) + + # Actual output should match that shared expected index + assert data.columns.equals(expected_old_dense_columns) + + # Still selectable by derived-stat level + assert data.xs("distance", axis=1, level=-1).shape == (n_frames, n_streams) + assert data.xs("sig", axis=1, level=-1).shape == (n_frames, n_streams) + + # Deterministic fake fitter + np.testing.assert_allclose(d, 0.0) + np.testing.assert_allclose(o, 0.0) + + assert patch_fit_sarimax.call_count == 2 * n_streams + patch_hdf_write.assert_called_once() diff --git a/tests/test_stitcher.py b/tests/test_stitcher.py index 033552e54c..699ba96d7f 100644 --- a/tests/test_stitcher.py +++ b/tests/test_stitcher.py @@ -11,8 +11,8 @@ import numpy as np import pandas as pd import pytest -from deeplabcut.refine_training_dataset.stitch import Tracklet, TrackletStitcher +from deeplabcut.refine_training_dataset.stitch import Tracklet, TrackletStitcher TRACKLET_LEN = 1000 TRACKLET_START = 50 @@ -100,7 +100,7 @@ def test_tracklet_data_access(tracklet): @pytest.mark.parametrize( "tracklet, where, norm", - list(zip(make_fake_tracklets(), ("head", "tail"), (False, True))), + list(zip(make_fake_tracklets(), ("head", "tail"), (False, True), strict=False)), ) def test_tracklet_calc_velocity(tracklet, where, norm): _ = tracklet.calc_velocity(where, norm) diff --git a/tests/test_tf_install_smoke.py b/tests/test_tf_install_smoke.py new file mode 100644 index 0000000000..af19b8c8a3 --- /dev/null +++ b/tests/test_tf_install_smoke.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# + +"""Smoke test for TensorFlow when optional TF extras are installed.""" + +import pytest + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + + +def test_tensorflow_imports_and_has_matmul() -> None: + assert tf.__version__ + a = tf.constant([[1.0, 2.0]]) + b = tf.constant([[3.0], [4.0]]) + c = tf.matmul(a, b) + + if tf.executing_eagerly(): + result = c.numpy() + else: + with tf.compat.v1.Session() as sess: + result = sess.run(c) + + assert (result == [[11.0]]).all() + + +def test_tf_slim_imports_and_has_conv2d() -> None: + try: + import tf_slim as slim + except ImportError as e: + raise AssertionError("tf_slim is not installed or not importable") from e + + assert slim.conv2d(tf.constant([[[[1.0]]]]), 1, kernel_size=[1, 1], stride=1).shape == (1, 1, 1, 1) + + +def test_tf_keras_imports_and_has_regularizers() -> None: + try: + import tf_keras as keras + except ImportError as e: + raise AssertionError("tf_keras is not installed or not importable") from e + import numpy as np + + assert keras.regularizers.l2(0.01).l2 == np.array(0.01, dtype="float32") diff --git a/tests/test_trackingutils.py b/tests/test_trackingutils.py index 1795db03ee..b3dac6d263 100644 --- a/tests/test_trackingutils.py +++ b/tests/test_trackingutils.py @@ -10,7 +10,8 @@ # import numpy as np import pytest -from deeplabcut.pose_estimation_tensorflow.lib import trackingutils + +from deeplabcut.core import trackingutils @pytest.fixture() @@ -21,9 +22,7 @@ def ellipse(): def test_ellipse(ellipse): assert ellipse.aspect_ratio == 2 - np.testing.assert_equal( - ellipse.contains_points(np.asarray([[0, 0], [10, 10]])), [True, False] - ) + np.testing.assert_equal(ellipse.contains_points(np.asarray([[0, 0], [10, 10]])), [True, False]) def test_ellipse_similarity(ellipse): @@ -73,12 +72,8 @@ def test_tracking_ellipse(real_assemblies, real_tracklets): trackers = mot_tracker.track(animals[..., :2]) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] - assert all( - t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values() - ) + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] + assert all(t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values()) def test_box_tracker(): @@ -105,12 +100,8 @@ def test_tracking_box(real_assemblies, real_tracklets): trackers = mot_tracker.track(bboxes) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] - assert all( - t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values() - ) + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] + assert all(t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values()) def test_tracking_montblanc( @@ -127,9 +118,7 @@ def test_tracking_montblanc( trackers = mot_tracker.track(animals[..., :2]) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] for k, assemblies in tracklets.items(): ref = tracklets_ref[k] for ind, data in assemblies.items(): @@ -140,12 +129,8 @@ def test_tracking_montblanc( def test_calc_bboxes_from_keypoints(): # Test bounding box from a single keypoint xy = np.asarray([[[0, 0, 1]]]) - np.testing.assert_equal( - trackingutils.calc_bboxes_from_keypoints(xy, 10), [[-10, -10, 10, 10, 1]] - ) - np.testing.assert_equal( - trackingutils.calc_bboxes_from_keypoints(xy, 20, 10), [[-10, -20, 30, 20, 1]] - ) + np.testing.assert_equal(trackingutils.calc_bboxes_from_keypoints(xy, 10), [[-10, -10, 10, 10, 1]]) + np.testing.assert_equal(trackingutils.calc_bboxes_from_keypoints(xy, 20, 10), [[-10, -20, 30, 20, 1]]) width = 200 height = width * 2 @@ -160,9 +145,7 @@ def test_calc_bboxes_from_keypoints(): slack = 20 bboxes = trackingutils.calc_bboxes_from_keypoints(xyp, slack=slack) - np.testing.assert_equal( - bboxes, [[-slack, -slack, width + slack, height + slack, 0.5]] - ) + np.testing.assert_equal(bboxes, [[-slack, -slack, width + slack, height + slack, 0.5]]) offset = 50 bboxes = trackingutils.calc_bboxes_from_keypoints(xyp, offset=offset) diff --git a/tests/test_trainingsetmanipulation.py b/tests/test_trainingsetmanipulation.py index d49093dc9a..97749c52d1 100644 --- a/tests/test_trainingsetmanipulation.py +++ b/tests/test_trainingsetmanipulation.py @@ -8,22 +8,25 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os + +import numpy as np import pandas as pd +import pytest from conftest import TEST_DATA_DIR +from skimage import color, io + from deeplabcut.generate_training_dataset import ( - read_image_shape_fast, SplitTrials, - format_training_data, format_multianimal_training_data, - trainingsetmanipulation, + format_training_data, multiple_individuals_trainingsetmanipulation, + parse_video_filenames, + read_image_shape_fast, + trainingsetmanipulation, ) - from deeplabcut.utils.auxfun_videos import imread from deeplabcut.utils.conversioncode import guarantee_multiindex_rows -from skimage import color, io def test_read_image_shape_fast(tmp_path): @@ -58,23 +61,16 @@ def test_format_training_data(monkeypatch): "read_image_shape_fast", lambda _: fake_shape, ) - df = pd.read_hdf(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")).xs( - "mus1", level="individuals", axis=1 - ) + df = pd.read_hdf(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")).xs("mus1", level="individuals", axis=1) guarantee_multiindex_rows(df) train_inds = list(range(10)) _, data = format_training_data(df, train_inds, 12, "") assert len(data) == len(train_inds) # Check data comprise path, shape, and xy coordinates assert all(len(d) == 3 for d in data) - assert all( - (d[0].size == 3 and d[0].dtype.char == "U" and d[0][0, -1].endswith(".png")) - for d in data - ) + assert all((d[0].size == 3 and d[0].dtype.char == "U" and d[0][0, -1].endswith(".png")) for d in data) assert all(np.all(d[1] == np.array(fake_shape)[None]) for d in data) - assert all( - (d[2][0, 0].shape[1] == 3 and d[2][0, 0].dtype == np.int64) for d in data - ) + assert all((d[2][0, 0].shape[1] == 3 and d[2][0, 0].dtype == np.int64) for d in data) def test_format_multianimal_training_data(monkeypatch): @@ -93,8 +89,142 @@ def test_format_multianimal_training_data(monkeypatch): assert all(isinstance(d, dict) for d in data) assert all(len(d["image"]) == 3 for d in data) assert all(np.all(d["size"] == np.array(fake_shape)) for d in data) - assert all( - (xy.shape[1] == 3 and np.isfinite(xy).all()) - for d in data - for xy in d["joints"].values() + assert all((xy.shape[1] == 3 and np.isfinite(xy).all()) for d in data for xy in d["joints"].values()) + + +@pytest.mark.parametrize( + "videos, expected_filenames", + [ + ([], []), + (["/data/my-video.mov"], ["my-video"]), + (["/data/my-video.mp4", "/data2/my-video.mov"], ["my-video"]), + (["/data/my-video.mov", "/data/video2.mov"], ["my-video", "video2"]), + (["/a/v1.mov", "/a/v2.mp4", "/b/v1.mov"], ["v1", "v2"]), + (["v1.mov", "v2.mov", "v1.mov"], ["v1", "v2"]), + (["/a/v1.mp4", "/a/v2.mov", "/b/v2.mov"], ["v1", "v2"]), + (["/a/v1.mp4", "/a/v2.mov", "/b/v2.mov", "/b/v3.mp4"], ["v1", "v2", "v3"]), + ], +) +def test_parse_video_filenames(videos: list[str], expected_filenames: list[str]): + filenames = parse_video_filenames(videos) + assert filenames == expected_filenames + + +def test_format_training_data_ignores_likelihood_columns(monkeypatch): + fake_shape = 3, 480, 640 + monkeypatch.setattr( + trainingsetmanipulation, + "read_image_shape_fast", + lambda _: fake_shape, ) + + # Base single-animal dataframe (x/y only) + df = pd.read_hdf(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")).xs( + "mus1", + level="individuals", + axis=1, + ) + guarantee_multiindex_rows(df) + + # Add a likelihood column so the layout becomes: + # x, y, likelihood, x, y, likelihood, ... + new_cols = [] + new_arrays = [] + + coord_level = df.columns.names.index("coords") + + for col in df.columns: + new_cols.append(col) + new_arrays.append(df[col].to_numpy()) + + if col[coord_level] == "y": + lik_col = list(col) + lik_col[coord_level] = "likelihood" + new_cols.append(tuple(lik_col)) + new_arrays.append(np.ones(len(df), dtype=float)) + + df_with_likelihood = pd.DataFrame( + np.column_stack(new_arrays), + index=df.index, + columns=pd.MultiIndex.from_tuples(new_cols, names=df.columns.names), + ) + + train_inds = list(range(10)) + + baseline_train_data, baseline_matlab_data = format_training_data(df, train_inds, 12, "") + train_data, matlab_data = format_training_data(df_with_likelihood, train_inds, 12, "") + + # The presence of likelihood columns should not change the formatted result + assert len(train_data) == len(baseline_train_data) + assert len(matlab_data) == len(baseline_matlab_data) + + for got, expected in zip(train_data, baseline_train_data, strict=False): + assert got["image"] == expected["image"] + assert got["size"] == expected["size"] + assert np.array_equal(got["joints"], expected["joints"]) + + for got, expected in zip(matlab_data, baseline_matlab_data, strict=False): + assert np.array_equal(got["image"], expected["image"]) + assert np.array_equal(got["size"], expected["size"]) + assert np.array_equal(got["joints"][0, 0], expected["joints"][0, 0]) + + +def test_merge_annotateddatasets_drops_likelihood_columns(tmp_path): + scorer = "testscorer" + video_name = "video1" + bodyparts = ["nose", "tail"] + + project_path = tmp_path + labeled_data_dir = project_path / "labeled-data" / video_name + labeled_data_dir.mkdir(parents=True) + + trainingsetfolder_full = project_path / "training-datasets" / "iteration-0" + trainingsetfolder_full.mkdir(parents=True) + + # Build a single-animal annotation dataframe with x/y/likelihood columns + columns = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "bodyparts", "coords"], + ) + + index = pd.MultiIndex.from_tuples( + [("labeled-data", video_name, "img0001.png")], + ) + + data = np.array([[10.0, 20.0, 0.9, 30.0, 40.0, 0.8]]) + df = pd.DataFrame(data, index=index, columns=columns) + + input_h5 = labeled_data_dir / f"CollectedData_{scorer}.h5" + df.to_hdf(input_h5, key="df_with_missing", mode="w") + + cfg = { + "project_path": str(project_path), + "video_sets": {str(project_path / "videos" / f"{video_name}.mp4"): {}}, + "scorer": scorer, + "bodyparts": bodyparts, + "multianimalproject": False, + } + + merged = trainingsetmanipulation.merge_annotateddatasets( + cfg, + trainingsetfolder_full, + ) + + # Returned dataframe should not contain likelihood anymore + coord_level = "coords" if "coords" in merged.columns.names else merged.columns.names[-1] + assert "likelihood" not in merged.columns.get_level_values(coord_level) + + # Saved merged h5 should also not contain likelihood + output_h5 = trainingsetfolder_full / f"CollectedData_{scorer}.h5" + saved = pd.read_hdf(output_h5) + + coord_level = "coords" if "coords" in saved.columns.names else saved.columns.names[-1] + assert "likelihood" not in saved.columns.get_level_values(coord_level) + + # Sanity check: x/y are preserved + assert set(saved.columns.get_level_values(coord_level)) == {"x", "y"} + output_csv = trainingsetfolder_full / f"CollectedData_{scorer}.csv" + saved_csv = pd.read_csv(output_csv, header=[0, 1, 2], index_col=[0, 1, 2]) + + coord_level = "coords" if "coords" in saved_csv.columns.names else saved_csv.columns.names[-1] + assert "likelihood" not in saved_csv.columns.get_level_values(coord_level) diff --git a/tests/test_triangulation.py b/tests/test_triangulation.py index 0fcf2b058c..a1b2fe382c 100644 --- a/tests/test_triangulation.py +++ b/tests/test_triangulation.py @@ -11,6 +11,7 @@ import numpy as np import pandas as pd import pytest + from deeplabcut.pose_estimation_3d import triangulation @@ -48,9 +49,7 @@ def test_undistort_views(n_view_pairs, is_multi, stereo_params): df = df.xs("bird1", level="individuals", axis=1) view_pairs = [(df, df) for _ in range(n_view_pairs)] - cam_params = { - f"camera-1-camera-{i}": stereo_params for i in range(2, n_view_pairs + 2) - } + cam_params = {f"camera-1-camera-{i}": stereo_params for i in range(2, n_view_pairs + 2)} dfs = triangulation._undistort_views(view_pairs, cam_params) assert len(dfs) == n_view_pairs assert all(len(pair) == 2 for pair in dfs) diff --git a/tests/test_video.py b/tests/test_video.py index 02b70e828f..915933cf6c 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -9,10 +9,11 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + import pytest from conftest import TEST_DATA_DIR -from deeplabcut.utils.auxfun_videos import VideoWriter +from deeplabcut.utils.auxfun_videos import VideoWriter POS_FRAMES = 1 # Equivalent to cv2.CAP_PROP_POS_FRAMES @@ -57,9 +58,7 @@ def test_reader_wrong_fps(video_clip): def test_reader_duration(video_clip): - assert video_clip.calc_duration() == pytest.approx( - video_clip.calc_duration(robust=False), abs=0.01 - ) + assert video_clip.calc_duration() == pytest.approx(video_clip.calc_duration(robust=False), abs=0.01) def test_reader_set_frame(video_clip): @@ -93,9 +92,7 @@ def test_writer_bbox(video_clip): assert video_clip.get_bbox(relative=True) == (0, 1, 0, 1) -@pytest.mark.parametrize( - "start, end", [(0, 10), ("0:0", "0:10"), ("00:00:00", "00:00:10")] -) +@pytest.mark.parametrize("start, end", [(0, 10), ("0:0", "0:10"), ("00:00:00", "00:00:10")]) def test_writer_shorten_invalid_timestamps(video_clip, start, end): with pytest.raises(ValueError): video_clip.shorten(start, end) diff --git a/tests/tools/conftest.py b/tests/tools/conftest.py new file mode 100644 index 0000000000..ef9b94e067 --- /dev/null +++ b/tests/tools/conftest.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import importlib +import sys +from pathlib import Path +from types import ModuleType + +import pytest + + +def _repo_root() -> Path: + # tests/tools/conftest.py -> repo root is 2 levels up + return Path(__file__).resolve().parents[2] + + +def load_selector_module() -> ModuleType: + root = _repo_root() + tools_dir = root / "tools" + init_file = tools_dir / "__init__.py" + selector_path = tools_dir / "test_selector.py" + + if not selector_path.exists(): + raise FileNotFoundError(f"Selector script not found: {selector_path}") + + if not init_file.exists(): + raise FileNotFoundError(f"tools package marker not found: {init_file}") + + # Ensure repo root is importable so `tools.test_selector` resolves as a package import. + root_str = str(root) + if root_str not in sys.path: + sys.path.insert(0, root_str) + + return importlib.import_module("tools.test_selector") + + +@pytest.fixture(scope="session") +def selector(): + """Imported selector module (tools.test_selector).""" + return load_selector_module() diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py new file mode 100644 index 0000000000..1abe6c47cb --- /dev/null +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -0,0 +1,559 @@ +from __future__ import annotations + +import importlib.util +import json +import os +import subprocess +from collections.abc import Callable +from datetime import date, datetime, timezone +from pathlib import Path +from types import ModuleType + +import pytest + + +@pytest.fixture(autouse=True) +def no_github_step_summary(monkeypatch): + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + + +# ----------------------------- +# Module loader (tools/ is not necessarily a package) +# ----------------------------- +def load_tool_module() -> ModuleType: + repo_root = Path(__file__).resolve().parents[3] + tool_path = repo_root / "tools" / "docs_and_notebooks_check.py" + assert tool_path.exists(), f"Missing tool: {tool_path}" + + spec = importlib.util.spec_from_file_location("docs_and_notebooks_check", tool_path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[attr-defined] + return mod + + +@pytest.fixture(scope="session") +def tool() -> ModuleType: + return load_tool_module() + + +def _write_default_cfg(repo: Path, include: list[str]) -> Path: + cfg_path = repo / "tools" / "docs_and_notebooks_report_config.yml" + cfg_path.parent.mkdir(parents=True, exist_ok=True) + cfg_path.write_text( + "version: 1\n" + "scan:\n" + " include:\n" + "".join(f" - {pat}\n" for pat in include) + " exclude: []\n" + "policy:\n" + " warn_if_content_older_than_days: 365\n" + " warn_if_verified_older_than_days: 365\n" + " missing_last_verified_is_warning: true\n" + " fail_on_scan_errors: false\n" + " require_metadata: []\n" + " require_recent_verification: []\n" + " require_notebook_normalized: []\n", + encoding="utf-8", + ) + return cfg_path + + +# ----------------------------- +# Git helpers for a temp repo +# ----------------------------- +def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: + return subprocess.run(cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True) + + +def _git_init(repo: Path) -> None: + _run(["git", "init"], repo) + _run(["git", "config", "user.email", "ci@example.com"], repo) + _run(["git", "config", "user.name", "CI"], repo) + + +def _git_commit(repo: Path, message: str, when_iso: str) -> None: + env = os.environ.copy() + env["GIT_AUTHOR_DATE"] = when_iso + env["GIT_COMMITTER_DATE"] = when_iso + _run(["git", "add", "-A"], repo, env=env) + _run(["git", "commit", "-m", message], repo, env=env) + + +def _write(repo: Path, rel: str, content: str) -> None: + p = repo / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content, encoding="utf-8") + + +# ----------------------------- +# Shared fixtures +# ----------------------------- +@pytest.fixture +def repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + return repo + + +@pytest.fixture +def cfg(tool) -> Callable[..., object]: + def _make_cfg( + include: list[str], + exclude: list[str] | None = None, + **policy_overrides, + ): + policy = tool.PolicyConfig(**policy_overrides) + return tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=include, exclude=exclude or []), + policy=policy, + ) + + return _make_cfg + + +# ----------------------------- +# Contract tests +# ----------------------------- +def test_marker_constants_exist(tool): + assert hasattr(tool, "META_COMMIT_MARKER") + assert hasattr(tool, "SUGGESTED_TAGGED_COMMIT") + assert tool.META_COMMIT_MARKER in tool.SUGGESTED_TAGGED_COMMIT + + +def test_schema_contract_fields(tool): + # DLCMeta must have new fields and must NOT have old last_git_updated + meta = tool.DLCMeta() + assert hasattr(meta, "last_content_updated") + assert hasattr(meta, "last_metadata_updated") + assert hasattr(meta, "last_verified") + assert hasattr(meta, "verified_for") + assert not hasattr(meta, "last_git_updated") + + +def test_git_content_date_skips_meta_commits(tool, repo: Path): + """ + Contract: last_content_updated is computed from git history excluding metadata commits. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + # meta-only rewrite (simulated) committed with marker + _write( + repo, + rel, + "---\ndeeplabcut:\n last_metadata_updated: 2026-03-01\n---\n# hello\n", + ) + _git_commit( + repo, + f"chore(meta): update {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) + + # raw touched date = 2026-03-01 + touched = tool.git_last_touched(repo, rel) + assert touched == date(2026, 3, 1) + + # content updated date should skip marker commit => 2020-01-01 + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2020, 1, 1) + assert used_fallback is False + + +def test_git_content_date_fallback_when_only_meta_commits(tool, repo: Path): + """ + If all commits touching the file are meta-marker commits, we fall back to git_last_touched + and flag used_fallback=True. + """ + rel = "docs/page.md" + _write(repo, rel, "---\ndeeplabcut:\n notes: hi\n---\n") + _git_commit( + repo, + f"chore(meta): init {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) + + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2026, 3, 1) + assert used_fallback is True + + +def test_scan_is_read_only(tool, repo: Path, cfg): + """ + Contract: report/check (scan_files) must be read-only. + We validate by asserting file content does not change. + """ + rel = "docs/page.md" + orig = "---\ndeeplabcut:\n last_verified: 2020-01-01\n---\n# hello\n" + _write(repo, rel, orig) + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + before = (repo / rel).read_text(encoding="utf-8") + records = tool.scan_files(repo, tool_cfg, targets=[r".\docs\page.md"]) + after = (repo / rel).read_text(encoding="utf-8") + + assert before == after + assert len(records) == 1 + assert records[0].path == rel + assert records[0].kind == "md" + + +def test_scan_targets_support_directory_and_glob(tool, repo: Path, cfg): + rel_a = "docs/gui/napari/basic_usage.md" + rel_b = "docs/gui/napari/advanced_usage.md" + rel_c = "docs/other/overview.md" + + _write(repo, rel_a, "# a\n") + _write(repo, rel_b, "# b\n") + _write(repo, rel_c, "# c\n") + _git_commit(repo, "docs: add pages", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=["docs/**/*.md"]) + + # Directory selector + recs_dir = tool.scan_files(repo, tool_cfg, targets=["docs/gui/napari/"]) + paths_dir = sorted(r.path for r in recs_dir) + assert set(paths_dir) == {rel_a, rel_b} + + # Glob selector + recs_glob = tool.scan_files(repo, tool_cfg, targets=["docs/gui/napari/*.md"]) + paths_glob = sorted(r.path for r in recs_glob) + assert set(paths_glob) == {rel_a, rel_b} + + # Recursive glob selector + recs_recursive = tool.scan_files(repo, tool_cfg, targets=["docs/**/*.md"]) + paths_recursive = sorted(r.path for r in recs_recursive) + assert set(paths_recursive) == {rel_a, rel_b, rel_c} + + +def test_validate_requested_targets_treats_dot_slash_as_unmatched(tool, repo: Path, cfg): + _write(repo, "docs/page.md", "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, ["./"]) + assert matched == [] + assert unmatched == ["./"] + + +def test_validate_requested_targets_treats_empty_like_unmatched(tool, repo: Path, cfg): + _write(repo, "docs/page.md", "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, ["", " "]) + assert matched == [] + assert unmatched == ["", " "] + + +def test_validate_requested_targets_reports_mixed_valid_and_invalid_targets(tool, repo: Path, cfg): + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, [rel, "./"]) + assert rel in matched + assert unmatched == ["./"] + + +def test_scan_files_with_invalid_only_targets_matches_nothing(tool, repo: Path, cfg): + tool_cfg = cfg(include=["docs/**/*.md"]) + records = tool.scan_files(repo, tool_cfg, targets=["./"]) + assert records == [] + + +def test_validate_requested_targets_reports_unmatched(tool, repo: Path, cfg): + rel_a = "docs/gui/napari/basic_usage.md" + rel_b = "docs/gui/napari/advanced_usage.md" + + _write(repo, rel_a, "# a\n") + _write(repo, rel_b, "# b\n") + _git_commit(repo, "docs: add pages", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets( + repo, + tool_cfg, + targets=[ + r".\docs\gui\napari\basic_usage.md", + "docs/gui/napari/", + "docs/**/*.md", + "docs/missing/", + "examples/**/*.ipynb", + ], + ) + + assert matched == sorted([rel_a, rel_b]) + assert unmatched == ["docs/missing/", "examples/**/*.ipynb"] + + +def test_update_requires_ack_when_write(tool, repo: Path, cfg): + """ + Contract: write mode should refuse unless --ack-meta-commit-marker is provided. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + # should refuse to write without ack + with pytest.raises(SystemExit): + tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=False, + ) + + +def test_update_set_content_date_from_git_only_changes_that_field(tool, repo: Path, cfg): + """ + Contract: update --set-content-date-from-git only sets last_content_updated + (plus last_metadata_updated when writing), + does NOT override last_verified/verified_for unless explicitly provided. + """ + rel = "docs/page.md" + initial = "---\ndeeplabcut:\n last_verified: 2020-02-02\n verified_for: 3.0.0rc1\n---\n# hello\n" + _write(repo, rel, initial) + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + # Read back and confirm verified fields unchanged + text = (repo / rel).read_text(encoding="utf-8") + fm, _body, _ = tool.read_md_frontmatter(text) + assert isinstance(fm, dict) and tool.DLC_NAMESPACE in fm + meta = fm[tool.DLC_NAMESPACE] + + assert meta["last_verified"] == "2020-02-02" + assert meta["verified_for"] == "3.0.0rc1" + + # last_content_updated should reflect git content date (2020-01-01) + assert meta["last_content_updated"] == "2020-01-01" + + # last_metadata_updated should exist because we wrote + assert "last_metadata_updated" in meta + + +def test_update_set_verified_fields_only_changes_verified(tool, repo: Path, cfg): + """ + Contract: update with --set-last-verified / --set-verified-for changes only those fields + (plus last_metadata_updated if writing), and does not set last_content_updated unless requested. + """ + rel = "docs/page.md" + initial = "---\ndeeplabcut:\n last_content_updated: 2000-01-01\n---\n# hello\n" + _write(repo, rel, initial) + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=False, + set_last_verified=date(2026, 3, 5), + set_verified_for="3.0.0rc13", + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + text = (repo / rel).read_text(encoding="utf-8") + fm, _body, _ = tool.read_md_frontmatter(text) + meta = fm[tool.DLC_NAMESPACE] + + # Verified fields updated + assert meta["last_verified"] == "2026-03-05" + assert meta["verified_for"] == "3.0.0rc13" + + # last_content_updated remains whatever it was (not overwritten) + assert meta["last_content_updated"] == "2000-01-01" + + +def test_normalize_is_explicit_and_marks_would_change(tool, repo: Path, cfg): + """ + Contract: normalize is separate and explicit; in dry-run it should mark would_change + if notebook is not already in canonical nbformat output. + """ + rel = "docs/nbs/nb.ipynb" + # Minimal notebook JSON but not in nbformat canonical formatting (indent/newline differences) + raw = '{\n "cells": [],\n "metadata": {},\n "nbformat": 4,\n "nbformat_minor": 5\n}\n' + _write(repo, rel, raw) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + # Dry-run normalize: should set would_change True if not normalized + records = tool.normalize_notebooks( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=False, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + assert records[0].kind == "ipynb" + # may be True depending on canonical formatting differences + assert records[0].would_change + + +def test_write_outputs_contract(tool, repo: Path, cfg, tmp_path: Path): + """ + Contract: write_outputs creates both JSON and Markdown files and JSON is schema-valid. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + + report = tool.Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo), + config_path="in-memory", + totals=tool.summarize(records), + records=records, + ) + + out_dir = tmp_path / "out" + json_path, md_path = tool.write_outputs(report, tool_cfg, out_dir) + + assert json_path.exists() + assert md_path.exists() + + payload = json.loads(json_path.read_text(encoding="utf-8")) + assert payload["schema_version"] == tool.SCHEMA_VERSION + assert "records" in payload and isinstance(payload["records"], list) + assert md_path.read_text(encoding="utf-8").startswith("#") + + +def test_notebook_missing_dlc_namespace_warns_missing_metadata(tool, repo: Path, cfg): + rel = "docs/nbs/nb.ipynb" + # Valid minimal notebook, but no "deeplabcut" namespace under metadata + nb = '{\n "cells": [],\n "metadata": {},\n "nbformat": 4,\n "nbformat_minor": 5\n}\n' + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "missing_metadata" in r.warnings + assert r.meta is None + + +def test_notebook_invalid_dlc_namespace_warns_invalid_metadata(tool, repo: Path, cfg): + rel = "docs/nbs/nb.ipynb" + # deeplabcut namespace exists but is invalid: last_verified must be a date + nb = ( + "{\n" + ' "cells": [],\n' + ' "metadata": {\n' + ' "deeplabcut": {\n' + ' "last_verified": "not-a-date"\n' + " }\n" + " },\n" + ' "nbformat": 4,\n' + ' "nbformat_minor": 5\n' + "}\n" + ) + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook with bad meta", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "invalid_metadata" in r.warnings + assert r.meta is None + + +def test_main_prints_matched_files_and_fails_on_unmatched_targets(tool, repo: Path, monkeypatch, capsys): + rel = "docs/gui/napari/basic_usage.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main( + [ + "--config", + str(cfg_path), + "--no-step-summary", + "report", + "--targets", + r".\docs\gui\napari\basic_usage.md", + "docs/missing/", + ] + ) + + out = capsys.readouterr().out + assert rc == 2 + assert "Matched 1 file(s) from --targets:" in out + assert f"- {rel}" in out + assert "Unmatched --targets:" in out + assert "- docs/missing/" in out + + +def test_main_prints_matched_files_for_valid_targets(tool, repo: Path, monkeypatch, capsys): + rel = "docs/gui/napari/basic_usage.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main( + [ + "--config", + str(cfg_path), + "--no-step-summary", + "report", + "--targets", + "docs/gui/napari/", + ] + ) + + out = capsys.readouterr().out + assert rc == 0 + assert "Matched 1 file(s) from --targets:" in out + assert f"- {rel}" in out + assert "Report generated:" in out + + +def test_main_returns_2_for_invalid_target_selector(tool, repo: Path, monkeypatch): + _write(repo, "docs/page.md", "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main(["--config", str(cfg_path), "--no-step-summary", "report", "--targets", "./"]) + assert rc == 2 diff --git a/tests/tools/test_selector/test_selector_decision.py b/tests/tools/test_selector/test_selector_decision.py new file mode 100644 index 0000000000..c45ce63d7b --- /dev/null +++ b/tests/tools/test_selector/test_selector_decision.py @@ -0,0 +1,341 @@ +# tests/tools/test_selector/test_selector_decision.py +from __future__ import annotations + +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from tools.test_selector_config import ( + CATEGORY_RULES, + CategoryRule, + prefix, + validate_category_rules, +) + + +def assert_lanes(res, *, skip=False, docs=False, fast=False, full=False): + assert res.lanes.skip is skip + assert res.lanes.docs is docs + assert res.lanes.fast is fast + assert res.lanes.full is full + + +def test_fail_safe_on_empty_changes(selector): + res = selector.decide([]) + + assert_lanes(res, full=True) + assert "no_changed_files_or_diff_unavailable" in res.reasons + assert res.lane_reasons["full"] == ["no_changed_files_or_diff_unavailable"] + + +def test_docs_only(selector): + files = ["docs/index.md", "docs/guide/intro.md", "_config.yml"] + res = selector.decide(files) + + assert_lanes(res, docs=True) + assert res.pytest_paths == [] + assert res.functional_scripts == [] + assert "category:docs" in res.reasons + assert res.lane_reasons["docs"] == ["category:docs"] + + +def test_full_suite_trigger_pyproject_preserves_docs_lane(selector): + files = ["pyproject.toml", "docs/index.md"] + res = selector.decide(files) + + assert_lanes(res, full=True, docs=True) + assert "full_suite_trigger" in res.reasons + assert "category:docs" in res.reasons + assert res.lane_reasons["full"] == [ + "full_suite_trigger", + "full_suite_trigger_count:1", + ] + + +def test_full_suite_trigger_tests_folder(selector): + files = ["tests/test_something.py"] + res = selector.decide(files) + + assert_lanes(res, full=True) + assert "full_suite_trigger" in res.reasons + assert res.lane_reasons["full"] == [ + "full_suite_trigger", + "full_suite_trigger_count:1", + ] + + +def test_fast_core(selector): + files = ["deeplabcut/core/some_module.py"] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + # core rule should include these paths (subset check) + assert "tests/core/" in res.pytest_paths + assert "tests/utils/" in res.pytest_paths + # assert res.functional_scripts == [] # not empty, but we don't need to specify exact scripts here + + assert "category:core" in res.reasons + assert res.lane_reasons["fast"] == ["category:core"] + + # Provenance should attribute selected pytest roots to the core category. + assert "tests/core/" in res.provenance.pytest + assert res.provenance.pytest["tests/core/"] == ["core"] + + +def test_fast_multianimal_includes_functional(selector): + files = ["deeplabcut/pose_estimation_pytorch/multianimal/foo.py"] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + assert "tests/test_predict_multianimal.py" in res.pytest_paths + assert "examples/testscript_tensorflow_multi_animal.py" in res.functional_scripts + + assert "multianimal" in res.provenance.pytest["tests/test_predict_multianimal.py"] + assert "multianimal" in res.provenance.scripts["examples/testscript_tensorflow_multi_animal.py"] + + +def test_fast_ci_workflows_uses_full_suite(selector): + files = [".github/workflows/ci.yml"] + res = selector.decide(files) + + assert_lanes(res, full=True) + + +def test_no_category_matched_is_full(selector): + files = ["some/unknown/place/file.xyz"] + res = selector.decide(files) + + assert_lanes(res, full=True) + assert "no_category_matched" in res.reasons + assert res.lane_reasons["full"] == ["no_category_matched"] + + +def test_docs_and_core_run_both_lanes(selector): + files = ["docs/index.md", "deeplabcut/core/a.py"] + res = selector.decide(files) + + assert_lanes(res, docs=True, fast=True) + assert "category:docs" in res.reasons + assert "category:core" in res.reasons + + assert res.lane_reasons["docs"] == ["category:docs"] + assert res.lane_reasons["fast"] == ["category:core"] + + assert "tests/core/" in res.pytest_paths + assert "tests/utils/" in res.pytest_paths + + +def test_dedup_and_sorted_outputs(selector): + # Force overlap: core includes tests/test_auxiliaryfunctions.py and + # ci_tools contributes tests/tools/. Outputs should stay deduped and sorted. + files = [ + "deeplabcut/core/a.py", + "tools/whatever.py", + ] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + # No duplicates + assert len(res.pytest_paths) == len(set(res.pytest_paths)) + assert len(res.functional_scripts) == len(set(res.functional_scripts)) + + # Sorted + assert res.pytest_paths == sorted(res.pytest_paths) + assert res.functional_scripts == sorted(res.functional_scripts) + + +# ---------------------------- +# Validation of category rules +# ---------------------------- +def test_category_rule_rejects_empty_name(): + with pytest.raises(ValidationError, match="Rule name must not be empty"): + CategoryRule( + name="", + match_any=[prefix("docs/")], + ) + + +def test_category_rule_rejects_invalid_name(): + with pytest.raises(ValidationError, match=r"Rule name must match"): + CategoryRule( + name="docs-rule", + match_any=[prefix("docs/")], + ) + + +def test_category_rule_requires_non_empty_match_any(): + with pytest.raises(ValidationError, match="at least 1 item|at least one predicate"): + CategoryRule( + name="docs", + match_any=[], + ) + + +def test_category_rule_rejects_non_callable_match_any(): + with pytest.raises(ValidationError, match="callable"): + CategoryRule( + name="docs", + match_any=[123], # type: ignore[list-item] + ) + + +@pytest.mark.parametrize( + "field_name,bad_value", + [ + ("pytest_paths", "/absolute/path.py"), + ("pytest_paths", "../escape.py"), + ("functional_scripts", "/absolute/script.py"), + ("functional_scripts", "../escape_script.py"), + ], +) +def test_category_rule_rejects_invalid_repo_relative_paths(field_name, bad_value): + kwargs = { + "name": "docs", + "match_any": [prefix("docs/")], + "pytest_paths": [], + "functional_scripts": [], + } + kwargs[field_name] = [bad_value] + + with pytest.raises(ValidationError, match="repo-relative|path traversal|absolute path"): + CategoryRule(**kwargs) + + +def test_validate_category_rules_rejects_duplicate_names(): + rules = [ + CategoryRule(name="docs", match_any=[prefix("docs/")]), + CategoryRule(name="docs", match_any=[prefix("more-docs/")]), + ] + + with pytest.raises(ValueError, match="Duplicate CategoryRule name"): + validate_category_rules(rules) + + +def test_lint_only_changes_select_skip_lane(selector): + files = [".pre-commit-config.yaml"] + res = selector.decide(files) + + assert_lanes(res, skip=True) + assert res.pytest_paths == [] + assert res.functional_scripts == [] + + assert "lint_only" in res.reasons + assert "skip" in res.lane_reasons + assert "lint_only" in res.lane_reasons["skip"] + + +def test_validate_selected_paths_escalates_to_full_on_missing(selector, tmp_path: Path): + # Build a minimal repo dir with none of the selected paths present. + repo = tmp_path / "repo" + repo.mkdir() + + res = selector.SelectorResult( + lanes=selector.LaneSelection(fast=True), + pytest_paths=["tests/does_not_exist.py"], + functional_scripts=["examples/missing_script.py"], + provenance=selector.SelectionProvenance( + pytest={"tests/does_not_exist.py": ["core"]}, + scripts={"examples/missing_script.py": ["core"]}, + ), + reasons=["category:core"], + changed_files=["deeplabcut/core/foo.py"], + lane_reasons={"fast": ["category:core"]}, + ) + + out = selector.validate_selected_paths(res, repo) + + assert out.lanes.fast is False + assert out.lanes.full is True + + assert out.pytest_paths == [] + assert out.functional_scripts == [] + assert out.provenance.pytest == {} + assert out.provenance.scripts == {} + + assert "missing_selected_paths" in out.reasons + assert any(r.startswith("pytest:tests/does_not_exist.py") for r in out.reasons) + assert any(r.startswith("script:examples/missing_script.py") for r in out.reasons) + + assert "full" in out.lane_reasons + + +def test_validate_selected_paths_keeps_fast_when_paths_exist(selector, tmp_path: Path): + repo = tmp_path / "repo" + (repo / "tests").mkdir(parents=True) + (repo / "examples").mkdir(parents=True) + + (repo / "tests" / "test_ok.py").write_text("def test_ok(): pass\n") + (repo / "examples" / "script_ok.py").write_text("print('ok')\n") + + res = selector.SelectorResult( + lanes=selector.LaneSelection(fast=True), + pytest_paths=["tests/test_ok.py"], + functional_scripts=["examples/script_ok.py"], + provenance=selector.SelectionProvenance( + pytest={"tests/test_ok.py": ["core"]}, + scripts={"examples/script_ok.py": ["core"]}, + ), + reasons=["category:core"], + changed_files=["deeplabcut/core/foo.py"], + lane_reasons={"fast": ["category:core"]}, + ) + + out = selector.validate_selected_paths(res, repo) + + assert out.lanes.fast is True + assert out.lanes.full is False + assert out.pytest_paths == ["tests/test_ok.py"] + assert out.functional_scripts == ["examples/script_ok.py"] + + +# -------------------------------------- +# Current config validity & sanity checks +# -------------------------------------- + + +def test_current_category_rules_are_typed_models(): + assert CATEGORY_RULES + assert all(isinstance(rule, CategoryRule) for rule in CATEGORY_RULES) + + +def test_current_category_rules_pass_cross_rule_validation(): + validate_category_rules(CATEGORY_RULES) + + +def test_current_category_rule_names_are_unique(): + names = [rule.name for rule in CATEGORY_RULES] + assert len(names) == len(set(names)) + + +def test_current_category_rules_have_matchers(): + assert all(rule.match_any for rule in CATEGORY_RULES) + + +def test_required_category_rules_exist(): + names = {rule.name for rule in CATEGORY_RULES} + assert "docs" in names + assert "core" in names + + +def test_docs_rule_exists_once(): + docs_rules = [rule for rule in CATEGORY_RULES if rule.name == "docs"] + assert len(docs_rules) == 1 + + +def test_current_selected_paths_exist(): + repo_root = Path(__file__).resolve().parents[3] + missing = [] + + for rule in CATEGORY_RULES: + for path in rule.pytest_paths: + if not (repo_root / path).exists(): + missing.append((rule.name, "pytest", path)) + for path in rule.functional_scripts: + if not (repo_root / path).exists(): + missing.append((rule.name, "script", path)) + + assert missing == [] diff --git a/tests/tools/test_selector/test_selector_validation.py b/tests/tools/test_selector/test_selector_validation.py new file mode 100644 index 0000000000..41d2bfe912 --- /dev/null +++ b/tests/tools/test_selector/test_selector_validation.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + + +# ----------------- +# Git helpers +# ----------------- +def _git(repo: Path, *args: str) -> str: + proc = subprocess.run( + ["git", *args], + cwd=repo, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(f"git {' '.join(args)} failed: {proc.stderr.strip()}") + return proc.stdout.strip() + + +def _init_repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Test User") + _git(repo, "config", "user.email", "test@example.com") + return repo + + +def _commit_file(repo: Path, relpath: str, content: str, message: str) -> str: + path = repo / relpath + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + _git(repo, "add", relpath) + _git(repo, "commit", "-m", message) + return _git(repo, "rev-parse", "HEAD") + + +def _write_event(tmp_path: Path, payload: dict) -> Path: + event_path = tmp_path / "event.json" + event_path.write_text(json.dumps(payload), encoding="utf-8") + return event_path + + +# -------------- +# SHA validation & diff range parsing +# -------------- +def test_validate_sha_accepts(selector): + assert selector._validate_sha("x", "abc1234") == "abc1234" + assert selector._validate_sha("x", "a" * 40) == "a" * 40 + + +@pytest.mark.parametrize( + "bad", + [ + "", # empty + "notasha", # non-hex + "123", # too short + "g" * 40, # non-hex + " " * 8, # whitespace + ], +) +def test_validate_sha_rejects(selector, bad): + with pytest.raises(ValueError): + selector._validate_sha("x", bad) + + +def test_determine_diff_range_pr_uses_merge_base(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + merge_base = _commit_file(repo, "shared.txt", "base", "base commit") + base_sha = _commit_file(repo, "main.txt", "main", "main branch commit") + + _git(repo, "checkout", "-b", "feature", merge_base) + head_sha = _commit_file(repo, "feature.txt", "feature", "feature branch commit") + + event_path = _write_event( + tmp_path, + { + "pull_request": { + "base": {"sha": base_sha}, + "head": {"sha": head_sha}, + } + }, + ) + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == merge_base + assert head == head_sha + assert mode == selector.DiffMode.PR + + +def test_determine_diff_range_push_uses_before_after(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + before = _commit_file(repo, "a.txt", "one", "first commit") + after = _commit_file(repo, "a.txt", "two", "second commit") + + event_path = _write_event(tmp_path, {"before": before, "after": after}) + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == before + assert head == after + assert mode == selector.DiffMode.PUSH + + +def test_determine_diff_range_push_zero_sha_uses_empty_tree(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + after = _commit_file(repo, "initial.txt", "hello", "initial commit") + zero_sha = "0" * 40 + event_path = _write_event(tmp_path, {"before": zero_sha, "after": after}) + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == selector._empty_tree(repo) + assert head == after + assert mode == selector.DiffMode.INITIAL + + +def test_determine_diff_range_fallback_uses_head_parent(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + prev = _commit_file(repo, "a.txt", "one", "first commit") + head_sha = _commit_file(repo, "a.txt", "two", "second commit") + + monkeypatch.delenv("GITHUB_EVENT_NAME", raising=False) + monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == prev + assert head == head_sha + assert mode == selector.DiffMode.FALLBACK + + +# ----------------- +# Paths +# ----------------- +def test_normalize_relpath_basic(selector): + assert selector._normalize_relpath("docs/index.md") == "docs/index.md" + assert selector._normalize_relpath("docs\\index.md") == "docs/index.md" + + +@pytest.mark.parametrize( + "bad", + [ + "", # empty + " ", # whitespace + "/etc/passwd", # absolute unix + "C:/Windows/x", # absolute windows + "../secret.txt", # traversal + "docs/../../x", # traversal inside + "a\x00b", # NUL + ], +) +def test_normalize_relpath_rejects_bad(selector, bad): + with pytest.raises(ValueError): + selector._normalize_relpath(bad) diff --git a/tests/utils/test_collect_video_paths.py b/tests/utils/test_collect_video_paths.py new file mode 100644 index 0000000000..251778c141 --- /dev/null +++ b/tests/utils/test_collect_video_paths.py @@ -0,0 +1,207 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for ``collect_video_paths``. + +These tests pin down the rule: + +* When ``video_type`` is not set, directory enumeration filters by + ``SUPPORTED_VIDEOS`` but explicitly-supplied files are trusted (returned + as-is, even if they have no suffix). +* When ``video_type`` is set, it is honoured everywhere — both for files + pulled from directories and for files supplied by the caller. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from deeplabcut.utils.auxfun_videos import SUPPORTED_VIDEOS, collect_video_paths +from deeplabcut.utils.deprecation import DLCDeprecationWarning + + +def _touch(path: Path) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"") + return path + + +def test_keeps_suffixless_files_when_explicitly_listed(tmp_path): + """Regression test: a caller-supplied file without an extension (e.g. + a content-addressed cache entry) must not be silently dropped.""" + suffixed = _touch(tmp_path / "video.mp4") + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths([suffixed, hashed], extensions=None) + + assert {p.name for p in result} == {"video.mp4", "abcd1234"} + + +def test_accepts_path_objects_and_strings(tmp_path): + suffixed = _touch(tmp_path / "video.mp4") + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths([str(suffixed), hashed], extensions=None) + + assert {p.name for p in result} == {"video.mp4", "abcd1234"} + + +def test_accepts_single_path_argument(tmp_path): + """A single path (not wrapped in a list) is also valid input.""" + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths(hashed, extensions=None) + + assert [p.name for p in result] == ["abcd1234"] + + +def test_explicit_video_type_filters_listed_files(tmp_path): + """When ``extensions`` is set, it filters explicitly-supplied files too.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions="mp4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_explicit_video_type_accepts_leading_dot(tmp_path): + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions=".mp4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_explicit_video_type_case_insensitive(tmp_path): + """Extension matching must be case-insensitive.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions="MP4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_multiple_extensions_filter_directory(tmp_path): + """A sequence of extensions filters directory contents to only matching files.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + _touch(tmp_path / "video.mkv") + + result = collect_video_paths(tmp_path, extensions=["mp4", "avi"]) + + assert {p.name for p in result} == {mp4.name, avi.name} + + +def test_directory_enumeration_filters_by_supported_videos(tmp_path): + """Directory scans must continue to discriminate videos from non-videos.""" + mp4 = _touch(tmp_path / "video.mp4") + _touch(tmp_path / "notes.txt") + _touch(tmp_path / "results.h5") + _touch(tmp_path / "abcd1234") # suffix-less file in a directory: not a video + + result = collect_video_paths(tmp_path, extensions=None) + + assert [p.name for p in result] == [mp4.name] + + +def test_directory_enumeration_skips_dlc_artifacts(tmp_path): + """``*_labeled.*`` and ``*_full.*`` are DLC outputs, not inputs.""" + mp4 = _touch(tmp_path / "video.mp4") + _touch(tmp_path / "video_labeled.mp4") + _touch(tmp_path / "video_full.mp4") + + result = collect_video_paths(tmp_path, extensions=None) + + assert {p.name for p in result} == {mp4.name} + + +def test_disable_exclude_patterns_includes_dlc_artifacts(tmp_path): + """Setting ``exclude_patterns=[]`` disables all pattern exclusion.""" + mp4 = _touch(tmp_path / "video.mp4") + labeled = _touch(tmp_path / "video_labeled.mp4") + full = _touch(tmp_path / "video_full.mp4") + + result = collect_video_paths(tmp_path, extensions=None, exclude_patterns=[]) + + assert {p.name for p in result} == {mp4.name, labeled.name, full.name} + + +def test_mixed_files_and_directories(tmp_path): + """The function handles a mix of explicit files and directories.""" + folder = tmp_path / "folder" + in_folder = _touch(folder / "from_dir.mp4") + _touch(folder / "ignored.txt") + + explicit_mp4 = _touch(tmp_path / "explicit.mp4") + explicit_hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths( + [folder, explicit_mp4, explicit_hashed], + extensions=None, + ) + + assert {p.name for p in result} == { + in_folder.name, + explicit_mp4.name, + explicit_hashed.name, + } + + +def test_duplicates_are_removed(tmp_path): + mp4 = _touch(tmp_path / "video.mp4") + + result = collect_video_paths([mp4, mp4, str(mp4)], extensions=None) + + assert len(result) == 1 + assert result[0].name == "video.mp4" + + +def test_missing_path_raises(tmp_path): + with pytest.raises(FileNotFoundError): + collect_video_paths([tmp_path / "does_not_exist.mp4"], extensions=None) + + +@pytest.mark.parametrize("ext", SUPPORTED_VIDEOS) +def test_each_supported_extension_picked_up_in_directory(tmp_path, ext): + expected = _touch(tmp_path / f"clip.{ext}") + + result = collect_video_paths(tmp_path, extensions=None) + + assert [p.name for p in result] == [expected.name] + + +def test_sorted_by_default_when_not_shuffled(tmp_path): + a = _touch(tmp_path / "a.mp4") + b = _touch(tmp_path / "b.mp4") + c = _touch(tmp_path / "c.mp4") + + result = collect_video_paths([c, a, b], extensions=None, shuffle=False) + + assert [p.name for p in result] == ["a.mp4", "b.mp4", "c.mp4"] + + +@pytest.mark.parametrize("deprecated_value", ["", [""], ("",), {""}]) +def test_deprecated_empty_extensions_warns(tmp_path, deprecated_value): + """Empty / blank extension values are deprecated and should emit a warning.""" + _touch(tmp_path / "video.mp4") + + with pytest.warns(DLCDeprecationWarning): + collect_video_paths(tmp_path, extensions=deprecated_value) + + +def test_empty_sequence_raises(tmp_path): + """An empty sequence is not a valid filter; callers must pass None instead.""" + with pytest.raises(ValueError): + collect_video_paths(tmp_path, extensions=[]) diff --git a/tests/utils/test_deprecation.py b/tests/utils/test_deprecation.py new file mode 100644 index 0000000000..7f5a73fd5a --- /dev/null +++ b/tests/utils/test_deprecation.py @@ -0,0 +1,291 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import warnings + +import pytest +from packaging.version import Version + +from deeplabcut.utils.deprecation import ( + DLCDeprecationWarning, + deprecated, + renamed_parameter, +) + +# --------------------------------------------------------------------------- +# @deprecated +# --------------------------------------------------------------------------- + + +def test_deprecated_emits_deprecation_warning(): + @deprecated() + def old_fn(): + return 42 + + with pytest.warns(DLCDeprecationWarning): + result = old_fn() + + assert result == 42 + + +def test_deprecated_warning_contains_function_name(): + @deprecated() + def my_old_function(): + pass + + with pytest.warns(DLCDeprecationWarning, match="my_old_function"): + my_old_function() + + +def test_deprecated_warning_contains_replacement(): + @deprecated(replacement="new_module.new_fn") + def old_fn(): + pass + + with pytest.warns(DLCDeprecationWarning, match="new_module.new_fn"): + old_fn() + + +def test_deprecated_warning_contains_since_and_removed_in(): + @deprecated(since="3.1", removed_in="4.0") + def old_fn(): + pass + + with pytest.warns(DLCDeprecationWarning, match="3.1") as record: + old_fn() + + assert "4.0" in str(record[0].message) + + +def test_deprecated_preserves_return_value_and_args(): + @deprecated() + def add(a, b): + return a + b + + with pytest.warns(DLCDeprecationWarning): + assert add(2, 3) == 5 + + +def test_deprecated_preserves_name_and_docstring(): + @deprecated(replacement="new_fn") + def documented_fn(): + """Original docstring.""" + + assert documented_fn.__name__ == "documented_fn" + assert "Original docstring." in documented_fn.__doc__ + assert "Deprecated." in documented_fn.__doc__ + assert "new_fn" in documented_fn.__doc__ + + +def test_deprecated_attaches_metadata(): + @deprecated(replacement="new_fn", since="3.1", removed_in="4.0") + def old_fn(): + pass + + info = old_fn.__deprecated_info__ + assert info.kind == "callable" + assert info.target.endswith("old_fn") + assert info.replacement == "new_fn" + assert info.since == Version("3.1") + assert info.removed_in == Version("4.0") + + +def test_deprecated_invalid_since_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @deprecated(since="not-a-version") + def old_fn(): + pass + + +def test_deprecated_invalid_removed_in_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @deprecated(removed_in="definitely-not-a-version") + def old_fn(): + pass + + +def test_deprecated_removed_in_must_be_greater_than_since(): + with pytest.raises(ValueError, match="must be greater than"): + + @deprecated(since="4.0", removed_in="4.0") + def old_fn(): + pass + + +# --------------------------------------------------------------------------- +# @renamed_parameter +# --------------------------------------------------------------------------- + + +def test_renamed_parameter_old_name_emits_warning(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with pytest.warns(DLCDeprecationWarning): + fn(in_random_order=True) + + +def test_renamed_parameter_old_name_is_forwarded(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with pytest.warns(DLCDeprecationWarning): + result = fn(in_random_order=True) + + assert result is True + + +def test_renamed_parameter_new_name_no_warning(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + # No warning should be emitted when using the current name. + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + result = fn(shuffle=True) + + assert result is True + + +def test_renamed_parameter_warning_contains_names(): + @renamed_parameter(old="videotype", new="video_extensions", since="3.2") + def fn(video_extensions=None): + return video_extensions + + with pytest.warns(DLCDeprecationWarning, match="videotype") as record: + fn(videotype="mp4") + + message = str(record[0].message) + assert "video_extensions" in message + assert "3.2" in message + + +def test_renamed_parameter_preserves_name(): + @renamed_parameter(old="foo", new="bar") + def my_fn(bar=None): + """Docstring.""" + + assert my_fn.__name__ == "my_fn" + + +def test_renamed_parameter_old_and_new_together_raise(): + @renamed_parameter(old="videotype", new="video_extensions") + def fn(video_extensions=None): + return video_extensions + + with pytest.raises(TypeError, match="both 'videotype' and 'video_extensions'"): + fn(videotype="mp4", video_extensions="avi") + + +def test_renamed_parameter_attaches_metadata(): + @renamed_parameter(old="videotype", new="video_extensions", since="3.2") + def fn(video_extensions=None): + return video_extensions + + params = fn.__deprecated_params__ + assert len(params) == 1 + + info = params[0] + assert info.kind == "parameter" + assert info.target.endswith("fn") + assert info.old_parameter == "videotype" + assert info.new_parameter == "video_extensions" + assert info.since == Version("3.2") + + +def test_renamed_parameter_invalid_since_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @renamed_parameter(old="videotype", new="video_extensions", since="invalid-version") + def fn(video_extensions=None): + return video_extensions + + +def test_renamed_parameter_new_not_in_signature_raises(): + with pytest.raises(ValueError, match="not a parameter"): + + @renamed_parameter(old="foo", new="nonexistent") + def fn(bar=None): + return bar + + +def test_new_not_in_signature_raises(): + """Applying a rename whose 'new' is not in the signature raises an error.""" + with pytest.raises(ValueError, match="not a parameter"): + + @renamed_parameter(old="old_name", new="new_name") + def fn(not_new_name=None): + return not_new_name + + +def test_old_still_in_signature_raises(): + """Applying a rename when the old name is still in the signature raises an error.""" + with pytest.raises(ValueError, match="still a parameter"): + + @renamed_parameter(old="old_name", new="new_name") + def fn(old_name=None, new_name=None): + return new_name + + +def test_renamed_parameter_chaining_raises(): + """Chaining renames A→B→C raises an error.""" + with pytest.raises(ValueError, match="chaining renames is not allowed"): + + @renamed_parameter(old="A", new="B") # outer: A→B, but B is already deprecated to C + @renamed_parameter(old="B", new="C") # inner: B→C + def fn(C=None): + return C + + +def test_renamed_parameter_multiple_independent_renames(): + @renamed_parameter(old="batchsize", new="batch_size") + @renamed_parameter(old="videotype", new="video_extensions") + def fn(video_extensions=None, batch_size=None): + return video_extensions, batch_size + + with pytest.warns(DLCDeprecationWarning): + result = fn(videotype="mp4") + assert result == ("mp4", None) + + with pytest.warns(DLCDeprecationWarning): + result = fn(batchsize=4) + assert result == (None, 4) + + +def test_renamed_parameter_positional_arg_unaffected(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + result = fn(True) + + assert result is True + + +def test_multiple_subsequent_renames_allowed(): + @renamed_parameter(old="oldestname", new="newest", since="3.0.0") + @renamed_parameter(old="older_name", new="newest", since="4.0.0") + def fn(*, newest): + return newest + + with pytest.warns(DLCDeprecationWarning): + result = fn(oldestname=1) + assert result == 1 + + with pytest.warns(DLCDeprecationWarning): + result = fn(older_name=2) + assert result == 2 diff --git a/tests/utils/test_multiprocessing.py b/tests/utils/test_multiprocessing.py new file mode 100644 index 0000000000..a5ab47dd5f --- /dev/null +++ b/tests/utils/test_multiprocessing.py @@ -0,0 +1,40 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import time + +import pytest + +from deeplabcut.utils.multiprocessing import call_with_timeout + + +def _succeeding_method(parameter): + return parameter + + +def _failing_method(): + raise ValueError("Raise value error on purpose") + + +def _hanging_method(): + while True: + time.sleep(5) + + +@pytest.mark.skip(reason="Flaky on CI - imports that can exceed timeout on resource-constrained systems") +def test_call_with_timeout(): + parameter = (10, "Hello test") + assert call_with_timeout(_succeeding_method, 30, parameter) == parameter + + with pytest.raises(ValueError): + call_with_timeout(_failing_method, timeout=30) + + with pytest.raises(TimeoutError): + call_with_timeout(_hanging_method, timeout=1) diff --git a/tests/utils/test_skeleton.py b/tests/utils/test_skeleton.py new file mode 100644 index 0000000000..c83d5060d1 --- /dev/null +++ b/tests/utils/test_skeleton.py @@ -0,0 +1,374 @@ +import warnings +from types import SimpleNamespace + +import matplotlib + +matplotlib.use("Agg", force=True) + +import numpy as np +import pandas as pd +import pytest +from matplotlib.collections import LineCollection +from matplotlib.figure import Figure +from scipy.spatial import KDTree + +from deeplabcut.utils import skeleton as skeleton_mod +from deeplabcut.utils.skeleton import SkeletonBuilder, write_config + +# --------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------- + + +def make_config(project_path, scorer="TestScorer", skeleton=None): + return { + "project_path": str(project_path), + "scorer": scorer, + "skeleton": skeleton or [], + "skeleton_color": "red", + "dotsize": 4, + } + + +def make_test_builder(): + """ + Construct a SkeletonBuilder instance without calling __init__, + so individual methods can be unit-tested in isolation. + """ + builder = SkeletonBuilder.__new__(SkeletonBuilder) + return builder + + +def attach_fake_canvas(builder): + builder.fig = Figure() + builder.fig.canvas.draw_idle = lambda: None + + +# --------------------------------------------------------------------- +# pick_labeled_frame +# --------------------------------------------------------------------- + + +def test_pick_labeled_frame_multi_animal_drops_single(monkeypatch): + builder = make_test_builder() + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["single", "mouseA"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + + # "single" is fully labeled too, but should be dropped before choosing. + row = [ + 1.0, + 2.0, + 3.0, + 4.0, # single + 10.0, + 20.0, + 30.0, + 40.0, # mouseA + ] + builder.df = pd.DataFrame([row], index=index, columns=columns) + + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + picked_row, picked_col = builder.pick_labeled_frame() + + assert picked_row == ("labeled-data/session1", "img001.png") + assert picked_col == "mouseA" + + +def test_pick_labeled_frame_without_individuals(monkeypatch): + builder = make_test_builder() + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + + builder.df = pd.DataFrame( + [[1.0, 2.0, 3.0, 4.0]], + index=index, + columns=columns, + ) + + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + picked_row, picked_col = builder.pick_labeled_frame() + + assert picked_row == ("labeled-data/session1", "img001.png") + # fallback path uses count(...).to_frame(), so the single column is usually 0 + assert picked_col == 0 + + +# --------------------------------------------------------------------- +# clear +# --------------------------------------------------------------------- + + +def test_clear_resets_indices_segments_and_linecollection(): + builder = make_test_builder() + builder.inds = {(0, 1), (1, 2)} + builder.segs = { + ((0.0, 0.0), (10.0, 0.0)), + ((10.0, 0.0), (20.0, 0.0)), + } + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]]), np.array([[10.0, 0.0], [20.0, 0.0]])]) + attach_fake_canvas(builder) + + builder.clear() + + assert builder.inds == set() + assert builder.segs == set() + assert list(builder.lines.get_segments()) == [] + + +# --------------------------------------------------------------------- +# export +# --------------------------------------------------------------------- + + +def test_export_sorts_pairs_and_warns_for_unconnected(monkeypatch): + builder = make_test_builder() + builder.config_path = "dummy_config.yaml" + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + [30.0, 0.0], # intentionally left unconnected + ] + ) + builder.bpts = pd.Index(["nose", "tail", "paw", "ear"], name="bodyparts") + builder.inds = {(1, 2), (0, 1)} # intentionally unordered + builder.cfg = {"skeleton": []} + + captured = {} + + def fake_write_config(path, cfg): + captured["path"] = path + captured["cfg"] = cfg.copy() + + monkeypatch.setattr(skeleton_mod, "write_config", fake_write_config) + + with pytest.warns(UserWarning, match="didn't connect all the bodyparts"): + builder.export() + + assert captured["path"] == "dummy_config.yaml" + assert captured["cfg"]["skeleton"] == [ + ("nose", "tail"), + ("tail", "paw"), + ] + + +def test_export_without_warning_when_all_bodyparts_connected(monkeypatch): + builder = make_test_builder() + builder.config_path = "dummy_config.yaml" + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.bpts = pd.Index(["nose", "tail", "paw"], name="bodyparts") + builder.inds = {(0, 1), (1, 2)} + builder.cfg = {"skeleton": []} + + monkeypatch.setattr(skeleton_mod, "write_config", lambda path, cfg: None) + + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + builder.export() + + assert not any("didn't connect all the bodyparts" in str(w.message) for w in record) + assert builder.cfg["skeleton"] == [ + ("nose", "tail"), + ("tail", "paw"), + ] + + +# --------------------------------------------------------------------- +# on_select +# --------------------------------------------------------------------- + + +def test_on_select_adds_pairs_segments_and_updates_canvas(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = set() + builder.segs = set() + builder.lines = LineCollection([]) + attach_fake_canvas(builder) + + verts = [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)] + builder.on_select(verts) + + assert builder.inds == {(0, 1), (1, 2)} + assert ((0.0, 0.0), (10.0, 0.0)) in builder.segs + assert ((10.0, 0.0), (20.0, 0.0)) in builder.segs + assert len(builder.lines.get_segments()) == 2 + + +def test_on_select_ignores_duplicate_hits(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = set() + builder.segs = set() + builder.lines = LineCollection([]) + attach_fake_canvas(builder) + + # Repeated nearby vertices should not create duplicate pairs + verts = [(0.0, 0.0), (0.1, 0.0), (10.0, 0.0), (10.1, 0.0), (20.0, 0.0)] + builder.on_select(verts) + + assert builder.inds == {(0, 1), (1, 2)} + assert len(builder.segs) == 2 + + +# --------------------------------------------------------------------- +# on_pick +# --------------------------------------------------------------------- + + +def test_on_pick_right_click_removes_segment_and_pair(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = {(0, 1)} + builder.segs = {((0.0, 0.0), (10.0, 0.0))} + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]])]) + attach_fake_canvas(builder) + + event = SimpleNamespace( + mouseevent=SimpleNamespace(button=3), + artist=builder.lines, + ind=[0], + ) + + builder.on_pick(event) + + assert builder.inds == set() + assert builder.segs == set() + assert list(builder.lines.get_segments()) == [] + + +def test_on_pick_non_right_click_does_nothing(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = {(0, 1)} + builder.segs = {((0.0, 0.0), (10.0, 0.0))} + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]])]) + attach_fake_canvas(builder) + + event = SimpleNamespace( + mouseevent=SimpleNamespace(button=1), + artist=builder.lines, + ind=[0], + ) + + builder.on_pick(event) + + assert builder.inds == {(0, 1)} + assert builder.segs == {((0.0, 0.0), (10.0, 0.0))} + assert len(builder.lines.get_segments()) == 1 + + +# --------------------------------------------------------------------- +# __init__ lightweight integration +# --------------------------------------------------------------------- + + +def test_init_loads_dataframe_image_and_existing_skeleton(tmp_path, monkeypatch): + project_path = tmp_path / "project" + labeled_data = project_path / "labeled-data" / "session1" + labeled_data.mkdir(parents=True) + + cfg_path = project_path / "config.yaml" + cfg = make_config( + project_path=project_path, + scorer="TestScorer", + skeleton=[ + ["nose", "tail"], + ["missing", "nose"], + ], # second pair should be ignored + ) + write_config(cfg_path, cfg) + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + df = pd.DataFrame( + [[0.0, 0.0, 10.0, 0.0]], + index=index, + columns=columns, + ) + h5_path = labeled_data / "CollectedData_TestScorer.h5" + df.to_hdf(h5_path, key="df", mode="w") + + monkeypatch.setattr(skeleton_mod.io, "imread", lambda path: np.zeros((5, 5, 3), dtype=np.uint8)) + monkeypatch.setattr(SkeletonBuilder, "build_ui", lambda self: None) + monkeypatch.setattr(SkeletonBuilder, "display", lambda self: None) + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + builder = SkeletonBuilder(str(cfg_path)) + + assert builder.config_path == str(cfg_path) + assert list(builder.bpts) == ["nose", "tail"] + assert builder.xy.shape == (2, 2) + assert builder.image.shape == (5, 5, 3) + assert builder.inds == {(0, 1)} + assert ((0.0, 0.0), (10.0, 0.0)) in builder.segs + + +def test_init_raises_if_no_labeled_data_found(tmp_path, monkeypatch): + project_path = tmp_path / "project" + (project_path / "labeled-data").mkdir(parents=True) + + cfg_path = project_path / "config.yaml" + cfg = make_config(project_path=project_path, scorer="TestScorer") + write_config(cfg_path, cfg) + + monkeypatch.setattr(SkeletonBuilder, "build_ui", lambda self: None) + monkeypatch.setattr(SkeletonBuilder, "display", lambda self: None) + + with pytest.raises(IOError, match="No labeled data were found"): + SkeletonBuilder(str(cfg_path)) diff --git a/testscript_cli.py b/testscript_cli.py index 7b74543cdc..295a68f729 100644 --- a/testscript_cli.py +++ b/testscript_cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ modified from: https://github.com/DeepLabCut/DeepLabCut-core/testscript_cli.py by Mackenzie. @@ -9,25 +8,24 @@ It produces nothing of interest scientifically. """ -task = "Testcore" # Enter the name of your experiment Task -scorer = "Mackenzie" # Enter the name of the experimenter/labeler +import os +import platform + +import numpy as np +import pandas as pd -import os, subprocess, sys +import deeplabcut as dlc +from deeplabcut.core.engine import Engine +task = "Testcore" # Enter the name of your experiment Task +scorer = "Mackenzie" # Enter the name of the experimenter/labeler +print("Imported DLC!") +engine = Engine.PYTORCH # def install(package): # subprocess.check_call([sys.executable, "-m", "pip", "install", package]) # install("tensorflow==1.13.1") -import deeplabcut as dlc - -from pathlib import Path -import pandas as pd -import numpy as np -import platform - -print("Imported DLC!") - basepath = os.path.dirname(os.path.abspath("testscript_cli.py")) videoname = "reachingvideo1" video = [ @@ -105,7 +103,7 @@ videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -116,33 +114,14 @@ print("CREATING TRAININGSET") dlc.create_training_dataset( - path_config_file, net_type=net_type, augmenter_type=augmenter_type + path_config_file, + net_type=net_type, + augmenter_type=augmenter_type, + engine=engine, ) -posefile = os.path.join( - cfg["project_path"], - "dlc-models/iteration-" - + str(cfg["iteration"]) - + "/" - + cfg["Task"] - + cfg["date"] - + "-trainset" - + str(int(cfg["TrainingFraction"][0] * 100)) - + "shuffle" - + str(1), - "train/pose_cfg.yaml", -) - -DLC_config = dlc.auxiliaryfunctions.read_plainconfig(posefile) -DLC_config["save_iters"] = numiter -DLC_config["display_iters"] = 2 -DLC_config["multi_step"] = [[0.001, numiter]] - -print("CHANGING training parameters to end quickly!") -dlc.auxiliaryfunctions.write_plainconfig(posefile, DLC_config) - print("TRAIN") -dlc.train_network(path_config_file) +dlc.train_network(path_config_file, epochs=numiter, displayiters=2) print("EVALUATE") dlc.evaluate_network(path_config_file, plotting=True) @@ -166,7 +145,8 @@ dlc.create_training_dataset(path_config_file, Shuffles=[2],net_type=net_type,augmenter_type=augmenter_type2) cfg=dlc.auxiliaryfunctions.read_config(path_config_file) -posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(2),'train/pose_cfg.yaml') +posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + +'-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(2),'train/pose_cfg.yaml') DLC_config=dlc.auxiliaryfunctions.read_plainconfig(posefile) DLC_config['save_iters']=numiter DLC_config['display_iters']=1 @@ -190,5 +170,6 @@ dlc.export_model(path_config_file, shuffle=1, make_tar=False) print( - "ALL DONE!!! - default/imgaug cases of DLCcore training and evaluation are functional (no extract outlier or refinement tested)." + "ALL DONE!!! - default/imgaug cases of DLCcore training and evaluation are functional (no extract outlier or" + "refinement tested)." ) diff --git a/tools/README.md b/tools/README.md index 32389da346..20d1b29f8b 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,13 +1,209 @@ # Developer tools useful for maintaining the repository -## Code headers +This document summarizes the developer tooling and workflows used in this repo. -The code headers can be standardized by running +```bash +pip install -e . --group dev +``` + +--- + +## 1) Pre-commit (recommended) + +Enable the repository hooks locally: + +```bash +pre-commit install +``` + +Run on all files: + +Steering committee members may edit the `NOTICE.yml` to update the header. + +## 2) Ruff cleanup helpers + +For **local Ruff backlog work** (not a substitute for CI or pre-commit), see [Ruff cleanup helpers](ruff_cleanup_helpers.md). It documents `generate_ruff_report.py` (Markdown report from Ruff JSON) and `fix_e501_with_autopep8.py` (targeted long-line cleanup plus Ruff fix/format). + +--- + +## 3) License headers + +Code headers can be standardized by running: + +Please follow the instructions in `CONTRIBUTING.md` for contributing to the codebase, including running tests and pre-commit checks before opening a pull request. + +Run from the repository root. Update `NOTICE.yml` to change header content. + +--- + +## 4) Running tests locally + +### Run the full test suite + +```bash +pytest +``` + +### Run a specific test module or folder + +```bash +coverage run -m pytest +coverage report +``` + +## 5) Intelligent test selection (local + CI) + +The repository includes a deterministic test-selection tool to reduce CI runtime by running only the relevant workflows and tests based on changed files. + +### What it outputs + +The selector emits **orthogonal workflow lanes** plus structured selections: + +- `lanes`: which workflow lanes should run + - `skip`: skip test execution entirely (for lint-only changes) + - `docs`: run docs checks + - `fast`: run targeted pytest paths and optional functional scripts + - `full`: delegate to the full test workflow / matrix +- `pytest_paths`: list of pytest path arguments (JSON) +- `functional_scripts`: list of Python scripts to run (JSON) +- `provenance`: mapping from each selected test/script to the category rule(s) that selected it + +It also emits audit metadata: + +- `selected_workflows`: ordered list of enabled lanes (`skip`, `docs`, `fast`, `full`) +- `lane_reasons`: reasons for each enabled lane +- `diff_mode`: how the diff range was determined +- `reasons`: aggregate machine-readable reasons for the decision +- `changed_files`: files considered for the decision +- `schema_version`: output schema version + +### Rule configuration + +Routing rules are defined in `tools/test_selector_config.py`. + +That file contains: + +- reusable path predicate helpers such as `prefix(...)`, `suffix(...)`, `equals(...)`, `case_insensitive_match(...)`, and `all_of(...)` +- conservative `FULL_SUITE_TRIGGERS` +- `LINT_ONLY_FILES` +- validated `CATEGORY_RULES` built from the `CategoryRule` schema +- `CATEGORY_RULE_BY_NAME` for stable lookup of named rules such as `docs` + +The current refactor keeps the rule predicates simple and location-based while validating the rule structure at import time. + +### Run locally (no CI env required) + +> [!IMPORTANT] +> Requires `pydantic>=2,<3` + +Print the decision as JSON: -``` bash -python tools/update_license_headers.py +```bash +python tools/test_selector.py --json ``` -from the repository root. +Write the decision report (`selection.json` and `decision.md`) under `tmp/test-selection/`: + +```bash +python tools/test_selector.py --report-dir tmp/test-selection --json +``` + +Write a GitHub job-summary-compatible Markdown report when `GITHUB_STEP_SUMMARY` is available: + +```bash +python tools/test_selector.py --report-dir tmp/test-selection --write-summary +``` + +Override the diff range manually: + +```bash +python tools/test_selector.py --base-sha --head-sha --json +``` + +In GitHub Actions, the workflow typically adds `--write-github-output` and `--write-summary`. + +### Diff modes + +The selector records how the diff was determined in `diff_mode`: + +- `pr`: pull request diff using `merge-base(base, head)..head` +- `push`: push diff using `before..after` +- `manual`: explicit `--base-sha` / `--head-sha` +- `fallback`: fallback to `HEAD^..HEAD` +- `initial`: initial commit (`empty-tree..HEAD`) +- `fallback_no_head`: could not resolve `HEAD` + +### Report files + +The selector always writes report artifacts for transparency: + +- `tmp/test-selection/selection.json`: machine-readable output +- `tmp/test-selection/decision.md`: human-readable summary with workflow lanes, reasons, explained changed files, selected tests, and provenance + +These reports are especially useful when a change unexpectedly routes to `full`. + +### Notes + +- The selector can enable more than one lane at once. For example, a PR can legitimately enable both `docs` and `fast`, or `docs` and `full`. +- Docs changes are **orthogonal** to test routing: docs changes can enable the docs lane while still contributing selected tests/scripts if such rules are configured. +- `LINT_ONLY_FILES` are ignored for routing. If *only* lint-only files changed, the selector enables the `skip` lane. +- If category rules match changed files but do not contribute explicit tests/scripts, the selector can fall back to the minimal pytest set defined by `MINIMAL_PYTEST`. + +### Troubleshooting the selector + +If a workflow run is unexpectedly selecting `full`, check: + +- `tmp/test-selection/decision.md` +- `tmp/test-selection/selection.json` +- `lane_reasons` +- `diff_mode` +- `changed_files` + +Common causes include: + +- a file matched a conservative full-suite trigger +- no category rule matched the routed files +- selected paths configured by a rule no longer exist in the repository +- diff resolution fell back because CI checkout history was incomplete + +--- + +## 6) Docs: Jupyter Book build (local) + +The repo uses Jupyter Book for docs: + +```bash +python -m pip install -U pip +python -m pip install .[docs] +jupyter-book build . +``` + +`.github/workflows/build-book.yml` is the canonical CI implementation. + +--- + +## 7) Testing the test selector + +The selector has dedicated tests covering: + +- decision behavior for docs / fast / full / skip routing +- provenance and deduplicated selections +- `CategoryRule` schema validation +- integrity checks for the currently defined rules + +Run the selector-focused tests with: + +```bash +pytest tests/tools/test_selector/ +``` + +--- + +## 8) Troubleshooting tips -You can edit the `NOTICE.yml` to update the header. +- If a workflow run is unexpectedly selecting `full`, inspect the selector reports first. +- If targeted tests fail due to missing dependencies, either: + - broaden the fast-lane install (for example by installing required extras), or + - adjust selection rules so that the fast lane only selects tests that run in the minimal environment. +- If manual diff selection is used, always pass both `--base-sha` and `--head-sha` together. +- In CI, ensure checkout history is deep enough for `merge-base` / `diff` operations (`fetch-depth: 0` is typically safest). diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py new file mode 100644 index 0000000000..0d720814d8 --- /dev/null +++ b/tools/docs_and_notebooks_check.py @@ -0,0 +1,1259 @@ +"""DeepLabCut docs & notebooks automated checks tool. + +Goals +----- +- SAFE by default: read-only operations in CI (report/check). +- Idempotent updates (update mode) that only touch: + * Notebook-level metadata for .ipynb (never cells/outputs) + * YAML frontmatter for .md docs (optional) +- Uses pydantic schemas with explicit schema_version for validation. +- Aims to be contributor-friendly by default: check mode enforces configured policy, while + surfacing scan/parsing issues without failing unless strict mode is enabled. + +Terminology +----------- +last_content_updated + Computed from git history, excluding metadata-only commits. + (Metadata commits must include META_COMMIT_MARKER in the commit message.) + +last_verified + Human-controlled date indicating the file was verified to work/be accurate. + +verified_for + Human-controlled string, typically the project version (e.g. 3.0.0rc13). + +tier + Optional classification (left unset by default; do not auto-populate). + +Usage modes +----------- +Report (read-only): + python tools/docs_and_notebooks_check.py report + +Check (read-only; policy enforcement): + python tools/docs_and_notebooks_check.py check + + Runs scans and evaluates configured policy rules. + Exits non-zero for policy violations. + Scan/parsing errors are always reported in console / JSON / Markdown output, + but are non-fatal by default unless strict mode is enabled or they imply a + policy violation. + +Update content-date field from git (write mode; requires --write): + python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git + +Update verification fields for selected targets (write mode): + python tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ + --set-last-verified today --set-verified-for 3.0.0rc13 + +Normalize notebooks deterministically (explicit churn; write mode): + python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb + + +Configuration +------------- +Uses tools/docs_and_notebooks_report_config.yml by default. + +Outputs +------- +- docs_nb_checks.json: machine-readable report +- docs_nb_checks.md: human-readable summary + +Notes for CI +------------ +- Ensure actions/checkout uses fetch-depth: 0 (or sufficiently deep), + otherwise git log may not see history. +- Requires: + - pydantic>=2,<3 + - PyYAML + - nbformat>=5 + to be installed in the environment. + Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) + rather than adding to requirements, since these are only needed for this tool. +""" + +# tools/docs_and_notebooks_check.py +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import re +import subprocess +from collections.abc import Sequence +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Literal, TypedDict + +import nbformat +import yaml +from nbformat.validator import NotebookValidationError +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +SCHEMA_VERSION = 1 +GLOB_CHARS = set("*?[") +DLC_NAMESPACE = "deeplabcut" +OUTPUT_FILENAME = "docs_nb_checks" +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_CFG = SCRIPT_DIR / "docs_and_notebooks_report_config.yml" + + +# ----------------------------- +# Metadata commit marker / guidance +# ----------------------------- +# IMPORTANT: +# Metadata-only updates and notebook normalization rewrite files and will change +# "git last touched" timestamps. To preserve meaningful "content age", all such +# commits must include this marker in the commit message. +META_COMMIT_MARKER = "chore(metadata)" +SUGGESTED_TAGGED_COMMIT = f"{META_COMMIT_MARKER}: update docs/notebooks metadata" + + +# ----------------------------- +# Pydantic schemas +# ----------------------------- + + +class DLCMeta(BaseModel): + """Metadata embedded in files under the `deeplabcut` namespace.""" + + model_config = ConfigDict(extra="allow") + + # Tool-managed: last meaningful content update date (excluding metadata commits) + last_content_updated: date | None = None + + # Optional tool-managed: last time metadata/normalization was performed + last_metadata_updated: date | None = None + # Optional human-managed verification fields + last_verified: date | None = None + # Version or other string indicating what this file was verified for (e.g. "3.0.0rc13") + verified_for: str | None = None + # Extra metadata fields for later usage (e.g. allowlist tier classification), but not currently used by the tool + tier: str | None = None + ignore: bool = False + notes: str | None = None + + +class ScanConfig(BaseModel): + include: list[str] = Field(default_factory=list) + exclude: list[str] = Field(default_factory=list) + + +class PolicyConfig(BaseModel): + warn_if_content_older_than_days: int = 365 + warn_if_verified_older_than_days: int = 365 + missing_last_verified_is_warning: bool = True + + # Strict-mode toggle: if true, scan/parsing errors also fail `check` + fail_on_scan_errors: bool = False + + # Allowlists for strict checks (start empty; ratchet later) + require_metadata: list[str] = Field(default_factory=list) + require_recent_verification: list[str] = Field(default_factory=list) + + require_notebook_normalized: list[str] = Field(default_factory=list) + + +class ToolConfig(BaseModel): + version: int = 1 + scan: ScanConfig + policy: PolicyConfig + + +FileKind = Literal["ipynb", "md", "other"] + + +class FileRecord(BaseModel): + path: str + kind: FileKind + + # Computed from git (excluding metadata-only commits) + last_content_updated: date | None = None + # Debug-only: raw git last touched (may be metadata commit) + last_git_touched: date | None = None + + # Read from file metadata/frontmatter + meta: DLCMeta | None = None + + # Derived + days_since_content_update: int | None = None + days_since_verified: int | None = None + + warnings: list[str] = Field(default_factory=list) + errors: list[str] = Field(default_factory=list) + + # If update mode would change file + would_change: bool = False + + +class Report(BaseModel): + schema_version: int = SCHEMA_VERSION + generated_at: datetime + repo_root: str + config_path: str + + totals: dict[str, int] + records: list[FileRecord] + + +# Rebuild models due to __future__ annotations +DLCMeta.model_rebuild() +ScanConfig.model_rebuild() +PolicyConfig.model_rebuild() +ToolConfig.model_rebuild() +FileRecord.model_rebuild() +Report.model_rebuild() + +TargetKind = Literal["invalid", "file", "dir", "glob"] + + +class TargetSpec(TypedDict): + raw: str + normalized: str + kind: TargetKind + + +# ----------------------------- +# Helpers +# ----------------------------- +def normalize_target_spec(spec: str, repo_root: Path) -> str: + """ + Normalize a CLI target into a repo-relative POSIX-style path/pattern. + + Examples + -------- + .\\docs\\a.md -> docs/a.md + ./docs/a.md -> docs/a.md + docs\\gui\\ -> docs/gui + /abs/path/in/repo/a.md -> docs/a.md (if inside repo) + """ + s = spec.strip() + if not s: + return s + + # Normalize slashes first so Windows-style input works everywhere + s = s.replace("\\", "/") + + # Strip leading ./ repeatedly + while s.startswith("./"): + s = s[2:] + + # If absolute and inside repo, make it repo-relative + p = Path(s) + if p.is_absolute(): + try: + s = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + except ValueError: + # Outside repo: keep normalized absolute text so it can fail validation cleanly + s = str(p).replace(os.sep, "/") + + # Collapse repeated slashes + s = re.sub(r"/+", "/", s) + + # Remove trailing slash for canonical matching + if len(s) > 1: + s = s.rstrip("/") + + return s + + +def compile_target_specs(targets: list[str] | None, repo_root: Path) -> list[TargetSpec] | None: + """ + Convert raw CLI targets into normalized selector specs. + + Each spec is a dict with: + - raw: original user input + - normalized: normalized repo-relative selector + - kind: file | dir | glob | invalid + """ + if not targets: + return None + + specs: list[TargetSpec] = [] + + for raw in targets: + normalized = normalize_target_spec(raw, repo_root) + if not normalized: + specs.append({"raw": raw, "normalized": "", "kind": "invalid"}) + continue + + if any(ch in normalized for ch in GLOB_CHARS): + specs.append({"raw": raw, "normalized": normalized, "kind": "glob"}) + continue + + # Treat explicit trailing slash/backslash as directory intent + if raw.endswith(("/", "\\")): + specs.append({"raw": raw, "normalized": normalized, "kind": "dir"}) + continue + + candidate = Path(normalized) + abs_candidate = candidate if candidate.is_absolute() else (repo_root / candidate) + if abs_candidate.exists() and abs_candidate.is_dir(): + specs.append({"raw": raw, "normalized": normalized, "kind": "dir"}) + else: + specs.append({"raw": raw, "normalized": normalized, "kind": "file"}) + + return specs + + +def target_spec_matches_path(rel_path: str, spec: TargetSpec) -> bool: + rel_path = rel_path.replace("\\", "/") + + kind = spec["kind"] + normalized = spec["normalized"] + + if kind == "invalid": + return False + + if kind == "file": + return rel_path == normalized + + if kind == "dir": + return rel_path == normalized or rel_path.startswith(normalized + "/") + + if kind == "glob": + # Intentionally use simple shell-style matching here so patterns like + # docs/**/*.md behave the way users generally expect across platforms. + return fnmatch.fnmatchcase(rel_path, normalized) + + return False + + +def target_matches(rel_path: str, specs: list[TargetSpec] | None) -> bool: + if specs is None: + return True + return any(target_spec_matches_path(rel_path, spec) for spec in specs) + + +def iter_scan_candidate_paths(repo_root: Path, cfg: ToolConfig) -> list[str]: + """ + Return all repo-relative paths that are in scope for scanning, before applying --targets. + """ + rels: list[str] = [] + for p in glob_paths(repo_root, cfg.scan.include): + rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, cfg.scan.exclude): + continue + rels.append(rel) + return sorted(set(rels)) + + +def validate_requested_targets( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, +) -> tuple[list[str], list[str]]: + """ + Validate CLI target selectors against the current scan universe. + + Returns: + matched_paths: repo-relative file paths matched by any selector + unmatched_targets: raw target strings that matched nothing + """ + if not targets: + return [], [] + + specs = compile_target_specs(targets, repo_root) + candidates = iter_scan_candidate_paths(repo_root, cfg) + + matched_paths = sorted({rel for rel in candidates if target_matches(rel, specs)}) + + unmatched_targets: list[str] = [] + for spec in specs or []: + if spec["kind"] == "invalid": + unmatched_targets.append(spec["raw"]) + elif not any(target_spec_matches_path(rel, spec) for rel in candidates): + unmatched_targets.append(spec["raw"]) + + return matched_paths, unmatched_targets + + +def print_target_match_summary(matched_paths: list[str], requested_targets: list[str] | None) -> None: + """ + Print a small CLI summary whenever --targets is used. + """ + if not requested_targets: + return + + print(f"\nMatched {len(matched_paths)} file(s) from --targets:") + preview_limit = 50 + for rel in matched_paths[:preview_limit]: + print(f"- {rel}") + if len(matched_paths) > preview_limit: + print(f"... and {len(matched_paths) - preview_limit} more") + + +def _iso_today() -> date: + return datetime.now(timezone.utc).date() + + +def _run_git(args: Sequence[str], cwd: Path) -> tuple[int, str, str]: + p = subprocess.run( + ["git", *args], + cwd=str(cwd), + capture_output=True, + text=True, + ) + return p.returncode, p.stdout.strip(), p.stderr.strip() + + +def find_repo_root(start: Path) -> Path: + cur = start.resolve() + for _ in range(50): + if (cur / ".git").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + code, out, _err = _run_git(["rev-parse", "--show-toplevel"], cwd=start) + if code == 0 and out: + return Path(out).resolve() + raise RuntimeError("Could not locate repository root") + + +def glob_paths(repo_root: Path, patterns: list[str]) -> list[Path]: + results: list[Path] = [] + for pat in patterns: + results.extend(repo_root.glob(pat)) + return sorted({p.resolve() for p in results if p.is_file()}) + + +def is_excluded(rel_path: str, exclude_patterns: list[str]) -> bool: + return any(fnmatch.fnmatch(rel_path, pat) for pat in exclude_patterns) + + +def file_kind(path: Path) -> str: + s = path.suffix.lower() + if s == ".ipynb": + return "ipynb" + if s in {".md", ".markdown"}: + return "md" + return "other" + + +def _parse_git_iso_date(out: str) -> date | None: + out = (out or "").strip() + if not out: + return None + + try: + return date.fromisoformat(out) + except Exception: + pass + + try: + if out.endswith("Z"): + out = out[:-1] + "+00:00" + return datetime.fromisoformat(out).date() + except Exception: + return None + + +def _git_log_date(repo_root: Path, rel_path: str, extra_args: Sequence[str] = ()) -> date | None: + args = [ + "log", + "-1", + "--date=short", + "--format=%cd", + *extra_args, + "--", + rel_path, + ] + code, out, _err = _run_git(args, cwd=repo_root) + if code != 0: + return None + return _parse_git_iso_date(out) + + +def git_last_touched(repo_root: Path, rel_path: str) -> date | None: + return _git_log_date(repo_root, rel_path) + + +def git_last_content_updated(repo_root: Path, rel_path: str) -> tuple[date | None, bool]: + d = _git_log_date( + repo_root, + rel_path, + extra_args=[ + "--fixed-strings", + "--invert-grep", + "--grep", + META_COMMIT_MARKER, + ], + ) + if d is not None: + return d, False + return git_last_touched(repo_root, rel_path), True + + +FRONTMATTER_RE = re.compile(r"^---\s*$") + + +def read_md_frontmatter(text: str) -> tuple[dict | None, str, str | None]: + lines = text.splitlines(keepends=True) + if not lines or not FRONTMATTER_RE.match(lines[0]): + return None, text, None + + end_idx = None + for i in range(1, min(len(lines), 5000)): + if FRONTMATTER_RE.match(lines[i]): + end_idx = i + break + + if end_idx is None: + return None, text, "unterminated_markdown_frontmatter" + + fm_text = "".join(lines[1:end_idx]) + body = "".join(lines[end_idx + 1 :]) + + if yaml is None: + raise RuntimeError("PyYAML is required to parse Markdown frontmatter") + + fm = yaml.safe_load(fm_text) if fm_text.strip() else {} + if not isinstance(fm, dict): + return None, text, "markdown_frontmatter_not_mapping" + + return fm, body, None + + +def dump_md_frontmatter(frontmatter: dict, body: str) -> str: + if yaml is None: + raise RuntimeError("PyYAML is required to write Markdown frontmatter") + fm_text = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True) + body_to_write = body + if body_to_write.startswith("\n"): + body_to_write = body_to_write[1:] + return "---\n" + fm_text + "---\n" + body_to_write + + +def read_ipynb_meta(path: Path) -> tuple[Any, dict, bool]: + """ + Read a notebook using nbformat. + Returns (notebook_node, deeplabcut_meta_dict, has_dlc_namespace). + """ + nb = nbformat.read(str(path), as_version=4) + + meta = getattr(nb, "metadata", {}) or {} + has_dlc = DLC_NAMESPACE in meta + + raw_dlc_meta = meta.get(DLC_NAMESPACE) + return nb, raw_dlc_meta, has_dlc + + +def notebook_is_normalized(path: Path, nb: Any) -> bool: + original = path.read_text(encoding="utf-8") + # Normalize newline style so CRLF vs LF differences do not cause false mismatches + original_normalized = original.replace("\r\n", "\n").replace("\r", "\n") + normalized = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n" + return original_normalized == normalized + + +def write_ipynb_meta(path: Path, nb: Any) -> None: + """ + Write a notebook using nbformat. + + Note: nbformat writes JSON in a canonical form; it *will* rewrite the file, + so expect diffs if the notebook wasn't previously normalized to the same style. + """ + # Validate before writing (optional but recommended) + nbformat.validate(nb) + + # Use a stable indentation to reduce churn (choose 2 if your repo tends that way) + text = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + + path.write_text(text + "\n", encoding="utf-8") + + +def parse_dlc_meta(raw: Any) -> tuple[DLCMeta | None, bool]: + # returns (meta, valid) + if raw is None or not isinstance(raw, dict): + return None, False + try: + return DLCMeta.model_validate(raw), True + except ValidationError: + return None, False + + +def meta_to_jsonable(meta: DLCMeta) -> dict: + """ + Return JSON-serializable metadata (dates become ISO strings). + This prevents json.dumps() from failing when writing .ipynb files. + """ + return meta.model_dump(mode="json", exclude_none=True) + + +def compute_days_since(d: date | None, today: date) -> int | None: + return None if d is None else (today - d).days + + +def match_allowlist(rel_path: str, allowlist: list[str]) -> bool: + # Support exact matches or glob patterns + return any(pat == rel_path or fnmatch.fnmatch(rel_path, pat) for pat in allowlist) + + +# ----------------------------- +# Core scanning +# ----------------------------- + + +def load_config(config_path: Path) -> ToolConfig: + if yaml is None: + raise RuntimeError("PyYAML is required (pip install pyyaml)") + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) + return ToolConfig.model_validate(raw) + + +def scan_files(repo_root: Path, cfg: ToolConfig, targets: list[str] | None = None) -> list[FileRecord]: + today = _iso_today() + paths = glob_paths(repo_root, cfg.scan.include) + records: list[FileRecord] = [] + target_specs = compile_target_specs(targets, repo_root) + + for p in paths: + rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, cfg.scan.exclude): + continue + if not target_matches(rel, target_specs): + continue + kind = file_kind(p) + rec = FileRecord(path=rel, kind=kind) + + rec.last_git_touched = git_last_touched(repo_root, rel) + rec.last_content_updated, used_fallback = git_last_content_updated(repo_root, rel) + rec.days_since_content_update = compute_days_since(rec.last_content_updated, today) + if used_fallback: + rec.warnings.append("content_date_fallback_to_git_touched") + + try: + if kind == "ipynb": + nb, raw_meta, has_dlc = read_ipynb_meta(p) + + try: + nbformat.validate(nb) + except NotebookValidationError as e: + rec.errors.append(f"nbformat_invalid: {e}") + + try: + if not notebook_is_normalized(p, nb): + rec.warnings.append("notebook_not_normalized") + except Exception as e: + rec.errors.append(f"notebook_normalization_check_failed: {e}") + + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw_meta) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + + elif kind == "md": + text = p.read_text(encoding="utf-8") + fm, _body, fm_error = read_md_frontmatter(text) + + if fm_error: + rec.meta = None + rec.warnings.append("invalid_metadata") + rec.errors.append(f"markdown_frontmatter_invalid: {fm_error}") + else: + fm = fm or {} + has_dlc = DLC_NAMESPACE in fm + raw = fm.get(DLC_NAMESPACE) + + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + + else: + rec.meta = None + + except Exception as e: + rec.errors.append(f"metadata_read_failed: {e}") + + # ignore=True means: keep reporting diagnostics, but skip freshness/policy logic + if rec.meta and rec.meta.ignore: + records.append(rec) + continue + + last_verified = rec.meta.last_verified if rec.meta else None + rec.days_since_verified = compute_days_since(last_verified, today) + + # Future dates are data errors + if rec.last_content_updated is not None and rec.last_content_updated > today: + rec.errors.append("future_last_content_updated") + if rec.meta and rec.meta.last_metadata_updated is not None: + if rec.meta.last_metadata_updated > today: + rec.errors.append("future_last_metadata_updated") + if last_verified is not None and last_verified > today: + rec.errors.append("future_last_verified") + + pol = cfg.policy + + if ( + rec.days_since_content_update is not None + and rec.days_since_content_update > pol.warn_if_content_older_than_days + ): + rec.warnings.append(f"content_stale>{pol.warn_if_content_older_than_days}d") + + if last_verified is None and pol.missing_last_verified_is_warning: + rec.warnings.append("missing_last_verified") + elif rec.days_since_verified is not None and rec.days_since_verified > pol.warn_if_verified_older_than_days: + rec.warnings.append(f"verified_stale>{pol.warn_if_verified_older_than_days}d") + + records.append(rec) + + return records + + +# ----------------------------- +# Update mode +# ----------------------------- +def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: + """ + Guardrail: writing metadata/normalization without the marker convention will + destroy the meaning of content freshness signals. Require an explicit ack. + """ + if not write: + return + if ack_marker: + return + raise SystemExit( + "Refusing to write without acknowledging metadata-commit convention.\n" + "Re-run with --ack-meta-commit-marker and commit with:\n" + f" {SUGGESTED_TAGGED_COMMIT}\n" + ) + + +def update_files( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, + write: bool, + set_content_date_from_git: bool, + set_last_verified: date | None, + set_verified_for: str | None, + ack_meta_commit_marker: bool, +) -> list[FileRecord]: + today = _iso_today() + records = scan_files(repo_root, cfg, targets=targets) + + for rec in records: + if rec.kind not in {"ipynb", "md"}: + continue + if rec.meta and rec.meta.ignore: + continue + + meta = rec.meta or DLCMeta() + + # Build the desired metadata WITHOUT touching last_metadata_updated. + if set_content_date_from_git and rec.last_content_updated is not None: + meta.last_content_updated = rec.last_content_updated + + if set_last_verified is not None: + meta.last_verified = set_last_verified + if set_verified_for is not None: + meta.verified_for = set_verified_for + + desired_base = meta_to_jsonable(meta) + abs_path = repo_root / rec.path + changed = False + + if rec.kind == "ipynb": + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: + changed = True + if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + nb_meta[DLC_NAMESPACE] = merged_final + write_ipynb_meta(abs_path, nb) + + elif rec.kind == "md": + text = abs_path.read_text(encoding="utf-8") + fm, body, fm_error = read_md_frontmatter(text) + if fm_error: + msg = f"markdown_frontmatter_invalid: {fm_error}" + if msg not in rec.errors: + rec.errors.append(msg) + continue + + fm = fm or {} + + prev = fm.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: + changed = True + if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + fm[DLC_NAMESPACE] = merged_final + abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") + + rec.would_change = changed + rec.meta = meta + rec.days_since_verified = compute_days_since(meta.last_verified, today) + + return records + + +# ----------------------------- +# Notebook formatting +# ----------------------------- +def normalize_notebooks( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, + write: bool, + ack_meta_commit_marker: bool, +) -> list[FileRecord]: + """ + Normalize notebooks deterministically (canonical nbformat JSON). + This is intentionally separated from update() because it causes churn. + """ + _require_meta_marker_ack(write=write, ack_marker=ack_meta_commit_marker) + records = scan_files(repo_root, cfg, targets=targets) + today = _iso_today() + + for rec in records: + if rec.kind != "ipynb": + continue + if rec.meta and rec.meta.ignore: + continue + + abs_path = repo_root / rec.path + try: + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) + nbformat.validate(nb) + + if not notebook_is_normalized(abs_path, nb): + rec.would_change = True + if write: + # Update embedded maintenance timestamp + meta = rec.meta or DLCMeta() + meta.last_metadata_updated = today + + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + merged = dict(prev) + merged.update(meta_to_jsonable(meta)) + nb_meta[DLC_NAMESPACE] = merged + + # Write to persist metadata update (still canonical) + write_ipynb_meta(abs_path, nb) + rec.meta = meta + + except Exception as e: + rec.errors.append(f"normalize_failed: {e}") + + return records + + +# ----------------------------- +# Output formatting +# ----------------------------- + + +def summarize(records: list[FileRecord]) -> dict[str, int]: + return { + "files": len(records), + "warnings": sum(1 for r in records if r.warnings), + "errors": sum(1 for r in records if r.errors), + "missing_metadata": sum(1 for r in records if "missing_metadata" in r.warnings), + "missing_last_verified": sum(1 for r in records if "missing_last_verified" in r.warnings), + "content_stale": sum(1 for r in records if any(w.startswith("content_stale") for w in r.warnings)), + "verified_stale": sum(1 for r in records if any(w.startswith("verified_stale") for w in r.warnings)), + } + + +def to_markdown(report: Report, cfg: ToolConfig) -> str: + pol = cfg.policy + t = report.totals + lines: list[str] = [] + + lines.append("# 🌡️ DeepLabCut freshness report\n") + lines.append(f"Generated: {report.generated_at.isoformat()}\n") + lines.append(f"Schema: v{report.schema_version}\n\n") + + lines.append("## Summary\n") + lines.append(f"- Files scanned: **{t['files']}**\n") + lines.append(f"- Files with warnings: **{t['warnings']}**\n") + lines.append(f"- Files with scanning errors: **{t['errors']}**\n") + lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") + lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") + lines.append(f"- Content-stale (> {pol.warn_if_content_older_than_days}d): **{t['content_stale']}**\n") + lines.append(f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n") + + def fmt_date(d: date | None) -> str: + return d.isoformat() if d else "-" + + warn_recs = [r for r in report.records if r.warnings and not (r.meta and r.meta.ignore)] + warn_recs.sort( + key=lambda r: ( + -(r.days_since_verified or -1), + -(r.days_since_content_update or -1), + r.path, + ) + ) + + if warn_recs: + lines.append("## Warnings\n") + for r in warn_recs: + meta = r.meta + lines.append(f"- **{r.path}** ({r.kind})\n") + lines.append( + f" - last_content_updated: {fmt_date(r.last_content_updated)} " + f"(days: {r.days_since_content_update if r.days_since_content_update is not None else '-'})\n" + ) + if r.last_git_touched: + lines.append(f" - last_git_touched: {fmt_date(r.last_git_touched)}\n") + if meta and meta.last_metadata_updated: + lines.append(f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n") + lv = meta.last_verified if meta else None + lines.append( + f" - last_verified: {fmt_date(lv)} " + f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n" + ) + if meta and meta.verified_for: + lines.append(f" - verified_for: {meta.verified_for}\n") + if meta and meta.tier: + lines.append(f" - tier: {meta.tier}\n") + lines.append(f" - warnings: {', '.join(r.warnings)}\n") + if r.errors: + lines.append(f" - errors: {', '.join(r.errors)}\n") + lines.append("\n") + + err_recs = [r for r in report.records if r.errors] + if err_recs: + lines.append("## Scan errors\n") + for r in err_recs: + lines.append(f"- **{r.path}**: {', '.join(r.errors)}\n") + lines.append("\n") + + lines.append("## Notes\n") + lines.append("- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n") + lines.append( + "- last_git_touched / last_content_updated are computed from git history. " + "last_verified is human-controlled.\n\n" + ) + lines.append( + "- In `check` mode, scan/parsing errors are reported for visibility but do not " + "fail by default unless strict mode is enabled or they trigger an enforced policy rule.\n" + ) + return "".join(lines) + + +def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> tuple[Path, Path]: + out_dir.mkdir(parents=True, exist_ok=True) + json_path = out_dir / f"{OUTPUT_FILENAME}.json" + md_path = out_dir / f"{OUTPUT_FILENAME}.md" + + payload = report.model_dump(mode="json") + + json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + md_path.write_text(to_markdown(report, cfg), encoding="utf-8") + return json_path, md_path + + +# ----------------------------- +# Check enforcement +# ----------------------------- +def enforce(cfg: ToolConfig, records: list[FileRecord]) -> list[str]: + pol = cfg.policy + violations: list[str] = [] + today = _iso_today() + + for r in records: + if r.meta and r.meta.ignore: + continue + if r.kind not in {"ipynb", "md"}: + continue + + has_invalid_metadata = "invalid_metadata" in (r.warnings or []) + + if match_allowlist(r.path, pol.require_metadata): + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") + elif r.meta is None: + violations.append(f"{r.path}: missing metadata") + + if match_allowlist(r.path, pol.require_recent_verification): + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") + else: + lv = r.meta.last_verified if r.meta else None + if lv is None: + violations.append(f"{r.path}: missing last_verified") + else: + days = (today - lv).days + if days > pol.warn_if_verified_older_than_days: + violations.append( + f"{r.path}: last_verified is {days}d old (> {pol.warn_if_verified_older_than_days}d)" + ) + + if r.kind == "ipynb" and match_allowlist(r.path, pol.require_notebook_normalized): + if "notebook_not_normalized" in (r.warnings or []): + violations.append(f"{r.path}: notebook is not normalized (run update/format)") + + return violations + + +# ----------------------------- +# CLI +# ----------------------------- + + +def parse_date_token(token: str) -> date: + token = token.strip().lower() + if token in {"today", "now"}: + return _iso_today() + return date.fromisoformat(token) + + +def collect_scan_issues(records: list[FileRecord], target: Literal["errors", "warnings"]) -> list[str]: + items: list[str] = [] + for r in records: + for e in getattr(r, target, []): + items.append(f"{r.path}: {e}") + return items + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") + parser.add_argument("--config", default=str(DEFAULT_CFG), help="Path to YAML config file") + parser.add_argument( + "--no-step-summary", + action="store_true", + help="Do not write to GITHUB_STEP_SUMMARY", + ) + parser.add_argument("--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs") + + sub = parser.add_subparsers(dest="cmd", required=True) + rep = sub.add_parser("report", help="Generate staleness report (read-only)") + rep.add_argument( + "--targets", + nargs="*", + help=( + "Optional repo-relative targets to limit the operation. " + "Supports exact files, directories, and glob patterns " + "(e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + + chk = sub.add_parser( + "check", + help=( + "Run scans + policy checks (read-only). " + "Fails on enforced policy violations; scan errors are non-fatal by default." + ), + ) + chk.add_argument( + "--targets", + nargs="*", + help=( + "Optional list of relative file paths to scan (limits scan to these files). " + "Supports exact files, directories, and glob patterns (e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + chk.add_argument( + "--strict-mode", + action="store_true", + help="Enable failure on scan/parsing errors (overrides config for this run)", + ) + + up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") + up.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) + up.add_argument( + "--set-content-date-from-git", + action="store_true", + help="Set embedded last_content_updated from computed git content date", + ) + up.add_argument( + "--targets", + nargs="*", + help=( + "Optional list of relative file paths to update. " + "Supports exact files, directories, and glob patterns (e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + up.add_argument("--set-last-verified", default=None, help="YYYY-MM-DD or 'today'") + up.add_argument("--set-verified-for", default=None, help="String like 3.0.0rc13") + up.add_argument( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) + + norm = sub.add_parser( + "normalize", + help="Normalize notebooks deterministically (write mode requires --write)", + ) + norm.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) + norm.add_argument( + "--targets", + nargs="*", + help=( + "Optional list of relative notebook paths to normalize. " + "Supports exact files, directories, and glob patterns " + "(e.g. notebooks/example.ipynb, notebooks/, 'notebooks/**/*.ipynb'). " + "Both '/' and '\\' are accepted." + ), + ) + norm.add_argument( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) + + args = parser.parse_args(list(argv) if argv is not None else None) + + config_path = Path(args.config) + repo_root = find_repo_root(Path.cwd()) + cfg = load_config(config_path) + out_dir = Path(args.out_dir) + + requested_targets = getattr(args, "targets", None) + + if requested_targets: + matched_paths, unmatched_targets = validate_requested_targets(repo_root, cfg, requested_targets) + print_target_match_summary(matched_paths, requested_targets) + + if unmatched_targets: + print("\nUnmatched --targets:") + for raw in unmatched_targets: + print(f"- {raw}") + print( + "\nEach --targets selector must match at least one file in the configured scan set. " + "Use repo-relative paths, directories, or glob patterns " + "(for example: docs/page.md, docs/gui/, 'docs/**/*.md')." + ) + return 2 + + if args.cmd in {"report", "check"}: + records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) + elif args.cmd == "update": + lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None + records = update_files( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + set_content_date_from_git=bool(args.set_content_date_from_git), + set_last_verified=lv, + set_verified_for=args.set_verified_for, + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), + ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") + + else: # normalize + records = normalize_notebooks( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), + ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") + + report = Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo_root), + config_path=str(config_path), + totals=summarize(records), + records=records, + ) + + json_path, md_path = write_outputs(report, cfg, out_dir) + + # Emit GitHub Actions job summary if available + emit_summary = not getattr(args, "no_step_summary", False) + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if emit_summary and step_summary and md_path.exists(): + try: + content = md_path.read_text(encoding="utf-8") + # snippet = "\n".join(content.splitlines()[:220]) + "\n" + snippet = "\n".join(content.splitlines()[:]) + "\n" + Path(step_summary).write_text(snippet, encoding="utf-8") + except Exception: + pass + + scan_errors = collect_scan_issues(records, target="errors") + if scan_errors: + print("\nScan errors detected (non-fatal by default):") + for item in scan_errors[:20]: + print(f"- {item}") + if len(scan_errors) > 20: + print(f"... and {len(scan_errors) - 20} more (see report for full details)") + + if args.cmd == "check": + violations = enforce(cfg, records) + if violations: + print("Policy violations:") + for v in violations: + print(f"- {v}") + return 2 + strict_mode = bool((args.strict_mode) or cfg.policy.fail_on_scan_errors) + if strict_mode and scan_errors: + print("Strict mode enabled: failing due to scan/parsing errors.") + return 1 + + # Non-zero if metadata parsing errors occurred for non-report/check commands + if args.cmd not in {"report", "check"} and any(r.errors for r in records): + return 1 + else: + print("\nReport generated:") + print(f"- JSON: {json_path}") + print(f"- Markdown: {md_path}") + + if any(r.warnings for r in records): + print("Warnings detected; see report for details.") + if any(r.errors for r in records): + print("Scan errors detected; see report for details.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/docs_and_notebooks_report_config.yml b/tools/docs_and_notebooks_report_config.yml new file mode 100644 index 0000000000..60dfcb21f8 --- /dev/null +++ b/tools/docs_and_notebooks_report_config.yml @@ -0,0 +1,26 @@ +version: 1 + +scan: + include: + - "examples/COLAB/**/*.ipynb" + - "examples/JUPYTER/**/*.ipynb" + - "docs/**/*.md" + - "docs/**/*.ipynb" # if notebooks get added to docs (Jupyter Book supports this) + exclude: + - "**/.ipynb_checkpoints/**" + - "**/_build/**" + - "**/build/**" + +policy: + warn_if_content_older_than_days: 365 + warn_if_verified_older_than_days: 365 + missing_last_verified_is_warning: true + + # Ratchet lists for tiered verification requirements. + # Tiers have to be determined, and crucial targets identified. + # Then specific policies can be set for each tier, + # e.g. requiring more recent verification for higher tiers, + # or requiring verification for more recent versions. + require_metadata: [] + require_recent_verification: [] + require_notebook_normalized: [] diff --git a/tools/docs_and_notebooks_tool_README.md b/tools/docs_and_notebooks_tool_README.md new file mode 100644 index 0000000000..fa27c5dc8f --- /dev/null +++ b/tools/docs_and_notebooks_tool_README.md @@ -0,0 +1,190 @@ +# Docs & Notebooks Checks Tool + +This tool scans DeepLabCut documentation pages and notebooks and produces **two independent signals**: + +- **`last_content_updated`**: computed from git history as the last *meaningful content* update **excluding metadata-only commits**. +- **`last_verified`**: a human-controlled date indicating the content was verified to work/be accurate. + +In addition, the tool can optionally track: + +- **`last_metadata_updated`**: when the tool last performed a metadata/normalization write (helps explain “file changed” without implying content changed). +- **`verified_for`**: a human-controlled string indicating what the content was verified against (e.g. `3.0.0rc13`). + +The tool is designed to be: + +- **Safe by default**: CI should run **read-only** modes (`report` / `check`). +- **Deterministic**: stable outputs and normalized notebook formatting when explicitly requested. +- **Future-proof**: versioned Pydantic schemas (`schema_version`). + +--- + +## What gets scanned + +Default include patterns are defined in `tools/docs_and_notebooks_report_config.yml`. +Typical patterns include: + +- `examples/COLAB/**/*.ipynb` +- `examples/JUPYTER/**/*.ipynb` +- `docs/**/*.md` +- `docs/**/*.ipynb` (if notebooks are added under docs) + +You can further restrict the scan via `--targets`. + +--- + +## Metadata storage locations + +### Notebooks (`.ipynb`) + +The tool **only** reads/writes **top-level notebook metadata** under the `deeplabcut` namespace. + +> [!IMPORTANT] +> It never edits notebook cells, outputs, or execution counts. + +Example (excerpt): + +```json +{ + "metadata": { + "deeplabcut": { + "last_content_updated": "2020-01-01", + "last_metadata_updated": "2026-03-05", + "last_verified": "2026-02-20", + "verified_for": "3.0.0rc13", + "ignore": false + } + } +} +``` + +> [!NOTE] +> `tier` is intentionally optional and is not auto-populated. + +### Markdown (`.md`) + +The tool reads/writes YAML frontmatter at the top of the file (if present): + +```yaml +--- +deeplabcut: + last_content_updated: 2020-01-01 + last_metadata_updated: 2026-03-05 + last_verified: 2026-02-20 + verified_for: 3.0.0rc13 + ignore: false +--- +``` + +If a doc page has **no** frontmatter, the tool can still report staleness (read-only), and `update` can add/modify metadata when explicitly requested. + +--- + +## The metadata-commit marker (critical) + +Because metadata updates and notebook normalization can rewrite files, they would normally make git (correctly) report that the file was “updated now”. + +To preserve a meaningful **`last_content_updated`**, **all metadata-only / normalization commits must include the marker**: + +- **Marker**: `META_COMMIT_MARKER` (see `tools/docs_and_notebooks_check.py`) +- **Suggested commit message**: `SUGGESTED_META_COMMIT_MESSAGE` + +When you run `update --write` or `normalize --write`, the tool will: + +- Require `--ack-meta-commit-marker` (guardrail) +- Print a suggested commit message + +> [!WARNING] +> If the marker changes in the future, previous iterations still HAVE to be acknowledged to avoid false positives. + +--- + +## Commands + +### 1) Report (read-only) + +Generate a report (does not modify files): + +```bash +python tools/docs_and_notebooks_check.py report +``` + +Writes (by default): + +- `tmp/docs_nb_checks/docs_nb_checks.json` +- `tmp/docs_nb_checks/docs_nb_checks.md` + +### 2) Check (read-only; may fail) + +Run policy checks. By default, CI will not fail unless allowlists are configured. + +```bash +python tools/docs_and_notebooks_check.py check +``` + +The allowlists live in `tools/docs_and_notebooks_report_config.yml`. +They are currently empty, but can help enforce stricter policies once populated (start empty; "ratchet" later). + +### 3) Update metadata (write mode; explicit intent) + +> [!WARNING] +> `update --write` modifies tracked files. Intended for maintainers (manual), not CI. + +#### 3a) Set `last_content_updated` from git (excluding meta commits) + +```bash +python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git --ack-meta-commit-marker +``` + +#### 3b) Set verification fields (human-controlled) + +```bash +python tools/docs_and_notebooks_check.py update --write --targets docs/page.md examples/JUPYTER/foo.ipynb --set-last-verified today --set-verified-for 3.0.0rc13 --ack-meta-commit-marker +``` + +> Tip: omit `--targets` to operate on all scanned files. + +### 4) Normalize notebooks (explicit churn) + +> [!IMPORTANT] +> Notebook normalization rewrites the notebook JSON into a canonical form. +> As such, it is provided as a separate command. + +Dry-run (shows which files *would* change): + +```bash +python tools/docs_and_notebooks_check.py normalize --targets docs/notebook.ipynb +``` + +Write: + +```bash +python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb --ack-meta-commit-marker +``` + +--- + +## CI integration + +Recommended CI usage: + +- Run `report` on PRs and upload the outputs as artifacts. +- Run `check` once allowlists are populated (start empty to avoid failures). + +> [!IMPORTANT] +> Use `actions/checkout` with `fetch-depth: 0` (or sufficiently deep) so `git log` sees history; shallow clones can cause missing or fallback timestamps. + +Dependencies required for this tool (install in the CI job): + +```bash +pip install pydantic pyyaml nbformat +``` + +--- + +## Troubleshooting + +- If you see `content_date_fallback_to_git_touched`, it usually means one of: + - The checkout history is too shallow, or + - *All* commits touching the file are metadata commits with the marker. + +- If Pydantic raises `class-not-fully-defined` errors, ensure the tool calls `.model_rebuild()` for its models (this is already done in the tool). diff --git a/tools/find_import_cycles.py b/tools/find_import_cycles.py new file mode 100644 index 0000000000..743392dc15 --- /dev/null +++ b/tools/find_import_cycles.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from collections import defaultdict +from pathlib import Path + + +def path_to_module(root: Path, file: Path) -> str: + rel = file.relative_to(root) + parts = rel.with_suffix("").parts + if parts[-1] == "__init__": + parts = parts[:-1] + return ".".join((root.name, *parts)) if parts else root.name + + +def module_to_file_map(root: Path) -> dict[str, Path]: + mapping = {} + for file in root.rglob("*.py"): + mod = path_to_module(root, file) + mapping[mod] = file + return mapping + + +def resolve_relative_import(current_module: str, module: str | None, level: int) -> str | None: + parts = current_module.split(".") + if level > len(parts): + return None + base = parts[:-level] + if module: + return ".".join(base + module.split(".")) + return ".".join(base) + + +def extract_imports(file: Path, current_module: str) -> set[str]: + source = file.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file)) + imports: set[str] = set() + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + imports.add(alias.name) + elif isinstance(node, ast.ImportFrom): + if node.level and current_module: + resolved = resolve_relative_import(current_module, node.module, node.level) + if resolved: + imports.add(resolved) + elif node.module: + imports.add(node.module) + + return imports + + +def internal_edges(root: Path) -> dict[str, set[str]]: + mod_to_file = module_to_file_map(root) + internal = set(mod_to_file) + edges: dict[str, set[str]] = defaultdict(set) + + for mod, file in mod_to_file.items(): + for imported in extract_imports(file, mod): + # Keep only imports that are inside the package + for candidate in internal: + if imported == candidate or imported.startswith(candidate + "."): + edges[mod].add(candidate) + break + + return edges + + +def find_cycles(edges: dict[str, set[str]]) -> list[list[str]]: + visited = set() + stack = [] + on_stack = set() + cycles = [] + + def dfs(node: str): + visited.add(node) + stack.append(node) + on_stack.add(node) + + for neighbor in edges.get(node, ()): + if neighbor not in visited: + dfs(neighbor) + elif neighbor in on_stack: + idx = stack.index(neighbor) + cycle = stack[idx:] + [neighbor] + cycles.append(cycle) + + stack.pop() + on_stack.remove(node) + + for node in edges: + if node not in visited: + dfs(node) + + # Deduplicate roughly + seen = set() + unique = [] + for cyc in cycles: + key = tuple(cyc) + if key not in seen: + seen.add(key) + unique.append(cyc) + return unique + + +def main(): + root = Path("deeplabcut") # change if needed + edges = internal_edges(root) + cycles = find_cycles(edges) + + if not cycles: + print("No cycles found.") + return + + print("Import cycles found:\n") + for cyc in cycles: + print(" -> ".join(cyc)) + + +if __name__ == "__main__": + main() diff --git a/tools/ruff_cleanup_helpers.md b/tools/ruff_cleanup_helpers.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/ruff_report.py b/tools/ruff_report.py new file mode 100644 index 0000000000..bfa7e6f206 --- /dev/null +++ b/tools/ruff_report.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Generate a readable Markdown report from Ruff JSON output. + +Usage: + python ruff_report.py . --output ruff-report.md + python ruff_report.py src tests --output lint/ruff-report.md +""" + +from __future__ import annotations + +import argparse +import collections +import json +import os +import subprocess +import sys +from collections.abc import Iterable +from pathlib import Path + +RULE_NOTES = { + "F401": "Unused import. Usually safe to delete; verify imports with side effects.", + "E501": "Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables.", + "E402": "Module import not at top of file. Move imports above executable code if possible.", + "F403": "`from x import *` makes names unclear. Replace with explicit imports.", + "F405": "Likely consequence of `import *`. Import the name explicitly.", + "F821": "Undefined name. Usually a real bug or missing import.", + "E722": "Bare `except:`. Catch `Exception` or a narrower exception type.", + "B904": "Inside `except`, use `raise ... from e` to preserve exception chaining.", + "B007": "Unused loop variable. Rename to `_` or use it.", + "UP031": "Old `%` formatting. Convert to f-strings or `.format()` where appropriate.", + "E721": "Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`.", + "B008": "Function call in default arg. Use `None` + initialize inside the function.", + "B023": "Function closes over loop variable. Bind it via default arg or helper.", + "B024": "ABC without abstract method. Add `@abstractmethod` or remove ABC intent.", + "F811": "Redefined while unused. Remove duplicate or rename.", + "B012": "Jump statement in `finally` can swallow exceptions. Restructure flow.", + "B016": "Raise an exception instance/class, not a literal.", + "B017": "Use a more specific exception with `assertRaises`.", + "B020": "Loop variable overrides iterator. Rename loop variables.", + "B027": "Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it.", +} + + +def run_ruff(paths: Iterable[str]) -> list[dict]: + cmd = [sys.executable, "-m", "ruff", "check", *paths, "--output-format=json", "--exit-zero"] + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode not in (0, 1): + print(proc.stdout) + print(proc.stderr, file=sys.stderr) + raise SystemExit(f"Failed to run Ruff: {' '.join(cmd)}") + data = json.loads(proc.stdout or "[]") + if not isinstance(data, list): + raise SystemExit("Unexpected Ruff JSON output") + return data + + +def relpath(path: str) -> str: + try: + return os.path.relpath(path) + except Exception: + return path + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*", default=["."], help="Files/directories to scan") + parser.add_argument("--output", default="tmp/ruff-report.md", help="Markdown output path") + args = parser.parse_args() + + issues = run_ruff(args.paths) + + by_rule: dict[str, list[dict]] = collections.defaultdict(list) + for item in issues: + by_rule[item.get("code", "UNKNOWN")].append(item) + + out = Path(args.output) + out.parent.mkdir(parents=True, exist_ok=True) + + lines: list[str] = [] + lines.append("# Ruff manual-fix report\n") + lines.append(f"Generated from: `{', '.join(args.paths)}`\n") + lines.append(f"Total remaining issues: **{len(issues)}**\n") + + lines.append("## Summary\n") + lines.append("| Rule | Count | Note |") + lines.append("|---|---:|---|") + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + note = RULE_NOTES.get(rule, "") + lines.append(f"| `{rule}` | {len(items)} | {note} |") + lines.append("") + + lines.append("## Suggested triage order\n") + preferred = ["F403", "F405", "F821", "E722", "B904", "E402", "F401", "E501"] + present = [r for r in preferred if r in by_rule] + if present: + for idx, rule in enumerate(present, 1): + lines.append(f"{idx}. `{rule}` — {RULE_NOTES.get(rule, '')}") + lines.append("") + + lines.append("## Table of contents by rule\n") + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + anchor = rule.lower() + lines.append(f"- [{rule} ({len(items)})](#{anchor})") + lines.append("") + + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"## {rule}\n") + lines.append(f"Count: **{len(items)}** ") + if rule in RULE_NOTES: + lines.append(f"Hint: {RULE_NOTES[rule]} ") + lines.append("") + + file_groups: dict[str, list[dict]] = collections.defaultdict(list) + for item in items: + file_groups[relpath(item["filename"])].append(item) + + lines.append("### Files affected\n") + lines.append("| File | Count |") + lines.append("|---|---:|") + for filename, entries in sorted(file_groups.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"| `{filename}` | {len(entries)} |") + lines.append("") + + lines.append("### Details\n") + for filename, entries in sorted(file_groups.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"#### `{filename}` ({len(entries)})\n") + lines.append("| Line | Col | Message |") + lines.append("|---:|---:|---|") + for e in sorted( + entries, key=lambda x: (x.get("location", {}).get("row", 0), x.get("location", {}).get("column", 0)) + ): + loc = e.get("location", {}) + line = loc.get("row", "") + col = loc.get("column", "") + msg = (e.get("message", "") or "").replace("|", "\\|") + lines.append(f"| {line} | {col} | {msg} |") + lines.append("") + lines.append("Quick open commands:") + lines.append("") + lines.append("```powershell") + lines.append(f'code -g "{filename}:{entries[0].get("location", {}).get("row", 1)}"') + lines.append("```") + lines.append("") + + out.write_text("\n".join(lines), encoding="utf-8") + print(f"Wrote {out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_selector.py b/tools/test_selector.py new file mode 100755 index 0000000000..7e50c17dec --- /dev/null +++ b/tools/test_selector.py @@ -0,0 +1,950 @@ +#!/usr/bin/env python3 +"""Deterministic, strictly validated test selector for DeepLabCut. + +Outputs orthogonal workflow mode selections plus structured test selections: + + - lanes: which workflow lanes should run (skip, docs, fast, full) + - pytest_paths: JSON list of pytest path arguments + - functional_scripts: JSON list of python script paths + - provenance: JSON mapping each selected test/script to the category rules that selected it + +Safety principles +----------------- +- Fail-safe: if changes cannot be determined or are ambiguous, the "full" lane is always selected. +- Deterministic: derives diff range from GitHub Actions event payload when available. + * pull_request: uses merge-base(base.sha, head.sha) .. head.sha + * push: uses before .. after + * manual override: uses exactly --base-sha .. --head-sha + * fallback: attempts HEAD~1 .. HEAD +- Secure: never emits shell command strings; only structured data. +- Strict: Pydantic schema validation (extra=forbid), SHA validation, path sanitization. + +Intended usage in GitHub Actions +------------------------------- +- Checkout with sufficient history for merge-base/diff (typically fetch-depth: 0). +- Run: + python tools/test_selector.py --write-github-output --json + +This will write the following keys to $GITHUB_OUTPUT: + - run_skip (bool): whether to run the skip mode + - run_docs (bool): whether to run the docs workflow + - run_fast (bool): whether to run targeted test execution + - run_full (bool): whether to run the full matrix/full suite workflow + - selected_workflows (list): list of selected workflow lanes + - lane_reasons (dict): reasons for selecting each workflow lane + - diff_mode (str): how the diff was determined + - pytest_paths (list): list of pytest path arguments + - functional_scripts (list): list of python script paths + - reasons (list): aggregate machine-readable reasons for the selection + - changed_files (list): list of changed files + - provenance (dict): mapping each selected test/script to the category rules that selected it + +Notes +----- +- This script intentionally keeps the routing rules simple and location-based. +- Extend CATEGORY_RULES and FULL_SUITE_TRIGGERS as needed, keeping rules auditable. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +from collections import defaultdict +from collections.abc import Callable, Sequence +from enum import Enum +from pathlib import Path +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +try: + from .test_selector_config import ( + CATEGORY_RULE_BY_NAME, + CATEGORY_RULES, + FULL_SUITE_TRIGGERS, + LINT_ONLY_FILES, + MINIMAL_PYTEST, + ) +# Allows to run as "python tools/test_selector.py" without installing as a package, +# but still import the config from the same location. +except ImportError: # pragma: no cover + from test_selector_config import ( + CATEGORY_RULE_BY_NAME, + CATEGORY_RULES, + FULL_SUITE_TRIGGERS, + LINT_ONLY_FILES, + MINIMAL_PYTEST, + ) + + +SHA_RE = re.compile(r"^[0-9a-f]{7,40}$", re.IGNORECASE) +# DLC_NAMESPACE = "deeplabcut" + + +class DiffMode(str, Enum): + """How the diff was determined, for auditing and reporting.""" + + PR = "pr" # merge-base(base, head) .. head + PUSH = "push" # before .. after + MANUAL = "manual" # explicit base...head from CLI args + FALLBACK = "fallback" # HEAD^ .. HEAD + INITIAL = "initial" # empty tree .. HEAD + FALLBACK_NO_HEAD = "fallback_no_head" # couldn't resolve HEAD + + +MODE_LABELS = { + DiffMode.PR: "Pull request (merge-base..HEAD)", + DiffMode.PUSH: "Push (before..after)", + DiffMode.MANUAL: "Manual override", + DiffMode.FALLBACK: "Fallback (HEAD^..HEAD)", + DiffMode.INITIAL: "Initial commit (empty tree..HEAD)", + DiffMode.FALLBACK_NO_HEAD: "Fallback (couldn't resolve HEAD)", +} + + +class LaneSelection(BaseModel): + """Which workflow lanes should run.""" + + model_config = ConfigDict(extra="forbid") + + skip: bool = False # Skip all tests (e.g. lint-only changes only) + docs: bool = False # Run docs build checks + fast: bool = False # Run targeted pytest + optional functional scripts in test workflow + full: bool = False # Delegate to full test workflow/matrix + + +class SelectionProvenance(BaseModel): + """Why each selected test/script path was included.""" + + model_config = ConfigDict(extra="forbid") + + pytest: dict[str, list[str]] = Field(default_factory=dict) + scripts: dict[str, list[str]] = Field(default_factory=dict) + + +class SelectorResult(BaseModel): + """Strict output schema.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: int = 2 + diff_mode: DiffMode = DiffMode.FALLBACK_NO_HEAD + + lanes: LaneSelection = Field(default_factory=LaneSelection) + + pytest_paths: list[str] = Field(default_factory=list) + functional_scripts: list[str] = Field(default_factory=list) + provenance: SelectionProvenance = Field(default_factory=SelectionProvenance) + + reasons: list[str] = Field(default_factory=list) + changed_files: list[str] = Field(default_factory=list) + lane_reasons: dict[str, list[str]] = Field(default_factory=dict) + + +SelectorResult.model_rebuild() # Ensure model is fully built at import time for validation in main() + + +# ----------------------------- +# Git helpers +# ----------------------------- +def _run_git(args: Sequence[str], cwd: Path) -> str: + proc = subprocess.run( + ["git", *args], + cwd=str(cwd), + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(f"git {' '.join(args)} failed: {proc.stderr.strip()}") + return proc.stdout.strip() + + +def find_repo_root() -> Path: + out = _run_git(["rev-parse", "--show-toplevel"], Path.cwd()) + return Path(out).resolve() + + +def _validate_sha(label: str, sha: str) -> str: + if not sha or not SHA_RE.match(sha): + raise ValueError(f"Invalid {label} SHA: {sha!r}") + return sha + + +def _ensure_commit_exists(sha: str, cwd: Path) -> None: + _run_git(["cat-file", "-e", f"{sha}^{{commit}}"], cwd) + + +def _load_github_event() -> dict[str, Any]: + path = os.environ.get("GITHUB_EVENT_PATH") + if not path: + return {} + try: + return json.loads(Path(path).read_text(encoding="utf-8")) + except Exception: + return {} + + +def _normalize_relpath(p: str) -> str: + """Normalize and validate a repo-relative path from git output.""" + if "\x00" in p: + raise ValueError("NUL byte in path") + p = p.strip().replace("\\", "/") + if not p: + raise ValueError("Empty path") + if p.startswith("/") or re.match(r"^[A-Za-z]:/", p): + raise ValueError(f"Absolute path not allowed: {p}") + parts = [x for x in p.split("/") if x not in ("", ".")] + if any(x == ".." for x in parts): + raise ValueError(f"Path traversal not allowed: {p}") + return "/".join(parts) + + +def _empty_tree(repo: Path) -> str: + # Avoid hardcoding; derive the empty tree hash deterministically. + empty = _run_git(["hash-object", "-t", "tree", os.devnull], repo) + return _validate_sha("empty-tree", empty) + + +def determine_diff_range(repo: Path, override_base: str | None, override_head: str | None) -> tuple[str, str, DiffMode]: + """Return (base_commit, head_commit, mode).""" + zero_sha = "0" * 40 + event_name = os.environ.get("GITHUB_EVENT_NAME", "") + event = _load_github_event() + + if override_base and override_head: + base = _validate_sha("base", override_base) + head = _validate_sha("head", override_head) + _ensure_commit_exists(base, repo) + _ensure_commit_exists(head, repo) + return base, head, DiffMode.MANUAL + + if event_name == "pull_request" and "pull_request" in event: + base_sha = _validate_sha("base", event["pull_request"]["base"]["sha"]) + head_sha = _validate_sha("head", event["pull_request"]["head"]["sha"]) + _ensure_commit_exists(base_sha, repo) + _ensure_commit_exists(head_sha, repo) + # Use merge-base to approximate the PR triple-dot diff base deterministically. + merge_base = _run_git(["merge-base", head_sha, base_sha], repo) + merge_base = _validate_sha("merge-base", merge_base) + _ensure_commit_exists(merge_base, repo) + return merge_base, head_sha, DiffMode.PR + + if event_name == "push" and "before" in event and "after" in event: + before = _validate_sha("before", event["before"]) + after = _validate_sha("after", event["after"]) + _ensure_commit_exists(after, repo) + + if before == zero_sha: + empty = _empty_tree(repo) + return empty, after, DiffMode.INITIAL + try: + _ensure_commit_exists(before, repo) + return before, after, DiffMode.PUSH + except Exception: + empty = _empty_tree(repo) + return empty, after, DiffMode.INITIAL + + # Fallback: try parent..HEAD; if no parent (initial commit), diff empty-tree..HEAD + try: + head = _validate_sha("HEAD", _run_git(["rev-parse", "HEAD"], repo)) + _ensure_commit_exists(head, repo) + + try: + prev = _validate_sha("HEAD^", _run_git(["rev-parse", "--verify", "HEAD^"], repo)) + _ensure_commit_exists(prev, repo) + return prev, head, DiffMode.FALLBACK + except Exception: + # Initial commit (no parent): treat as "everything added" + empty = _empty_tree(repo) + return empty, head, DiffMode.INITIAL + except Exception: + return "", "", DiffMode.FALLBACK_NO_HEAD + + +def changed_files(repo: Path, base: str, head: str) -> list[str]: + if not base or not head: + return [] + out = _run_git(["diff", "--name-only", "--diff-filter=ACMRTD", base, head], repo) + files = [_normalize_relpath(line) for line in out.splitlines() if line.strip()] + return sorted(set(files)) + + +def _is_safe_relpath(p: str) -> bool: + """Safety check for a git-relative path: no absolute, no traversal, no NUL.""" + return ( + p + and "\x00" not in p + and not p.startswith("/") + and not re.match(r"^[A-Za-z]:/", p) + and ".." not in Path(p).parts + ) + + +def validate_selected_paths(res: SelectorResult, repo: Path) -> SelectorResult: + missing: list[str] = [] + + # validate pytest paths (files/dirs) + for p in res.pytest_paths: + if not _is_safe_relpath(p) or not (repo / p).exists(): + missing.append(f"pytest:{p}") + + # validate functional scripts (files) + for s in res.functional_scripts: + if not _is_safe_relpath(s) or not (repo / s).exists(): + missing.append(f"script:{s}") + + if missing: + # Fail-safe escalation: disable fast lane selection, enable full lane. + # Preserve docs lane if it was independently selected. + res.lanes.fast = False + res.lanes.full = True + res.pytest_paths = [] + res.functional_scripts = [] + res.provenance = SelectionProvenance() + res.reasons = res.reasons + ["missing_selected_paths"] + missing + + lane_reasons = dict(res.lane_reasons) + lane_reasons.pop("fast", None) + full_reasons = list(lane_reasons.get("full", [])) + full_reasons.extend(["missing_selected_paths", *missing]) + lane_reasons["full"] = full_reasons + res.lane_reasons = lane_reasons + + return res + + +# ----------------------------- +# Decision logic +# ----------------------------- + + +def _matches_any(path: str, preds: Sequence[Callable[[str], bool]]) -> bool: + for pred in preds: + try: + if pred(path): + return True + except Exception: + continue + return False + + +def decide(files: list[str]) -> SelectorResult: + reasons: list[str] = [] + lane_reasons: dict[str, list[str]] = {} + lanes = LaneSelection() + + if not files: + lanes.full = True + full_reasons = ["no_changed_files_or_diff_unavailable"] + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=full_reasons, + changed_files=[], + lane_reasons={"full": full_reasons}, + ) + + # Lint-only filtering (routing should ignore these files) + lint_only = [f for f in files if f in LINT_ONLY_FILES] + routed_files = [f for f in files if f not in LINT_ONLY_FILES] + + if lint_only: + reasons.append(f"lint_only_count:{len(lint_only)}") + + # If *only* lint-only files changed, skip all lanes + if not routed_files: + lanes.skip = True + skip_reasons = [*reasons, "lint_only"] + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=skip_reasons, + changed_files=files, + lane_reasons={"skip": skip_reasons}, + ) + + # Docs lane is orthogonal: if any routed file matches docs, enable docs lane. + docs_rule = CATEGORY_RULE_BY_NAME.get("docs") + docs_touched = bool(docs_rule and any(_matches_any(f, docs_rule.match_any) for f in routed_files)) + docs_matched_files = {f for f in routed_files if docs_rule and _matches_any(f, docs_rule.match_any)} + non_docs_routed_files = [f for f in routed_files if f not in docs_matched_files] + + docs_pytests_sorted: list[str] = [] + docs_scripts_sorted: list[str] = [] + + if docs_touched: + lanes.docs = True + reasons.append("category:docs") + lane_reasons["docs"] = ["category:docs"] + docs_pytests_sorted = sorted(set(docs_rule.pytest_paths)) if docs_rule else [] + docs_scripts_sorted = sorted(set(docs_rule.functional_scripts)) if docs_rule else [] + + # Full-suite triggers always win over fast, but docs lane can still remain enabled. + triggered: list[tuple[str, str]] = [] + for f in routed_files: + for name, pred in FULL_SUITE_TRIGGERS: + if _matches_any(f, [pred]): + triggered.append((f, name)) + + if triggered: + lanes.full = True + full_reasons = [ + "full_suite_trigger", + f"full_suite_trigger_count:{len(triggered)}", + ] + reasons.extend(full_reasons) + lane_reasons["full"] = full_reasons + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Match NON-doc categories only for test-routing / escalation logic. + matched_non_docs = [] + for rule in CATEGORY_RULES: + if rule.name == "docs": + continue + if any(_matches_any(f, rule.match_any) for f in routed_files): + matched_non_docs.append(rule) + + matched_non_docs = sorted(matched_non_docs, key=lambda r: r.name) + + for rule in matched_non_docs: + reasons.append(f"category:{rule.name}") + + pytest_paths_set: set[str] = set() + functional_set: set[str] = set() + pytest_sources: dict[str, set[str]] = defaultdict(set) + script_sources: dict[str, set[str]] = defaultdict(set) + + # Docs rules may contribute tests/scripts to the fast lane. + if docs_touched: + for p in docs_pytests_sorted: + pytest_paths_set.add(p) + pytest_sources[p].add("docs") + for s in docs_scripts_sorted: + functional_set.add(s) + script_sources[s].add("docs") + + # Non-doc matched categories contribute to fast lane. + for rule in matched_non_docs: + cat = rule.name + for p in rule.pytest_paths or []: + pytest_paths_set.add(p) + pytest_sources[p].add(cat) + for s in rule.functional_scripts or []: + functional_set.add(s) + script_sources[s].add(cat) + + # If we matched non-doc categories but none provided explicit tests/scripts, + # fall back to the minimal pytest lane. + fallback_used = False + if not pytest_paths_set and not functional_set and matched_non_docs: + for p in MINIMAL_PYTEST: + pytest_paths_set.add(p) + pytest_sources[p].add("fallback_minimal_pytest") + reasons.append("fallback_minimal_pytest") + fallback_used = True + + # If the routed changes are truly docs-only (no non-doc files remain) and no + # tests were selected, return docs lane only. + if not pytest_paths_set and not functional_set: + if lanes.docs and not non_docs_routed_files: + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Otherwise fail-safe to full when nothing matched at all. + lanes.full = True + full_reasons = ["no_category_matched"] + reasons.extend(full_reasons) + lane_reasons["full"] = full_reasons + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Fast lane selected + lanes.fast = True + + fast_reasons: list[str] = [] + if docs_touched and (docs_pytests_sorted or docs_scripts_sorted): + fast_reasons.append("category:docs") + fast_reasons.extend(f"category:{rule.name}" for rule in matched_non_docs) + if fallback_used: + fast_reasons.append("fallback_minimal_pytest") + lane_reasons["fast"] = fast_reasons + + return SelectorResult( + lanes=lanes, + pytest_paths=sorted(pytest_paths_set), + functional_scripts=sorted(functional_set), + provenance=SelectionProvenance( + pytest={k: sorted(v) for k, v in sorted(pytest_sources.items())}, + scripts={k: sorted(v) for k, v in sorted(script_sources.items())}, + ), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + +# ----------------------------- +# Outputs +# ----------------------------- +def explain_changed_files(files: list[str]) -> dict[str, Any]: + """ + Build an explanation structure for reporting: + - per-file: full_trigger_matches, category_matches + - grouped: full_triggers, by_category, uncategorized + """ + per_file: dict[str, dict[str, Any]] = {} + by_category: dict[str, list[str]] = defaultdict(list) + full_trigger_files: dict[str, list[str]] = defaultdict(list) + lint_only_files: list[str] = [] + uncategorized: list[str] = [] + + # Prep category predicates + categories = [(r.name, r.match_any) for r in CATEGORY_RULES] + + for f in files: + # Which full-suite triggers does this file match? + ft = [] + for trig_name, pred in FULL_SUITE_TRIGGERS: + try: + if pred(f): + ft.append(trig_name) + except Exception: + continue + + # Which categories does it match? + cats = [] + for cat_name, preds in categories: + if _matches_any(f, preds): + cats.append(cat_name) + is_lint_only = f in LINT_ONLY_FILES + + per_file[f] = { + "full_triggers": ft, + "categories": cats, + "lint_only": is_lint_only, + } + + if ft: + for t in ft: + full_trigger_files[t].append(f) + + if cats: + for c in cats: + by_category[c].append(f) + + if is_lint_only: + lint_only_files.append(f) + else: + # Only uncategorized if it matched no categories AND no full-suite triggers + if not ft and not cats: + uncategorized.append(f) + + # Deterministic ordering + for t in full_trigger_files: + full_trigger_files[t] = sorted(set(full_trigger_files[t])) + for c in by_category: + by_category[c] = sorted(set(by_category[c])) + + return { + "per_file": per_file, + "full_trigger_files": dict(full_trigger_files), + "by_category": dict(by_category), + "lint_only": sorted(set(lint_only_files)), + "uncategorized": sorted(set(uncategorized)), + } + + +def _render_file_line( + f: str, + info: dict[str, Any], + emoji: bool = False, + add_tag: bool = True, + add_marker: bool = False, + category_only: bool = True, +) -> str: + # Optional, single marker only + marker = "" + if add_marker: + if info.get("full_triggers"): + marker = "⚠️ " if emoji else "" + elif info.get("lint_only"): + marker = "🧹 " if emoji else "" + elif not info.get("categories"): + marker = "❓ " if emoji else "" + + tags = [] + if add_tag: + if info.get("categories"): + header = "🏷️ " if emoji else "Category match :" + tags.append(f"{header} " + ", ".join(info["categories"])) + if info.get("full_triggers") and not category_only: + header = "🚨 " if emoji else "Full triggers" + tags.append(f"{header} " + ", ".join(info["full_triggers"])) + if info.get("lint_only") and not category_only: + header = "🧹 " if emoji else "Lint-only :" + tags.append(f"{header}") + + tag_str = (" — " + " | ".join(tags)) if tags else "" + return f"- {marker}`{f}`{tag_str}" + + +def _enabled_lane_names(res: SelectorResult) -> list[str]: + order = ("skip", "docs", "fast", "full") + return [name for name in order if getattr(res.lanes, name)] + + +def _lane_label(name: str, emoji: bool = False) -> str: + if not emoji: + return name + return { + "skip": "⏩ skip", + "docs": "📚 docs", + "fast": "⚡ fast", + "full": "🧪 full", + }.get(name, name) + + +def _compact_reasons(reasons: list[str]) -> list[str]: + cats = sorted({r.split(":", 1)[1] for r in reasons if r.startswith("category:")}) + other = [r for r in reasons if not r.startswith("category:")] + out = [] + if cats: + out.append("categories: " + ", ".join(cats)) + out.extend(other) + return out + + +def _details_open(summary: str, add_blank: bool = True) -> str: + s = f"
{summary}\n" + if add_blank: + s += "\n" + return s + + +def _details_close() -> str: + return "\n
\n" + + +def _render_decision_markdown( + res: SelectorResult, + limit: int = 40, + style: str = "minimal", + emoji: bool = False, +) -> str: + def bullet(items: list[str], limit_: int = limit) -> str: + if not items: + return "_(none)_" + shown = items[:limit_] + s = "\n".join(f"- `{x}`" for x in shown) + if len(items) > limit_: + s += f"\n- … and {len(items) - limit_} more" + return s + + # Selection line (minimal, no emoji by default) + selected_lanes = _enabled_lane_names(res) + if emoji: + selected_lanes_label = ", ".join(_lane_label(name, emoji=True) for name in selected_lanes) + else: + selected_lanes_label = ", ".join(f"`{name}`" for name in selected_lanes) + + if not selected_lanes_label: + selected_lanes_label = "_(none)_" + + diff_mode = f"{MODE_LABELS.get(res.diff_mode, res.diff_mode.value)}" + + md: list[str] = [] + md.append("# Test selection\n") + md.append(f"**Selected workflows:** {selected_lanes_label}\n") + md.append(f"**Diff mode:** `{diff_mode}`\n") + + # Reasons (compacted) + md.append("## Why\n") + for r in _compact_reasons(res.reasons): + md.append(f"- `{r}`") + md.append("") + + if style == "detailed" and res.lane_reasons: + md.append("## Workflow lanes\n") + for lane in _enabled_lane_names(res): + lane_rs = res.lane_reasons.get(lane, []) + md.append(f"### `{lane}`") + if lane_rs: + for r in lane_rs: + md.append(f"- `{r}`") + else: + md.append("_(none)_") + md.append("") + + # Explain changed files + exp = explain_changed_files(res.changed_files) + + md.append("## Changed files (explained)\n") + + # 1) Collapsible: Files that match full-suite triggers + # (Always collapsible if present; otherwise omit section.) + if exp["full_trigger_files"]: + total_triggered = sum(len(v) for v in exp["full_trigger_files"].values()) + md.append(_details_open(f"Files that match full-suite triggers ({total_triggered})")) + for trig_name in sorted(exp["full_trigger_files"].keys()): + files_for_trigger = exp["full_trigger_files"][trig_name] + md.append(f"**{trig_name}** ({len(files_for_trigger)})") + for f in files_for_trigger[:limit]: + md.append(_render_file_line(f, exp["per_file"][f], emoji=emoji)) + if len(files_for_trigger) > limit: + md.append(f"- … and {len(files_for_trigger) - limit} more") + md.append("") + md.append(_details_close()) + + # 2) Files grouped by category (includes uncategorized and lint-only as collapsible lists) + md.append("### Files grouped by category\n") + + if exp["by_category"]: + for cat in sorted(exp["by_category"].keys()): + files = exp["by_category"][cat] + + # Determine if this category has any explicit selection rules attached + rule = CATEGORY_RULE_BY_NAME.get(cat) + has_rules = bool(rule and (rule.pytest_paths or rule.functional_scripts)) + note = "" if has_rules else " — no specific testing rules attached" + + md.append(_details_open(f"{cat} ({len(files)}){note}")) + for f in files[:limit]: + # Already grouped by category; keep lines clean + md.append(f"- `{f}`") + if len(files) > limit: + md.append(f"- … and {len(files) - limit} more") + md.append(_details_close()) + else: + md.append("_(none)_\n") + + # Lint-only as collapsible + if exp.get("lint_only"): + lint_files = exp["lint_only"] + md.append(_details_open(f"Lint-only ({len(lint_files)}) — ignored for test selection")) + md.append("") + for f in lint_files[:limit]: + md.append(f"- `{f}`") + if len(lint_files) > limit: + md.append(f"- … and {len(lint_files) - limit} more") + md.append(_details_close()) + + # Uncategorized as collapsible — and clarify what it means + # IMPORTANT: explain_changed_files() already ensures that files that match ANY category + # never land here. This section is only for truly unmatched files. + if exp["uncategorized"]: + unc_files = exp["uncategorized"] + md.append( + _details_open( + f"Uncategorized ({len(unc_files)}) — no matching category (no specific testing rules attached)" + ) + ) + md.append("") + for f in unc_files[:limit]: + md.append(_render_file_line(f, exp["per_file"][f], emoji=emoji)) + if len(unc_files) > limit: + md.append(f"- … and {len(unc_files) - limit} more") + md.append(_details_close()) + + if style == "detailed": + md.append("## Changed files (raw)\n") + md.append(bullet(res.changed_files)) + md.append("") + + # Selected tests + md.append("## Selected tests\n") + md.append(_details_open("Pytest paths")) + md.append(bullet(res.pytest_paths)) + md.append(_details_close()) + md.append(_details_open("Functional scripts")) + md.append(bullet(res.functional_scripts)) + md.append(_details_close()) + + # Provenance collapsed by default, only if detailed + if style == "detailed": + md.append("## Provenance\n") + md.append(_details_open("Why these tests")) + md.append("") + + if res.provenance.pytest: + md.append("### Pytest\n") + for p, srcs in res.provenance.pytest.items(): + md.append(f"- `{p}` ← {', '.join(f'`{s}`' for s in srcs)}") + else: + md.append("### Pytest\n_(none)_") + + if res.provenance.scripts: + md.append("\n### Scripts\n") + for s, srcs in res.provenance.scripts.items(): + md.append(f"- `{s}` ← {', '.join(f'`{x}`' for x in srcs)}") + else: + md.append("\n### Scripts\n_(none)_") + + md.append(_details_close()) + + return "\n".join(md) + + +def write_report_files( + res: SelectorResult, + out_dir: Path, + report_style: str = "minimal", + no_emoji: bool = False, +) -> tuple[Path, Path]: + out_dir.mkdir(parents=True, exist_ok=True) + json_path = out_dir / "selection.json" + md_path = out_dir / "decision.md" + + json_path.write_text(res.model_dump_json(indent=2), encoding="utf-8") + md_path.write_text( + _render_decision_markdown(res, style=report_style, emoji=not no_emoji), + encoding="utf-8", + ) + return json_path, md_path + + +def create_job_summary(md_path: Path, overwrite: bool = True) -> None: + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + # Append markdown to the GitHub Actions Job Summary + mode = "w" if overwrite else "a" + with open(summary_path, mode, encoding="utf-8") as f: + f.write(md_path.read_text(encoding="utf-8")) + f.write("\n") + + +def write_github_output(res: SelectorResult) -> None: + out_path = os.environ.get("GITHUB_OUTPUT") + if not out_path: + raise RuntimeError("GITHUB_OUTPUT is not set") + + def j(v) -> str: + return json.dumps(v, separators=(",", ":"), ensure_ascii=False) + + selected_workflows = _enabled_lane_names(res) + + with open(out_path, "a", encoding="utf-8") as f: + f.write(f"run_skip={str(res.lanes.skip).lower()}\n") + f.write(f"run_docs={str(res.lanes.docs).lower()}\n") + f.write(f"run_fast={str(res.lanes.fast).lower()}\n") + f.write(f"run_full={str(res.lanes.full).lower()}\n") + f.write(f"selected_workflows={j(selected_workflows)}\n") + f.write(f"lane_reasons={j(res.lane_reasons)}\n") + f.write(f"diff_mode={res.diff_mode.value}\n") + f.write(f"pytest_paths={j(res.pytest_paths)}\n") + f.write(f"functional_scripts={j(res.functional_scripts)}\n") + f.write(f"reasons={j(res.reasons)}\n") + f.write(f"changed_files={j(res.changed_files)}\n") + f.write(f"provenance={j(res.provenance.model_dump())}\n") + + +def main(argv: Sequence[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Deterministic DeepLabCut test selector") + ap.add_argument("--json", action="store_true", help="Print JSON result to stdout") + ap.add_argument( + "--write-github-output", + action="store_true", + help="Write outputs to $GITHUB_OUTPUT", + ) + ap.add_argument( + "--base-sha", + default=None, + help="Override base commit SHA for manual diff selection (must be used with --head-sha)", + ) + ap.add_argument( + "--head-sha", + default=None, + help="Override head commit SHA for manual diff selection (must be used with --base-sha)", + ) + + ap.add_argument( + "--report-dir", + default="tmp/test-selection", + help="Directory to write decision report files (selection.json, decision.md)", + ) + ap.add_argument( + "--write-summary", + action="store_true", + help="Append decision.md to GitHub Actions Job Summary if available", + ) + ap.add_argument( + "--report-style", + choices=["minimal", "detailed"], + default="detailed", + help="Decision markdown verbosity: minimal or detailed (default: detailed)", + ) + ap.add_argument( + "--no-emoji", + action="store_true", + help="Disable emojis in markdown report (default: off)", + ) + + args = ap.parse_args(list(argv) if argv is not None else None) + if bool(args.base_sha) != bool(args.head_sha): + ap.error("Both --base-sha and --head-sha must be provided together") + + repo = find_repo_root() + + base, head, diff_mode = determine_diff_range(repo, args.base_sha, args.head_sha) + files = changed_files(repo, base, head) + + res = decide(files) + res.diff_mode = diff_mode + res.changed_files = files + res = validate_selected_paths(res, repo) + + # Strict validation + try: + res = SelectorResult.model_validate(res.model_dump()) + except ValidationError as e: + raise RuntimeError(f"Output validation failed: {e}") from e + + # Always write report files for transparency + report_dir = Path(args.report_dir) + json_path, md_path = write_report_files(res, report_dir, report_style=args.report_style, no_emoji=args.no_emoji) + + if args.json: + print(res.model_dump_json(indent=2)) + + if args.write_github_output: + write_github_output(res) + + # Write Job Summary (GitHub renders markdown from $GITHUB_STEP_SUMMARY) + if args.write_summary: + create_job_summary(md_path) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_selector_config.py b/tools/test_selector_config.py new file mode 100644 index 0000000000..20b5b2c1c8 --- /dev/null +++ b/tools/test_selector_config.py @@ -0,0 +1,341 @@ +"""Test selector configuration.""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from pathlib import PurePosixPath + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +PathPred = Callable[[str], bool] + + +def prefix(*values: str) -> PathPred: + """Match if path starts with any of the given prefixes.""" + prefixes = tuple(values) + return lambda p: p.startswith(prefixes) + + +def suffix(*values: str) -> PathPred: + """Match if path ends with any of the given suffixes.""" + suffixes = tuple(values) + return lambda p: p.endswith(suffixes) + + +def equals(*values: str) -> PathPred: + """Match if path equals any of the given exact values.""" + allowed = frozenset(values) + return lambda p: p in allowed + + +def case_insensitive_match(*values: str) -> PathPred: + """Case-insensitive substring match against any of the given values.""" + needles = tuple(v.lower() for v in values) + return lambda p: any(n in p.lower() for n in needles) + + +def all_of(*preds: PathPred) -> PathPred: + """Logical AND over predicates.""" + return lambda p: all(pred(p) for pred in preds) + + +# ----------------------------- +# Rules validation models +# ----------------------------- +_RULE_NAME_RE = re.compile(r"^[a-z0-9_]+$") + + +def _validate_relpath_string(value: str, field_name: str) -> str: + """Validate a repo-relative path string used in config.""" + if not isinstance(value, str): + raise TypeError(f"{field_name} entries must be strings") + + value = value.strip() + if not value: + raise ValueError(f"{field_name} entries must not be empty") + + value = value.replace("\\", "/") + + if value.startswith("/"): + raise ValueError(f"{field_name} must be repo-relative, got absolute path: {value!r}") + + if re.match(r"^[A-Za-z]:/", value): + raise ValueError(f"{field_name} must not be a Windows absolute path: {value!r}") + + parts = PurePosixPath(value).parts + if ".." in parts: + raise ValueError(f"{field_name} must not contain path traversal: {value!r}") + + if "\x00" in value: + raise ValueError(f"{field_name} must not contain NUL bytes: {value!r}") + + return value + + +class CategoryRule(BaseModel): + """Validated test-selection category rule.""" + + model_config = ConfigDict(extra="forbid") + + name: str + match_any: list[PathPred] = Field( + min_length=1, + description=("List of predicates; if any predicate matches any changed file, the rule is triggered."), + ) + pytest_paths: list[str] = Field( + default_factory=list, + description="Pytest paths selected if the rule is triggered.", + ) + functional_scripts: list[str] = Field( + default_factory=list, + description="Functional test scripts selected if the rule is triggered.", + ) + + @field_validator("name") + @classmethod + def validate_name(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("Rule name must not be empty") + if not _RULE_NAME_RE.match(value): + raise ValueError(f"Rule name must match ^[a-z0-9_]+$ (got {value!r})") + return value + + @field_validator("match_any") + @classmethod + def validate_match_any(cls, preds: list[PathPred]) -> list[PathPred]: + if not preds: + raise ValueError("match_any must contain at least one predicate") + for i, pred in enumerate(preds): + if not callable(pred): + raise TypeError(f"match_any[{i}] must be callable, got {type(pred).__name__}") + return preds + + @field_validator("pytest_paths") + @classmethod + def validate_pytest_paths(cls, values: list[str]) -> list[str]: + return [_validate_relpath_string(v, "pytest_paths") for v in values] + + @field_validator("functional_scripts") + @classmethod + def validate_functional_scripts(cls, values: list[str]) -> list[str]: + return [_validate_relpath_string(v, "functional_scripts") for v in values] + + +def validate_category_rules(rules: list[CategoryRule]) -> list[CategoryRule]: + """Validate cross-rule invariants.""" + seen: dict[str, int] = {} + + for idx, rule in enumerate(rules): + if rule.name in seen: + first_idx = seen[rule.name] + raise ValueError(f"Duplicate CategoryRule name {rule.name!r} at indexes {first_idx} and {idx}") + seen[rule.name] = idx + + return rules + + +# ----------------------------- +# Configuration +# ----------------------------- +MINIMAL_PYTEST = ["tests/test_auxiliaryfunctions.py"] + +POSE_TF = "deeplabcut/pose_estimation_tensorflow/" +POSE_PT = "deeplabcut/pose_estimation_pytorch/" + + +# Conservative full-suite triggers: if any changed file matches, plan=FULL. +FULL_SUITE_TRIGGERS = [ + ("Tests files changed", prefix("tests/")), + ("pyproject.toml changed", equals("pyproject.toml")), + ("lockfile changed", suffix(".lock")), + ("DEEPLABCUT.yaml changed", suffix("DEEPLABCUT.yaml")), + ("CI workflow changed", prefix(".github/workflows/")), +] + + +# Files that should be enforced by dedicated lint workflows, not by test selection +LINT_ONLY_FILES = { + ".pre-commit-config.yaml", + # ".pre-commit-hooks.yaml", +} + + +# The per-file/folder rules that determine test selection logic. Each rule has: +# - a name (for auditing/debugging purposes) +# - a set of path predicates (match_any) that trigger the rule if any predicate +# matches any changed file +# - a list of pytest paths to select if the rule is triggered (can be empty) +# - a list of functional test scripts to select if the rule is triggered (can be empty) +CATEGORY_RULES = validate_category_rules( + [ + # DOCS & NOTEBOOKS # + CategoryRule( + name="docs", + match_any=[ + prefix("docs/"), + all_of(suffix(".md", ".rst"), case_insensitive_match("docs")), + all_of(suffix(".ipynb"), case_insensitive_match("docs")), + equals("_config.yml", "_toc.yml"), + equals(".github/workflows/build-book.yml"), + ], + pytest_paths=[ + # NOTE: + # Optional docs-targeted tests may be attached here. + # If present, docs changes will still enable the docs lane, and may also + # contribute selections added here to the fast lane. + ], + functional_scripts=[ + # NOTE: + # Optional docs-targeted functional tests may be attached here. + # If present, docs changes will still enable the docs lane, and may also + # contribute selections added here to the fast lane. + ], + ), + CategoryRule( + name="notebooks_examples", + match_any=[ + prefix("examples/JUPYTER/", "examples/COLAB/"), + all_of(suffix(".ipynb"), case_insensitive_match("examples")), + ], + pytest_paths=MINIMAL_PYTEST, + functional_scripts=[], + ), + # CORE FUNCTIONALITY # + CategoryRule( + name="superanimal_modelzoo", + match_any=[ + prefix("deeplabcut/modelzoo/"), + case_insensitive_match("superanimal"), + # case_insensitive_match("modelzoo"), # too broad ? + ], + pytest_paths=[ + "tests/test_predict_supermodel.py", + "tests/pose_estimation_pytorch/modelzoo/", + "tests/pose_estimation_pytorch/other/test_modelzoo.py", # (currently all tests are skipped in this file..) # noqa: E501 + ], + functional_scripts=[ + # TODO: decide which of these functional testscripts are useful and not too heavy # noqa: E501 + "examples/testscript_superanimal_adaptation.py", # (runs inference + video adaptation training on shortened video) # noqa: E501 + # "examples/testscript_superanimal_create_pretrained_project.py", # (runs inference on example videos) # noqa: E501 + # "examples/testscript_superanimal_inference.py", # (runs inference on multiple videos with multiple models) # noqa: E501 + # "examples/testscript_superanimal_transfer_learning.py", # (runs full standard training pipeline after weight init) # noqa: E501 + ], + ), + CategoryRule( + name="multianimal", + match_any=[ + case_insensitive_match("multianimal"), + all_of(prefix(POSE_TF), case_insensitive_match("multi")), + all_of(prefix(POSE_PT), case_insensitive_match("multi")), + ], + pytest_paths=[ + "tests/test_auxfun_multianimal.py", + "tests/test_pose_multianimal_imgaug.py", + "tests/test_predict_multianimal.py", + "tests/test_stitcher.py", + "tests/test_trackingutils.py", + ], + functional_scripts=[ + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="core", + match_any=[ + prefix( + "deeplabcut/core/", + "deeplabcut/utils/", + POSE_TF, + POSE_PT, + ), + equals("deeplabcut/auxiliaryfunctions.py"), + ], + pytest_paths=[ + "tests/test_auxiliaryfunctions.py", + "tests/core/", + "tests/utils/", + ], + functional_scripts=[ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="pose_estimation_tensorflow", + match_any=[ + prefix(POSE_TF), + ], + pytest_paths=[ + "tests/test_dataset_augmentation.py", + "tests/test_pose_multianimal_imgaug.py", + "tests/test_predict_multianimal.py", + "tests/test_evaluate.py", + # "tests/test_inferenceutils.py", + # "tests/test_crossvalutils.py", + ], + functional_scripts=[ + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_tensorflow_single_animal.py", + ], + ), + CategoryRule( + name="pose_estimation_pytorch", + match_any=[ + prefix(POSE_PT), + ], + pytest_paths=[ + "tests/pose_estimation_pytorch/", + ], + functional_scripts=[ + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="3d_pose_estimation", + match_any=[ + prefix("deeplabcut/pose_estimation_3d/"), + ], + pytest_paths=[ + "tests/test_triangulation.py", + ], + functional_scripts=[ + "examples/testscript_3d.py", + ], + ), + CategoryRule( + name="generate_training_dataset", + match_any=[ + prefix("deeplabcut/generate_training_dataset/"), + ], + pytest_paths=[ + "tests/generate_training_dataset/", + ], + functional_scripts=[], + ), + # CI & TOOLING # + # CategoryRule( + # name="ci_workflows", + # match_any=[ + # prefix(".github/workflows/"), + # ], + # pytest_paths=MINIMAL_PYTEST, + # functional_scripts=[], + # ), + CategoryRule( + name="ci_tools", + match_any=[ + prefix("tools/"), + ], + pytest_paths=["tests/tools/"], + functional_scripts=[], + ), + ] +) + +CATEGORY_RULE_BY_NAME = {r.name: r for r in CATEGORY_RULES} diff --git a/tools/trim_lines.py b/tools/trim_lines.py new file mode 100644 index 0000000000..f962b609e3 --- /dev/null +++ b/tools/trim_lines.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Reduce Ruff E501 violations using autopep8, then normalize with Ruff. + +Usage: + python fix_e501_with_autopep8.py . --line-length 88 + python fix_e501_with_autopep8.py src tests --line-length 100 --check + +NOTE: if this creates broken escaped f-strings : +f"some string with a { + var +}" +Use the ^[ \t]*\}"[ \t]*$ regex to find and fix them. + +Requirements: + - ruff + - autopep8 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys + + +def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: + print("+", " ".join(cmd)) + proc = subprocess.run(cmd, text=True, capture_output=True) + if proc.stdout: + print(proc.stdout) + if proc.stderr: + print(proc.stderr, file=sys.stderr) + if check and proc.returncode != 0: + raise SystemExit(proc.returncode) + return proc + + +def ruff_json(paths: list[str]) -> list[dict]: + proc = run(["ruff", "check", *paths, "--output-format=json", "--exit-zero"], check=False) + try: + data = json.loads(proc.stdout or "[]") + except json.JSONDecodeError as e: + raise SystemExit(f"Could not parse Ruff JSON: {e}") from e + if not isinstance(data, list): + raise SystemExit("Unexpected Ruff JSON output") + return data + + +def unique_e501_files(paths: list[str]) -> list[str]: + data = ruff_json(paths) + files = sorted({item["filename"] for item in data if item.get("code") == "E501"}) + return files + + +def chunked(items: list[str], size: int = 50): + for i in range(0, len(items), size): + yield items[i : i + size] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*", default=["."], help="Files/directories to scan") + parser.add_argument("--line-length", type=int, default=88) + parser.add_argument("--check", action="store_true", help="Dry run; only show affected files") + args = parser.parse_args() + + files = unique_e501_files(args.paths) + if not files: + print("No E501 files found. Nothing to do.") + return 0 + + print(f"Found {len(files)} file(s) with E501.") + for f in files: + print(" -", f) + + if args.check: + return 0 + + for batch in chunked(files, 50): + run( + [ + "autopep8", + "--in-place", + "--aggressive", + f"--max-line-length={args.line_length}", + "--select=E501,W291,W292,W391", + *batch, + ] + ) + + run(["ruff", "check", *batch, "--fix", "--unsafe-fixes"], check=False) + run(["ruff", "format", *batch], check=False) + + after = len(unique_e501_files(args.paths)) + print(f"Remaining files with E501: {after}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/update_license_headers.py b/tools/update_license_headers.py index 4a05350e47..ca5381deb2 100644 --- a/tools/update_license_headers.py +++ b/tools/update_license_headers.py @@ -1,24 +1,25 @@ """Apply copyright headers to all code files in the repository. -This file can be called as a python script without arguments. For -configuration, see the instructions in NOTICE.yml. +This file can be called as a python script without arguments. For configuration, see the +instructions in NOTICE.yml. """ -import tempfile -import glob -import yaml import fnmatch +import glob import subprocess +import tempfile + +import yaml def load_config(filename): - with open(filename, "r") as fh: + with open(filename) as fh: config = yaml.safe_load(fh) return config def walk_directory(entry): - """Talk the directory""" + """Talk the directory.""" if "header" not in entry: raise ValueError("Current entry does not have a header.") @@ -28,8 +29,7 @@ def walk_directory(entry): def _list_include(): """List all files specified in the include list.""" for include_pattern in entry["include"]: - for filename in glob.iglob(include_pattern, recursive=True): - yield filename + yield from glob.iglob(include_pattern, recursive=True) def _filter_exclude(iterable): """Filter filenames from an iterator by the exclude patterns.""" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..b3d096b3f4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8412 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +conflicts = [[ + { package = "deeplabcut", extra = "apple-mchips" }, + { package = "deeplabcut", extra = "tf" }, + { package = "deeplabcut", extra = "tf-cu11" }, + { package = "deeplabcut", extra = "tf-cu12" }, + { package = "deeplabcut", extra = "tf-latest" }, +], [ + { package = "deeplabcut", extra = "fmpose3d" }, + { package = "deeplabcut", extra = "tf-cu11" }, + { package = "deeplabcut", extra = "tf-cu12" }, +]] + +[manifest] + +[[manifest.dependency-metadata]] +name = "openvino-dev" +version = "2022.1.0" + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "albumentations" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pyyaml" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/4bb46e1afe56a4908bfe6fe58a9231ed5d7e7f46e400559d8bce7fc7dedc/albumentations-1.4.3.tar.gz", hash = "sha256:d0a5ca4edb1695693faa39cdf427d53fdbd7d26f7f8e4100c307e5ff5a463d16", size = 171598, upload-time = "2024-04-03T01:51:05.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/01/4202bd81ab337dca5693d7d1cb25c8e9041d97762aee738a24382ff9af2f/albumentations-1.4.3-py3-none-any.whl", hash = "sha256:386a7a61b6978f90163bf678745674f1d9031fd39cf17a40ae01d88aade72608", size = 137007, upload-time = "2024-04-03T01:51:02.83Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[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.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "app-model" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "in-n-out" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pydantic-compat" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/23/a9537fd7021e8b6ea7db96b73e66fe61e809a3b23338de07d1176e69e4ee/app_model-0.4.0.tar.gz", hash = "sha256:ccf667999f6c659e921ca3490b6da176971e67cf2f41abc34e33caa8cfa18573", size = 120529, upload-time = "2025-06-20T17:41:08.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/fa/d7f4ae02a3b115abdf561d05ba8d5cdf9e3b2b5724b45d91e6cf08c163c3/app_model-0.4.0-py3-none-any.whl", hash = "sha256:5b1d69ee023d955b0c7a525771fd2fc02ff9d82cd2f12a982949423c3a901210", size = 65710, upload-time = "2025-06-20T17:41:07.696Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[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 = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "blosc2" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "ndindex" }, + { name = "numexpr", marker = "platform_machine != 'wasm32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "threadpoolctl", marker = "platform_machine != 'wasm32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/fb/c4d83eb40c8b9cb024507dbc4a3a91bfbc7e36ad5f2528c306575a87f42b/blosc2-4.3.1.tar.gz", hash = "sha256:f0c26b1190b24aae8b58ade95abd7c9aca5e7447db0f13c1915fcbe56a0b8167", size = 5354390, upload-time = "2026-05-19T17:34:45.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/a9/d952e141cea441a4387bf9c2e8917764569092e148eba2b3209408a0b655/blosc2-4.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c9bcd07ec550960513c6f9c2b901251c15d9bbfe79a554bbb9363162a92f5db", size = 5817410, upload-time = "2026-05-19T17:33:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/92/ab/fb94743b4511fb27accb4f9e3d5769cfd039d34801a7787b274cb6455c1c/blosc2-4.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b38dff8a6d3446f08c7b25c8a00d63da32b63a35c05561d6be77bb67cf352de", size = 5026379, upload-time = "2026-05-19T17:33:56.8Z" }, + { url = "https://files.pythonhosted.org/packages/81/df/a1b9bcb00d028f69fc58e45702d99d86a4eddf78640cd4ad87b2ab7cfa3d/blosc2-4.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:50ec0dec76b75411faaa7e79d2fb5adbfdd90b8b90c0637faa706d03336c87d9", size = 6306638, upload-time = "2026-05-19T17:33:59.029Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/b2515f08969c0ed5dd55337ca11dcf9034e2b23250b16badbe6538b27021/blosc2-4.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a52b29306ff7e5b49f8365a7f9567c8bda9a1d3b2e042a4ca185268a379e05d", size = 6589405, upload-time = "2026-05-19T17:34:01.047Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/1874f6b0ffec99ba908e99ec547e4d45f3740b5c03f8ab1f310a3e36e22c/blosc2-4.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:320c9d6ba943aab3570f15d413c4c8b634bbd9d70c00199bf2cf74fe01112f71", size = 4126788, upload-time = "2026-05-19T17:34:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/1d/29/f685930f492ec787557635b5e0e91b229c793cd3fd3d32083e5f088a15b9/blosc2-4.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a1d8d56a7bbd942dc9f46b2c78178b16075ff5e88d7fb466c96e6fd0ff7fcd6", size = 5811703, upload-time = "2026-05-19T17:34:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/3a/24/fc29520a463b96acf64d3fdf5c8bd6fd5d2a11dbda6d08af8fb69de8611b/blosc2-4.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93a02d9f11e4308385f4f556a14573e24310d49042f6d9ea61d74b9c42880980", size = 5021613, upload-time = "2026-05-19T17:34:05.941Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/d80c3e198e056eec0aed87065a68cead2e8c17cffbf8d6aad498d37946cc/blosc2-4.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6759c659467dcf28c7e5d72bf90b7e5559d5e17533364461ab69ab85cce3d4f2", size = 6295416, upload-time = "2026-05-19T17:34:07.722Z" }, + { url = "https://files.pythonhosted.org/packages/17/ba/8a5d0dcfb95126b6601e4f993ee276c9953be4748bd67f45202fee4ee58e/blosc2-4.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:706c27f5b93df7e320b1cdbc446f248d027106f968f762ecb240fde98bbe8af1", size = 6589973, upload-time = "2026-05-19T17:34:09.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/4b/a6fe2d0a496cf467c205cf83993c605dd38422a86adbe710fa2f8f5bcdf5/blosc2-4.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:95c8466beebef66c24ef2dd1993cda3c1f8c3c00ec3be0dcdc098dc5eb6b259c", size = 4124211, upload-time = "2026-05-19T17:34:11.07Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c7db23a891a5eb1893c1b3da206810db48d44cc0180e1b737dab1d5f5733/blosc2-4.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d5aa1e54412f45da894b022fa33387361332884a6689af4121dad1d91c205fa", size = 5857286, upload-time = "2026-05-19T17:34:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/04a147753b4e7fcf227f7a4c6480e3ee545909529d038bbd8a2e0111bd4a/blosc2-4.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf349b41af997c775a94e02af45a4215e8ff3d8ff5e2c8e29b452d697e2c3a90", size = 5020423, upload-time = "2026-05-19T17:34:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/44/22/8f53101763841e907456e92ae06e5318f87228e6fb4e7aa28134c3abee2f/blosc2-4.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a162b378741233170fee4fee213f33b03799d4be42603a3f8344f5ed0df9f32a", size = 6240864, upload-time = "2026-05-19T17:34:16.522Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/1c8818d8afd691dfbd847fb478bb606042114fdbdb321a8a12d0e707dd01/blosc2-4.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa6f6ea348ada734bf6d5c5b6988d47b975ad42fb160952ed92c6b0e8801e86", size = 6539023, upload-time = "2026-05-19T17:34:17.868Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/0ecbef3077298fe4f9daf9c6c8ece80a0f98d1c6efc56e71cc45ad158c75/blosc2-4.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:2cedea2ac373b64b00aa956a04f74d885bdef033878e8cf84e53c001bbef6dc8", size = 4121473, upload-time = "2026-05-19T17:34:19.57Z" }, + { url = "https://files.pythonhosted.org/packages/01/a9/b363f846afb1fec3b49acc4a248aa98bc31c55370effc59c455caa5398ad/blosc2-4.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fae6ed1deb9e79bd703d959e9c4f5a12d6cbfdb2b3713f646ad0f7266c96c114", size = 5855462, upload-time = "2026-05-19T17:34:21.179Z" }, + { url = "https://files.pythonhosted.org/packages/60/6f/48ad9373d2d7fb82321f177a4525cccece70a996242c0a2205160aa9c87d/blosc2-4.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87c0f387b8d47d4e6e1f27c72d92366ef16ceb1b80629ed5eaab241822c386af", size = 5018781, upload-time = "2026-05-19T17:34:23.04Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2f/5777d61be67c001d7674e00302350ba7a288cfea706c5181d440b0d2e8d5/blosc2-4.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c48881de2fe195084a1c583acb4ab66ee8b2f0b8f83fba00148a9eb06f4826fe", size = 6248194, upload-time = "2026-05-19T17:34:24.748Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d6/9ecbfbb73daf8b95149e48dbca9f84d6303e6fac77e3e635c09f281f6f79/blosc2-4.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce881214846959a375279ac2f7ab66e2bfa868bfbd01188ae1329c243f6d1275", size = 6541927, upload-time = "2026-05-19T17:34:26.219Z" }, + { url = "https://files.pythonhosted.org/packages/be/a7/40ead062061388ae6ac4981fa93127a7ed2da2ca66b2a9c09b191f00b7b1/blosc2-4.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:091202d20394e5593a365950cc43b09c49ececdcac36d3b491ddecc3600f36c2", size = 4121405, upload-time = "2026-05-19T17:34:27.615Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/a9b75f9a228e9ac3a202d23d8d8fa23d737064919ac2a3c3866cfaa80b5d/blosc2-4.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0c50a9c64cad0228e382a09e605c4fd6cd9aa2576b76ab16005121a4a57f9698", size = 5857264, upload-time = "2026-05-19T17:34:29.317Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/3561e51ed30c26b6272675b4528457b44c840af11b5b0741fbd9d9d62287/blosc2-4.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1eb44f6ff517c6e3476a3eb3a85b357e4604bd3274db354bf52f854fb50bf", size = 5021316, upload-time = "2026-05-19T17:34:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/12/6c/deff0f7314be3af1cf2fc728bae0be7414084b1ff51cb9a61c5a54997742/blosc2-4.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a31ae1e372f2f53215bc8bcecfe4aa5af20a8cfe4124dccc8dc334093539b2c9", size = 6260060, upload-time = "2026-05-19T17:34:32.648Z" }, + { url = "https://files.pythonhosted.org/packages/44/56/f941a447f47e3c997acb3ff1459c8a26e08f383ec60adf80e3ce04de6be3/blosc2-4.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565e4d038089c495e06b2e3e5a2881d61619460c8448c0291dbd84840437c28d", size = 6543150, upload-time = "2026-05-19T17:34:34.694Z" }, + { url = "https://files.pythonhosted.org/packages/77/15/8a91f03a84854615dfa0b2b7afd9ee29b0b3bde5e28ebe38f5f49de438e1/blosc2-4.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d76a639d7287bb3fec73dd1ae8196923d95e00b95069bce3c495eaa00acdfe8", size = 4204741, upload-time = "2026-05-19T17:34:36.095Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/a246dd2f7ef538a572f61bfeab53d6ae0bcdf3e8cc5187c284189f7f27dc/blosc2-4.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:365235f674f3b70ae2eae3e2e7f0cdbda7b5e02fa50b357ab211dd45b91ad386", size = 5895602, upload-time = "2026-05-19T17:34:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d0/40a9ac3634b520a9a9ffe30569b7b1296fd5e337c858883f379a6844f4c8/blosc2-4.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbd29c7f7d2fdaf164f81f68afa2580941b9832ace77f61c01c39788a48b67f7", size = 5067186, upload-time = "2026-05-19T17:34:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/91/6a/4607099251c65db83040686dfb6f1711278b536725552611365967d978e5/blosc2-4.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c558251376af56dea742ab60233faa1c52381a5f1b1a3db20dce192d3e8e7fd8", size = 6222994, upload-time = "2026-05-19T17:34:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bd/a9f95816a1a680ddd0f18f2d422df98ee4033f59db71960976e66abda361/blosc2-4.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cde49a4cf3b207d36abfeb963eccac998e34b930603baff068cd3593b2d9631f", size = 6512973, upload-time = "2026-05-19T17:34:42.135Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0e/b1669555dbe381344e97056c9c5c4c9bbf2111026d2f2aa660059bbc541f/blosc2-4.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3424327e0ae91e98b9d9c3a32cc27e76d82dc0ec193e81e5b6c770bce5767e7c", size = 4264125, upload-time = "2026-05-19T17:34:43.931Z" }, +] + +[[package]] +name = "bottleneck" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/38/144fb32c9efb196f651ddb30e7c48f6047a86972e5b350f3f10c9a5f6a16/bottleneck-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40de6be68218ba32cd15addbf4ad7bbbf0075b5c5c4347c579aeae110a5c9a96", size = 100393, upload-time = "2025-09-08T16:29:35.466Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e3/dbbf4b102f4e6aaf49ad3749a6d778f309473a2950c5ce3bb20b94f2ba84/bottleneck-1.6.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ad1882ba8c8da1f404de2610b45b05291e39eec56150270b03b5b25cf2bbb7f", size = 371509, upload-time = "2025-09-08T16:29:37.037Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/60fcbddee5fdf32923ba33ce2337a4cf12834b69de4f8e07219b5ef7c931/bottleneck-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f29b14b0ba5a816df6ab559add415c88ea8cf2146364e55f5f4c24ff7c85e494", size = 363480, upload-time = "2025-09-08T16:29:38.311Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/a25434dcadf083e05b0c71ece2de71fad5521268f905721e06e0a7efc5db/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17c227ed361cf9a2ab3751a727620298faca9a1e33dd76711ae80834cf34b254", size = 357120, upload-time = "2025-09-08T16:29:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b9/99580349c827695dfc094ac672eedba6e1ca244b6e745ff7447c0239d6d8/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d278b5633cea38bdae6eaf7df23d54ecb5e4db52f2ebc13fe40c0e738842f2a1", size = 367579, upload-time = "2025-09-08T16:29:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/6326994249388ceb2400d07c6a96a50941749d2d9ec80da22a99046e3a38/bottleneck-1.6.0-cp310-cp310-win32.whl", hash = "sha256:26c87c2f6364d82b67eab7218f0346e9c42f336088ca4e19d77dc76eecf272fc", size = 107838, upload-time = "2025-09-08T16:29:41.907Z" }, + { url = "https://files.pythonhosted.org/packages/2f/75/8f0e8e266ea99ffbc69500a927f0c114a07fe465bfbc59871d6fe22d9ee0/bottleneck-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d33bcd60a13d0603f5db9d953352a3c098242c46f8f919290fd11c54b42b9e5", size = 113364, upload-time = "2025-09-08T16:29:43.437Z" }, + { url = "https://files.pythonhosted.org/packages/83/96/9d51012d729f97de1e75aad986f3ba50956742a40fc99cbab4c2aa896c1c/bottleneck-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:69ef4514782afe39db2497aaea93b1c167ab7ab3bc5e3930500ef9cf11841db7", size = 100400, upload-time = "2025-09-08T16:29:44.464Z" }, + { url = "https://files.pythonhosted.org/packages/16/f4/4fcbebcbc42376a77e395a6838575950587e5eb82edf47d103f8daa7ba22/bottleneck-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:727363f99edc6dc83d52ed28224d4cb858c07a01c336c7499c0c2e5dd4fd3e4a", size = 375920, upload-time = "2025-09-08T16:29:45.52Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/7fa8cdc41cbf2dfe0540f98e1e0caf9ffbd681b1a0fc679a91c2698adaf9/bottleneck-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:847671a9e392220d1dfd2ff2524b4d61ec47b2a36ea78e169d2aa357fd9d933a", size = 367922, upload-time = "2025-09-08T16:29:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/13/7d/dccfa4a2792c1bdc0efdde8267e527727e517df1ff0d4976b84e0268c2f9/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:daef2603ab7b4ec4f032bb54facf5fa92dacd3a264c2fd9677c9fc22bcb5a245", size = 361379, upload-time = "2025-09-08T16:29:48.042Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/21c0fad823b71c3a8904cbb847ad45136d25573a2d001a9cff48d3985fab/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc7f09bda980d967f2e9f1a746eda57479f824f66de0b92b9835c431a8c922d4", size = 371911, upload-time = "2025-09-08T16:29:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/830ff80f8c74577d53034c494639eac7a0ffc70935c01ceadfbe77f590c2/bottleneck-1.6.0-cp311-cp311-win32.whl", hash = "sha256:1f78bad13ad190180f73cceb92d22f4101bde3d768f4647030089f704ae7cac7", size = 107831, upload-time = "2025-09-08T16:29:51.397Z" }, + { url = "https://files.pythonhosted.org/packages/6f/42/01d4920b0aa51fba503f112c90714547609bbe17b6ecfc1c7ae1da3183df/bottleneck-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f2adef59fdb9edf2983fe3a4c07e5d1b677c43e5669f4711da2c3daad8321ad", size = 113358, upload-time = "2025-09-08T16:29:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/8d/72/7e3593a2a3dd69ec831a9981a7b1443647acb66a5aec34c1620a5f7f8498/bottleneck-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bb16a16a86a655fdbb34df672109a8a227bb5f9c9cf5bb8ae400a639bc52fa3", size = 100515, upload-time = "2025-09-08T16:29:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/e7bbea08f4c0f0bab819d38c1a613da5f194fba7b19aae3e2b3a27e78886/bottleneck-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fbf5d0787af9aee6cef4db9cdd14975ce24bd02e0cc30155a51411ebe2ff35f", size = 377451, upload-time = "2025-09-08T16:29:56.718Z" }, + { url = "https://files.pythonhosted.org/packages/fe/80/a6da430e3b1a12fd85f9fe90d3ad8fe9a527ecb046644c37b4b3f4baacfc/bottleneck-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d08966f4a22384862258940346a72087a6f7cebb19038fbf3a3f6690ee7fd39f", size = 368303, upload-time = "2025-09-08T16:29:57.834Z" }, + { url = "https://files.pythonhosted.org/packages/30/11/abd30a49f3251f4538430e5f876df96f2b39dabf49e05c5836820d2c31fe/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:604f0b898b43b7bc631c564630e936a8759d2d952641c8b02f71e31dbcd9deaa", size = 361232, upload-time = "2025-09-08T16:29:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ac/1c0e09d8d92b9951f675bd42463ce76c3c3657b31c5bf53ca1f6dd9eccff/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d33720bad761e642abc18eda5f188ff2841191c9f63f9d0c052245decc0faeb9", size = 373234, upload-time = "2025-09-08T16:30:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/382c572ae3057ba885d484726bb63629d1f63abedf91c6cd23974eb35a9b/bottleneck-1.6.0-cp312-cp312-win32.whl", hash = "sha256:a1e5907ec2714efbe7075d9207b58c22ab6984a59102e4ecd78dced80dab8374", size = 108020, upload-time = "2025-09-08T16:30:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493, upload-time = "2025-09-08T16:30:02.872Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/e117cd5ff7056126d3291deb29ac8066476e60b852555b95beb3fc9d62a0/bottleneck-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015de414ca016ebe56440bdf5d3d1204085080527a3c51f5b7b7a3e704fe6fd", size = 100521, upload-time = "2025-09-08T16:30:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:456757c9525b0b12356f472e38020ed4b76b18375fd76e055f8d33fb62956f5e", size = 377719, upload-time = "2025-09-08T16:30:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/76593af47097d9633109bed04dbcf2170707dd84313ca29f436f9234bc51/bottleneck-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c65254d51b6063c55f6272f175e867e2078342ae75f74be29d6612e9627b2c0", size = 368577, upload-time = "2025-09-08T16:30:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/4dcacaf637d2b8d89ea746c74159adda43858d47358978880614c3fa4391/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a172322895fbb79c6127474f1b0db0866895f0b804a18d5c6b841fea093927fe", size = 361441, upload-time = "2025-09-08T16:30:07.613Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/21eb1eb1c42cb7be2872d0647c292fc75768d14e1f0db66bf907b24b2464/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5e81b642eb0d5a5bf00312598d7ed142d389728b694322a118c26813f3d1fa9", size = 373416, upload-time = "2025-09-08T16:30:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/48/cb/7957ff40367a151139b5f1854616bf92e578f10804d226fbcdecfd73aead/bottleneck-1.6.0-cp313-cp313-win32.whl", hash = "sha256:543d3a89d22880cd322e44caff859af6c0489657bf9897977d1f5d3d3f77299c", size = 108029, upload-time = "2025-09-08T16:30:09.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/735df4156fa5595501d5d96a6ee102f49c13d2ce9e2a287ad51806bc3ba0/bottleneck-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:48a44307d604ceb81e256903e5d57d3adb96a461b1d3c6a69baa2c67e823bd36", size = 113497, upload-time = "2025-09-08T16:30:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/8c1260df8ade7cebc2a8af513a27082b5e36aa4a5fb762d56ea6d969d893/bottleneck-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547e6715115867c4657c9ae8cc5ddac1fec8fdad66690be3a322a7488721b06b", size = 101606, upload-time = "2025-09-08T16:30:11.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/f03e2944e91ee962922c834ed21e5be6d067c8395681f5dc6c67a0a26853/bottleneck-1.6.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e4a4a6e05b6f014c307969129e10d1a0afd18f3a2c127b085532a4a76677aef", size = 391804, upload-time = "2025-09-08T16:30:13.13Z" }, + { url = "https://files.pythonhosted.org/packages/0b/58/2b356b8a81eb97637dccee6cf58237198dd828890e38be9afb4e5e58e38e/bottleneck-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2baae0d1589b4a520b2f9cf03528c0c8b20717b3f05675e212ec2200cf628f12", size = 383443, upload-time = "2025-09-08T16:30:14.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/cf7d09ed3736ad0d50c624787f9b580ae3206494d95cc0f4814b93eef728/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2e407139b322f01d8d5b6b2e8091b810f48a25c7fa5c678cfcdc420dfe8aea0a", size = 375458, upload-time = "2025-09-08T16:30:15.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/7c87a34a24e339860064f20fac49f6738e94f1717bc8726b9c47705601d8/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adefb89b92aba6de9c6ea871d99bcd29d519f4fb012cc5197917813b4fc2c7f", size = 386384, upload-time = "2025-09-08T16:30:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/59/57/db51855e18a47671801180be748939b4c9422a0544849af1919116346b5f/bottleneck-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:64b8690393494074923780f6abdf5f5577d844b9d9689725d1575a936e74e5f0", size = 109448, upload-time = "2025-09-08T16:30:18.076Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/683c090b624f13a5bf88a0be2241dc301e98b2fb72a45812a7ae6e456cc4/bottleneck-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:cb67247f65dcdf62af947c76c6c8b77d9f0ead442cac0edbaa17850d6da4e48d", size = 115190, upload-time = "2025-09-08T16:30:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/eb7c08964a3f3c4719f98795ccd21807ee9dd3071a0f9ad652a5f19196ff/bottleneck-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1d789042511a0f042b3bdcd2903e8567e956d3aa3be189cce3746daeb8550", size = 100544, upload-time = "2025-09-08T16:30:20.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/ec/c6f3be848f37689f481797ce7d9807d5f69a199d7fc0e46044f9b708c468/bottleneck-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fad24c99e39ad7623fc2a76d37feb26bd32e4dd170885edf4dbf4bfce2199a3", size = 378315, upload-time = "2025-09-08T16:30:21.409Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/2d6600836e2ea8f14fcefac592dc83497e5b88d381470c958cb9cdf88706/bottleneck-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643e61e50a6f993debc399b495a1609a55b3bd76b057e433e4089505d9f605c7", size = 368978, upload-time = "2025-09-08T16:30:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/bf72b49f5040212873b985feef5050015645e0a02204b591e1d265fc522a/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa668efbe4c6b200524ea0ebd537212da9b9801287138016fdf64119d6fcf201", size = 362074, upload-time = "2025-09-08T16:30:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c8/c4891a0604eb680031390182c6e264247e3a9a8d067d654362245396fadf/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f7dd35262e89e28fedd79d45022394b1fa1aceb61d2e747c6d6842e50546daa", size = 374019, upload-time = "2025-09-08T16:30:26.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/ed096f8d1b9147e84914045dd89bc64e3c32eee49b862d1e20d573a9ab0d/bottleneck-1.6.0-cp314-cp314-win32.whl", hash = "sha256:bd90bec3c470b7fdfafc2fbdcd7a1c55a4e57b5cdad88d40eea5bc9bab759bf1", size = 110173, upload-time = "2025-09-08T16:30:27.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/70/1414acb6ae378a15063cfb19a0a39d69d1b6baae1120a64d2b069902549b/bottleneck-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:b43b6d36a62ffdedc6368cf9a708e4d0a30d98656c2b5f33d88894e1bcfd6857", size = 115899, upload-time = "2025-09-08T16:30:28.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ed/4570b5d8c1c85ce3c54963ebc37472231ed54f0b0d8dbb5dde14303f775f/bottleneck-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:53296707a8e195b5dcaa804b714bd222b5e446bd93cd496008122277eb43fa87", size = 101615, upload-time = "2025-09-08T16:30:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/c148faa07ae91f266be1f3fad1fde95aa2449e12937f3f3df2dd720b86e0/bottleneck-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6df19cc48a83efd70f6d6874332aa31c3f5ca06a98b782449064abbd564cf0e", size = 392411, upload-time = "2025-09-08T16:30:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/e6ad221d345a059e7efb2ad1d46a22d9fdae0486faef70555766e1123966/bottleneck-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96bb3a52cb3c0aadfedce3106f93ab940a49c9d35cd4ed612e031f6deb27e80f", size = 384022, upload-time = "2025-09-08T16:30:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/40/5b15c01eb8c59d59bc84c94d01d3d30797c961f10ec190f53c27e05d62ab/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1db9e831b69d5595b12e79aeb04cb02873db35576467c8dd26cdc1ee6b74581", size = 376004, upload-time = "2025-09-08T16:30:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/cb228f5949553a5c01d1d5a3c933f0216d78540d9e0bf8dd4343bb449681/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4dd7ac619570865fcb7a0e8925df418005f076286ad2c702dd0f447231d7a055", size = 386909, upload-time = "2025-09-08T16:30:34.973Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/425065c37a67a9120bf53290371579b83d05bf46f3212cce65d8c01d470a/bottleneck-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:7fb694165df95d428fe00b98b9ea7d126ef786c4a4b7d43ae2530248396cadcb", size = 111636, upload-time = "2025-09-08T16:30:36.044Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "cachey" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "heapdict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/9c/e3c959c1601013bf8a72e8bf91ea1ebc6fe8a2305bd2324b039ee0403277/cachey-0.2.1.tar.gz", hash = "sha256:0310ba8afe52729fa7626325c8d8356a8421c434bf887ac851e58dcf7cf056a6", size = 6461, upload-time = "2020-03-11T15:34:08.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/f0/e24f3e5d5d539abeb783087b87c26cfb99c259f1126700569e000243745a/cachey-0.2.1-py3-none-any.whl", hash = "sha256:49cf8528496ce3f99d47f1bd136b7c88237e55347a15d880f47cefc0615a83c3", size = 6415, upload-time = "2020-03-11T15:34:07.347Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cmake" +version = "4.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/07/f1d6f7bcf056a139352cc2972f92a92005ac0ee98103165b1f620873b196/cmake-4.3.2.tar.gz", hash = "sha256:5f47f5f00910c474662d09a0516413c6e9750bde73cdc52dea3988102a274e06", size = 36969, upload-time = "2026-04-23T21:51:35.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/15/4c8980d5ceef53f0c490425f5b8e47f3ff863348400b0ea5ba4e349119f1/cmake-4.3.2-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f8f570813753ed4564928cf45c4c13c31e46b3e66b1a07fe695cb9f7b7af185e", size = 52883814, upload-time = "2026-04-23T21:50:16.764Z" }, + { url = "https://files.pythonhosted.org/packages/bc/50/2f336143dbcf5eca7c2d7e86273a84ef60231a0b7cfa33143d964c62f250/cmake-4.3.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ca739ab0d8960261fdb1bb6e1e6c16b9cd033ae0a98341483cc233ba7c81b22b", size = 29578008, upload-time = "2026-04-23T21:50:21.965Z" }, + { url = "https://files.pythonhosted.org/packages/52/e9/4b571a24924b5bdbd5c0b0fbb0b6b1008eb6472ceb9f23bca9e08e09632e/cmake-4.3.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b81038453a40aed73f8d28185c4c3ef43c3a91a7ff1577ce08e498769ecfe16", size = 30676971, upload-time = "2026-04-23T21:50:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d4/87f91ba2030c862a27a5b2df42e4e80d3244910acd10aa4cdcd0111820a9/cmake-4.3.2-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8add046a4ef7c606d8e0a444050415054c7da3b8535b2c8ce1f03e265dda098d", size = 30462837, upload-time = "2026-04-23T21:50:28.785Z" }, + { url = "https://files.pythonhosted.org/packages/21/ee/5475cd861db5e8e22fd6b3fd323a67f8f62df487163f72bc281739309bcf/cmake-4.3.2-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:7105a5411bbc405242677d29222812028566aafac3f78fda08c1717f03e5ca2a", size = 28377235, upload-time = "2026-04-23T21:50:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/63/cd/1008be054420fd759b73d3328326d047ea693c3910a55cb2a597d911baee/cmake-4.3.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:339655b93289c1b03c6a72523d46d3b0d19dc51406d3a90f8eefcbec525cb271", size = 29512079, upload-time = "2026-04-23T21:50:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/13/c4/e7c3649c4941927aff24c464090c4ce7f1f24167077f1b8aff3994def88d/cmake-4.3.2-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:ea95db137fd27f420d5e149c41e1f7621e786869afa3ca0fe18301df9e066607", size = 26628649, upload-time = "2026-04-23T21:50:40.047Z" }, + { url = "https://files.pythonhosted.org/packages/5a/05/ee3b002e0e7303a36e1f3c52e18d004fe089076a0e7b38df5e64a2328e88/cmake-4.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3947bc5973ca3846c76486993abd0fba0cb9119300d58ed9173a672d1eef505b", size = 26769012, upload-time = "2026-04-23T21:50:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/4e42625ce96197ffd72bb6a0e9c02d64506ee3075dd801219bb44fd9dcf7/cmake-4.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bc316be89fa43c265697c3b9ffcebde977bcc4515974372cc460c974f458ff98", size = 38595561, upload-time = "2026-04-23T21:50:48.601Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/80ab6fa7aceff0af186a844b6a53c3022e9775467804a1ce2ec3aafb02b3/cmake-4.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d10648929eb3294449ceae7a7c0b9714ca445280ddc25c209f427e7a07c6f3a9", size = 35226159, upload-time = "2026-04-23T21:50:53.968Z" }, + { url = "https://files.pythonhosted.org/packages/10/e3/73fc202fb943221a7ce78a5374c7a73093d26af85378ddab9c04586dc98b/cmake-4.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a69e629e6cd973e1f9b11d247d116acb47b35cdd8e39aee4b04b9040851cd8a0", size = 41262833, upload-time = "2026-04-23T21:50:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/3ef79820bfbcd5b51ef3b8029fc9c96da363daa11aae02ddd82df08fa446/cmake-4.3.2-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:95e0ff31a692d12130f33d1467d0074f8314cf3a79940694896de9367b6e4fae", size = 40437896, upload-time = "2026-04-23T21:51:04.304Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/8a1147fa26a3c1564bf654b744baeb2b8c0efccfde04190d3f9222c27bde/cmake-4.3.2-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:67775407b963385a7942dd56f0567ef3c75453c6336654aeafe43165d78648bb", size = 35492662, upload-time = "2026-04-23T21:51:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/42a744beb4c9f6e459f1a57162f391a9941f31850048fc461e88095f98da/cmake-4.3.2-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:aae70bb95762f6da20131cf72da6322557ff6784968755d201d42a44cd9494e5", size = 37723238, upload-time = "2026-04-23T21:51:13.595Z" }, + { url = "https://files.pythonhosted.org/packages/06/4c/513a73685feef886c824982b6618bb3cc7e7ac8579b34f1fe043bce11af5/cmake-4.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1c5c292b1189e48d01f0bed01ed800c31eed967afd033c4beabff9cc97209f2", size = 38554155, upload-time = "2026-04-23T21:51:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/14/aa/35b387f6cc0990247195109b04523a1c05dbb4f0fd0bae28e7f34c4a1e6f/cmake-4.3.2-py3-none-win32.whl", hash = "sha256:3c55f0c61c70642d9e7f6b4fc638622027f045b388e357d74efcca4a7111e4aa", size = 37819702, upload-time = "2026-04-23T21:51:22.933Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/7ceb7223d274e88d621ce00f2160ae74aead18a3d36f61b8fb52cbe6b7ca/cmake-4.3.2-py3-none-win_amd64.whl", hash = "sha256:78049aac277aabe376d8e82993f0be234d086d0e8ad4708755d5a209a04e1138", size = 41272507, upload-time = "2026-04-23T21:51:27.93Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/fa4da5330b63d5ce7909892005eced21d1fca58d022ed6b40f216f6f6c52/cmake-4.3.2-py3-none-win_arm64.whl", hash = "sha256:b218d636a99fa0eb23713d37d3e3c3a9c0e707e0579b46780ff908acef229386", size = 39613878, upload-time = "2026-04-23T21:51:33.041Z" }, +] + +[[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.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "(python_full_version < '3.12' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and platform_python_implementation != 'PyPy' and extra == 'extra-10-deeplabcut-tf') or (platform_python_implementation != 'PyPy' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/31/bfcc870f69c6a017c4ad5c42316207fc7551940db6f3639aa4466ec5faf3/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a022c96b8bd847e8dc0675523431149a4c3e872f440e3002213dbb9e08f0331a", size = 11800959, upload-time = "2025-10-21T14:51:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1e/9c8ed3f3dbed7b7d038805fdc65cbc65fda9983e84437778a9571e7092bc/cuda_bindings-12.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:f69107389e6b9948969bfd0a20c4f571fd1aefcfb1d2e1b72cc8ba5ecb7918ab", size = 11464568, upload-time = "2025-10-21T14:51:31.454Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2b/ebcbb60aa6dba830474cd360c42e10282f7a343c0a1f58d24fbd3b7c2d77/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306", size = 11840604, upload-time = "2025-10-21T14:51:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/dd/be/90d32049e06abcfba4b2e7df1dbcb5e16215c8852eef0cd8b25f38a66bd4/cuda_bindings-12.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913", size = 11490933, upload-time = "2025-10-21T14:51:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/ec/07/6aff13bc1e977e35aaa6b22f52b172e2890c608c6db22438cf7ed2bf43a6/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d", size = 11566797, upload-time = "2025-10-21T14:51:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3c/972edfddb4ae8a9fccd3c3766ed47453b6f805b6026b32f10209dd4b8ad4/cuda_bindings-12.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c", size = 11894363, upload-time = "2025-10-21T14:51:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b5/96a6696e20c4ffd2b327f54c7d0fde2259bdb998d045c25d5dedbbe30290/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5", size = 11624530, upload-time = "2025-10-21T14:52:01.539Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/e6/87/652796522cc1a7af559460e1ce59b642e05c1468b9c08522a9a096b4cf04/cuda_bindings-12.9.4-cp314-cp314-win_amd64.whl", hash = "sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575", size = 11517716, upload-time = "2025-10-21T14:52:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/39/73/d2fc40c043bac699c3880bf88d3cebe9d88410cd043795382826c93a89f0/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f", size = 11565056, upload-time = "2025-10-21T14:52:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { url = "https://files.pythonhosted.org/packages/ab/52/a30f46e822bfa6b4a659d1e8de8c4a4adf908ea075dac568b55362541bd8/cuda_bindings-12.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43", size = 12055608, upload-time = "2025-10-21T14:52:12.335Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ea/81999d01375645f34596c76eb046b4b36d58cc6fe2bddb2410f8a7b7a827/cuda_bindings-13.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:845025438a1b9e20718b9fb42add3e0eb72e85458bcab3eeb80bfd8f0a9dab33", size = 5600047, upload-time = "2026-03-11T00:12:34.848Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/29/5a/0ce1731c48bcd9f40996a4ef1abbf634f1a7fe4a15c5050b1e75ce3a7acf/cuda_bindings-13.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:debb51b211d246f8326f6b6e982506a5d0d9906672c91bc478b66addc7ecc60a", size = 5631363, upload-time = "2026-03-11T00:12:41.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a5/d7f01a415e134546248cef612adad8153c9f1eb10ec79505a7cd8294370b/cuda_bindings-13.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:45815daeb595bf3b405c52671a2542b1f8e9329f3b029494acbfcc74aeaa1f2d", size = 5840830, upload-time = "2026-03-11T00:12:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c4/84/d3b6220b51cbc02ca14db7387e97445126b4ff5125aaa6c5dd7dcb75e679/cuda_bindings-13.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8cebe3ce4aeeca5af9c490e175f76c4b569bbf4a35a62294b777bc77bf7ac4d8", size = 5796512, upload-time = "2026-03-11T00:12:54.483Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/e3/73/98bcb069778fe420226db75aff54b5dd6c3ecfd0912edabab723326e80b7/cuda_bindings-13.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd658bb5c0e55b7b3e5dd0ed509c6addb298c665db26a9bfba35e1e626000ba2", size = 5938605, upload-time = "2026-03-11T00:13:01.639Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/4e01cc06447d39476e138d1b1adec8d35c0d04eccd2c8d69befc08cd66e8/cuda_bindings-13.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ccf14e0c1def3b7200100aafff3a9f7e210ecb6e409329e92dcf6cd2c00d5c7", size = 6662637, upload-time = "2026-03-11T00:13:07.881Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dask" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/2a/5d8cc1579590af86576dde890254440e478c7174b93a02095ecfc2e6ba38/dask-2026.3.0.tar.gz", hash = "sha256:f7d96c8274e8a900d217c1ff6ea8d1bbf0b4c2c21e74a409644498d925eb8f85", size = 11000710, upload-time = "2026-03-18T07:10:14.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl", hash = "sha256:be614b9242b0b38288060fb2d7696125946469c98a1c30e174883fd199e0428d", size = 1485630, upload-time = "2026-03-18T07:10:12.832Z" }, +] + +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] +dataframe = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, +] + +[[package]] +name = "dask-image" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask", extra = ["array", "dataframe"] }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pims" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/c4/7b83217443201469384a415687a8b89da8e55fc7a182e9507a69851a78b9/dask_image-2025.11.0.tar.gz", hash = "sha256:45cf1a9c3a8a1c143c75d43f1494e4fe0827564d3ec6efb93618fb04603ba0b3", size = 79561, upload-time = "2025-11-13T01:57:28.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/4b/817da308fa1170da07ef01259585887a3bbb6ab80700b3e61ce4967301ec/dask_image-2025.11.0-py3-none-any.whl", hash = "sha256:4834ece8d7133f8cd7d4e672f7f5a598c9057e687b20f14f3121360e3e1690b4", size = 61936, upload-time = "2025-11-13T01:57:27.133Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "deeplabcut" +version = "3.0.0" +source = { editable = "." } +dependencies = [ + { name = "albumentations" }, + { name = "dlclibrary" }, + { name = "einops" }, + { name = "filelock" }, + { name = "filterpy" }, + { name = "h5py", marker = "sys_platform == 'darwin' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "huggingface-hub" }, + { name = "imageio-ffmpeg" }, + { name = "imgaug" }, + { name = "matplotlib" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas", extra = ["hdf5", "performance"] }, + { name = "pillow" }, + { name = "pycocotools" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "ruamel-yaml" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "statsmodels" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "timm" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.27.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "tqdm" }, +] + +[package.optional-dependencies] +apple-mchips = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack", marker = "sys_platform == 'darwin'" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim", marker = "sys_platform == 'darwin'" }, +] +docs = [ + { name = "jupyter-book" }, + { name = "numpydoc" }, + { name = "sphinxcontrib-mermaid" }, +] +fmpose3d = [ + { name = "fmpose3d" }, +] +gui = [ + { name = "napari-deeplabcut" }, + { name = "pyside6" }, + { name = "qdarkstyle" }, +] +modelzoo = [ + { name = "huggingface-hub" }, +] +openvino = [ + { name = "openvino-dev" }, +] +tf = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim" }, +] +tf-cu11 = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'win32'" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" } }, + { name = "tf-slim" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" } }, +] +tf-cu12 = [ + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.18.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.18.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tf-slim" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" } }, +] +tf-latest = [ + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.20.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim" }, +] +wandb = [ + { name = "wandb" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "nbformat" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "albumentations", specifier = "<=1.4.3" }, + { name = "dlclibrary", specifier = ">=0.0.12" }, + { name = "einops" }, + { name = "filelock", specifier = ">=3.12,<3.16" }, + { name = "filterpy", specifier = ">=1.4.4" }, + { name = "fmpose3d", marker = "extra == 'fmpose3d'", specifier = ">=0.0.8" }, + { name = "h5py", marker = "sys_platform == 'darwin'", specifier = ">=3.15.1" }, + { name = "huggingface-hub", specifier = ">=0.23" }, + { name = "huggingface-hub", marker = "extra == 'modelzoo'" }, + { name = "imageio-ffmpeg" }, + { name = "imgaug", specifier = ">=0.4" }, + { name = "jupyter-book", marker = "extra == 'docs'", specifier = "==1.0.4.post1" }, + { name = "matplotlib", specifier = ">=3.3,!=3.7,!=3.7.1,<3.9" }, + { name = "napari-deeplabcut", marker = "extra == 'gui'", specifier = ">=0.3.1" }, + { name = "networkx", specifier = ">=2.6" }, + { name = "numba", specifier = ">=0.54" }, + { name = "numpy", specifier = ">=1.18.5,<2" }, + { name = "numpydoc", marker = "extra == 'docs'" }, + { name = "openvino-dev", marker = "extra == 'openvino'", specifier = "==2022.1" }, + { name = "packaging", specifier = ">=26" }, + { name = "pandas", extras = ["hdf5", "performance"], specifier = ">=2.2,<3" }, + { name = "pillow", specifier = ">=7.1" }, + { name = "protobuf", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-cu11'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-cu12'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-latest'", specifier = "<7" }, + { name = "pycocotools" }, + { name = "pydantic", specifier = ">=2,<3" }, + { name = "pyside6", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'gui'", specifier = "<6.10" }, + { name = "pyside6", marker = "(platform_machine != 'x86_64' and extra == 'gui') or (sys_platform != 'linux' and extra == 'gui')" }, + { name = "pyyaml" }, + { name = "qdarkstyle", marker = "extra == 'gui'", specifier = "==3.1" }, + { name = "ruamel-yaml", specifier = ">=0.15" }, + { name = "scikit-image", specifier = ">=0.17" }, + { name = "scikit-learn", specifier = ">=1" }, + { name = "scipy", specifier = ">=1.9" }, + { name = "sphinxcontrib-mermaid", marker = "extra == 'docs'" }, + { name = "statsmodels", specifier = ">=0.11" }, + { name = "tables", specifier = ">3.8" }, + { name = "tensorflow", marker = "python_full_version >= '3.12' and extra == 'tf'", specifier = ">=2.16.1,<2.18" }, + { name = "tensorflow", marker = "python_full_version < '3.12' and extra == 'tf'", specifier = ">=2.12,<2.16" }, + { name = "tensorflow", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=2.15,<2.18" }, + { name = "tensorflow", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=2.12,<2.15" }, + { name = "tensorflow", marker = "extra == 'tf-cu11'", specifier = "==2.14" }, + { name = "tensorflow", marker = "extra == 'tf-cu12'", specifier = "==2.18" }, + { name = "tensorflow", marker = "extra == 'tf-latest'", specifier = ">=2.18" }, + { name = "tensorflow-io-gcs-filesystem", marker = "python_full_version < '3.12' and sys_platform == 'win32' and extra == 'tf'", specifier = "==0.31" }, + { name = "tensorflow-io-gcs-filesystem", marker = "sys_platform == 'win32' and extra == 'tf-cu11'", specifier = "==0.31" }, + { name = "tensorflow-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=1.2" }, + { name = "tensorflow-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'tf'", specifier = ">=1.2" }, + { name = "tensorflow-metal", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'tf'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-cu11'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-cu12'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-latest'", specifier = ">=1.2" }, + { name = "tensorpack", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=0.11" }, + { name = "tensorpack", marker = "extra == 'tf'", specifier = ">=0.11" }, + { name = "tensorpack", marker = "extra == 'tf-cu11'", specifier = "==0.11" }, + { name = "tensorpack", marker = "extra == 'tf-cu12'", specifier = "==0.11" }, + { name = "tensorpack", marker = "extra == 'tf-latest'", specifier = ">=0.11" }, + { name = "tf-keras", marker = "python_full_version >= '3.12' and extra == 'tf'", specifier = ">=2.15,<2.18" }, + { name = "tf-keras", marker = "python_full_version < '3.12' and extra == 'tf'", specifier = "<2.15" }, + { name = "tf-keras", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'" }, + { name = "tf-keras", marker = "extra == 'tf-cu11'", specifier = "==2.14.1" }, + { name = "tf-keras", marker = "extra == 'tf-cu12'", specifier = "==2.18" }, + { name = "tf-keras", marker = "extra == 'tf-latest'" }, + { name = "tf-slim", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=1.1" }, + { name = "tf-slim", marker = "extra == 'tf'", specifier = ">=1.1" }, + { name = "tf-slim", marker = "extra == 'tf-cu11'", specifier = "==1.1" }, + { name = "tf-slim", marker = "extra == 'tf-cu12'", specifier = "==1.1" }, + { name = "tf-slim", marker = "extra == 'tf-latest'", specifier = ">=1.1" }, + { name = "timm" }, + { name = "torch", specifier = ">=2" }, + { name = "torch", marker = "extra == 'tf-cu11'", specifier = "<2.1" }, + { name = "torch", marker = "extra == 'tf-cu12'", specifier = "<2.11" }, + { name = "torchvision" }, + { name = "torchvision", marker = "extra == 'tf-cu11'", specifier = "<0.16" }, + { name = "torchvision", marker = "extra == 'tf-cu12'", specifier = "<0.26" }, + { name = "tqdm" }, + { name = "wandb", marker = "extra == 'wandb'" }, +] +provides-extras = ["gui", "openvino", "docs", "fmpose3d", "tf", "tf-cu11", "tf-cu12", "tf-latest", "apple-mchips", "modelzoo", "wandb"] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage" }, + { name = "nbformat", specifier = ">5" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dlclibrary" +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/bf/14c4a73bfb090642a2f8353ff38a623024ee39f7efb4966962ed2a194c3a/dlclibrary-0.0.12.tar.gz", hash = "sha256:9c1dcb98edcba03f33c31e0c0f9d18ce1c349bef65f8d3cd5bde49d5e76171db", size = 13187, upload-time = "2026-05-19T08:04:33.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/57/c0ebe1191a2a445e0a671ce9255b149747ff6285aa6610e355b2e4e72cfe/dlclibrary-0.0.12-py3-none-any.whl", hash = "sha256:be8e22d334fd37cfde6e68936b73ee45a2f0f55baedc83832d773e4f2aedb38b", size = 17488, upload-time = "2026-05-19T08:04:32.456Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/dd/49e06f09b6645156550fb9aee9cc1e59aba7efbc972d665a1bd6ae0435d4/filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", size = 18007, upload-time = "2024-06-22T15:59:14.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f0/48285f0262fe47103a4a45972ed2f9b93e4c80b8fd609fa98da78b2a5706/filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7", size = 16159, upload-time = "2024-06-22T15:59:12.695Z" }, +] + +[[package]] +name = "filterpy" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/1d/ac8914360460fafa1990890259b7fa5ef7ba4cd59014e782e4ab3ab144d8/filterpy-1.4.5.zip", hash = "sha256:4f2a4d39e4ea601b9ab42b2db08b5918a9538c168cff1c6895ae26646f3d73b1", size = 177985, upload-time = "2018-10-10T22:38:24.63Z" } + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + +[[package]] +name = "fmpose3d" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "filterpy" }, + { name = "huggingface-hub" }, + { name = "numba" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "timm" }, + { name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.27.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/88/df6b75e3267f859c542244a6eca5edba7ecda4660bcddc6680aa2c5d74b0/fmpose3d-0.0.9.tar.gz", hash = "sha256:89e0137ed3525d2d8b6a4a6276a810643c5afd5556b32f3899eac909287d85bd", size = 114115, upload-time = "2026-04-10T12:14:09.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/1d/fb7a16fafcf437a590462b691372a99e64219e484ebb0e1fa3f849b5eeb9/fmpose3d-0.0.9-py3-none-any.whl", hash = "sha256:9d3a0aea8fdb54234c2dd9ec8d9df5dad3c16350eb7b9aba09f22345165e6d24", size = 131801, upload-time = "2026-04-10T12:14:08.562Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "freetype-py" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/9c/61ba17f846b922c2d6d101cc886b0e8fb597c109cedfcb39b8c5d2304b54/freetype-py-2.5.1.zip", hash = "sha256:cfe2686a174d0dd3d71a9d8ee9bf6a2c23f5872385cf8ce9f24af83d076e2fbd", size = 851738, upload-time = "2024-08-29T18:32:26.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/a8/258dd138ebe60c79cd8cfaa6d021599208a33f0175a5e29b01f60c9ab2c7/freetype_py-2.5.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d01ded2557694f06aa0413f3400c0c0b2b5ebcaabeef7aaf3d756be44f51e90b", size = 1747885, upload-time = "2024-08-29T18:32:17.604Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/280ad06dc944e40789b0a641492321a2792db82edda485369cbc59d14366/freetype_py-2.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2f6b3d68496797da23204b3b9c4e77e67559c80390fc0dc8b3f454ae1cd819", size = 1051055, upload-time = "2024-08-29T18:32:19.153Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/853cad240ec63e21a37a512ee19c896b655ce1772d803a3dd80fccfe63fe/freetype_py-2.5.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:289b443547e03a4f85302e3ac91376838e0d11636050166662a4f75e3087ed0b", size = 1043856, upload-time = "2024-08-29T18:32:20.565Z" }, + { url = "https://files.pythonhosted.org/packages/93/6f/fcc1789e42b8c6617c3112196d68e87bfe7d957d80812d3c24d639782dcb/freetype_py-2.5.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cd3bfdbb7e1a84818cfbc8025fca3096f4f2afcd5d4641184bf0a3a2e6f97bbf", size = 1108180, upload-time = "2024-08-29T18:32:21.871Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1b/161d3a6244b8a820aef188e4397a750d4a8196316809576d015f26594296/freetype_py-2.5.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3c1aefc4f0d5b7425f014daccc5fdc7c6f914fb7d6a695cc684f1c09cd8c1660", size = 1106792, upload-time = "2024-08-29T18:32:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "gast" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, +] + +[[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.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyasn1-modules", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests-oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/b4/ef2170c5f6aa5bc2461bab959a84e56d2819ce26662b50038d2d0602223e/google-auth-oauthlib-1.0.0.tar.gz", hash = "sha256:e375064964820b47221a7e1b7ee1fd77051b6323c3f9e3e19785f78ab67ecfc5", size = 20530, upload-time = "2023-02-07T20:53:20.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/07/8d9a8186e6768b55dfffeb57c719bc03770cf8a970a074616ae6f9e26a57/google_auth_oauthlib-1.0.0-py2.py3-none-any.whl", hash = "sha256:95880ca704928c300f48194d1770cf5b1462835b6e49db61445a520f793fd5fb", size = 18926, upload-time = "2023-02-07T20:53:18.837Z" }, +] + +[[package]] +name = "google-pasta" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ae/1db979ff6ae7958d80b288f63d5f6c30df96682700ea9fc340ce994d94a1/greenlet-3.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", size = 619894, upload-time = "2026-04-27T13:02:35.13Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, + { url = "https://files.pythonhosted.org/packages/ce/94/b0590e3d1978f02419f30502341c40d72f77eb0a2198119fe27df47714ee/greenlet-3.5.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", size = 415681, upload-time = "2026-04-27T13:05:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[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 = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, + { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + +[[package]] +name = "heapdict" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/9b/d8963ae7e388270b695f3b556b6dc9adb70ae9618fba09aa1e7b1886652d/HeapDict-1.0.1.tar.gz", hash = "sha256:8495f57b3e03d8e46d5f1b2cc62ca881aca392fd5cc048dc0aa2e1a6d23ecdb6", size = 4274, upload-time = "2019-09-09T18:57:02.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9d/cd4777dbcf3bef9d9627e0fe4bc43d2e294b1baeb01d0422399d5e9de319/HeapDict-1.0.1-py3-none-any.whl", hash = "sha256:6065f90933ab1bb7e50db403b90cab653c853690c5992e69294c2de2b253fc92", size = 3917, upload-time = "2019-09-09T18:57:00.821Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "hsluv" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/81/af16607fa045724e515579d312577261b436f36f419e7c677e7e88fcc943/hsluv-5.0.4.tar.gz", hash = "sha256:2281f946427a882010042844a38c7bbe9e0d0aaf9d46babe46366ed6f169b72e", size = 543090, upload-time = "2023-09-11T21:46:52.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/36/5bddefea3d7adf22a64f9aa9701492f8a9fe6948223f5cf2602c22ec9be7/hsluv-5.0.4-py2.py3-none-any.whl", hash = "sha256:0138bd10038e2ee1b13eecae9a7d49d4ec8c320b1d7eb4f860832c792e3e4567", size = 5252, upload-time = "2023-09-11T21:46:50.407Z" }, +] + +[[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]] +name = "huggingface-hub" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "imgaug" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "shapely" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/7d/820295b8fdaf06dce9688ef2fdeb5a317896d3276db7723e5a94e85e1253/imgaug-0.4.0.tar.gz", hash = "sha256:46bab63ed38f8980630ff721a09ca2281b7dbd4d8c11258818b6ebcc69ea46c7", size = 937254, upload-time = "2020-02-05T20:54:24.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/b1/af3142c4a85cba6da9f4ebb5ff4e21e2616309552caca5e8acefe9840622/imgaug-0.4.0-py2.py3-none-any.whl", hash = "sha256:ce61e65b4eb7405fc62c1b0a79d2fa92fd47f763aaecb65152d29243592111f9", size = 948018, upload-time = "2020-02-05T20:54:22.293Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "in-n-out" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/08/07edfac98a38ab0208557524cbdd94a296f565b0558417ccb2c03d14a6ea/in_n_out-0.2.1.tar.gz", hash = "sha256:43cde2b7de981d41a6d70618a2b7bd989481095922a53ead4dc75f2bbd5dffea", size = 26026, upload-time = "2024-04-22T18:56:50.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/06/711a4d105ad3d01d3ef351a1039bb5cc517a57dbf377d7da9a0808e34c77/in_n_out-0.2.1-py3-none-any.whl", hash = "sha256:343e81edb27cf41ec946134a92964f408465abdf6a065c6c55fe96f53bc3c8b7", size = 19951, upload-time = "2024-04-22T18:56:48.572Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { 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/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "decorator", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jedi", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pexpect", marker = "(python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "stack-data", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "traitlets", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, +] + +[[package]] +name = "ipython" +version = "9.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.11' and sys_platform == 'win32') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "decorator", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jedi", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pexpect", marker = "(python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "stack-data", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "traitlets", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, +] + +[[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 = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[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.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.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/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-book" +version = "1.0.4.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "linkify-it-py" }, + { name = "myst-nb" }, + { name = "myst-parser" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "sphinx-book-theme", version = "1.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-book-theme", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-comments" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-external-toc" }, + { name = "sphinx-jupyterbook-latex" }, + { name = "sphinx-multitoc-numbering" }, + { name = "sphinx-thebe" }, + { name = "sphinx-togglebutton" }, + { name = "sphinxcontrib-bibtex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/ee/5d10ce5b161764ad44219853f386e98b535cb3879bcb0d7376961a1e3897/jupyter_book-1.0.4.post1.tar.gz", hash = "sha256:2fe92c49ff74840edc0a86bb034eafdd0f645fca6e48266be367ce4d808b9601", size = 67412, upload-time = "2025-02-28T14:55:48.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/86/d45756beaeb4b9b06125599b429451f8640b5db6f019d606f33c85743fd4/jupyter_book-1.0.4.post1-py3-none-any.whl", hash = "sha256:3a27a6b2581f1894ffe8f347d1a3432f06fc616997547919c42cd41c54db625d", size = 45005, upload-time = "2025-02-28T14:55:46.561Z" }, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9", size = 32048, upload-time = "2024-11-15T16:03:55.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6", size = 33907, upload-time = "2024-11-15T16:03:54.021Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +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/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "keras" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/85/d52a86eb5ae700e1f8694157019249eb33350ae9e477cd03ecdb50939d22/keras-2.14.0.tar.gz", hash = "sha256:22788bdbc86d9988794fe9703bb5205141da797c4faeeb59497c58c3d94d34ed", size = 1251354, upload-time = "2023-09-11T17:21:04.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/58/34d4d8f1aa11120c2d36d7ad27d0526164b1a8ae45990a2fede31d0e59bf/keras-2.14.0-py3-none-any.whl", hash = "sha256:d7429d1d2131cc7eb1f2ea2ec330227c7d9d38dab3dfdf2e78defee4ecc43fcd", size = 1709236, upload-time = "2023-09-11T17:21:02.164Z" }, +] + +[[package]] +name = "keras" +version = "3.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.11'" }, + { name = "h5py", marker = "python_full_version < '3.11'" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "namex", marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "optree", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "rich", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/73/19e057f7a2a6d641246bacca21e0bbcb2be341afca98ea461a0f2a9ab92d/keras-3.12.2.tar.gz", hash = "sha256:e19c7c7f8f2a81e44d4f203e567731a15a270d8ef351060982b45a1fafdf3fce", size = 1129833, upload-time = "2026-05-07T21:48:18.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/ba/1f2daa7940d7c5c65efc85370453e9e67ace0d06b4a7346b53f0e7355453/keras-3.12.2-py3-none-any.whl", hash = "sha256:0433310d7d626d5cbbc58e98223b3a77ce7d7d4398bf7e169d4e8bdcf9ce0296", size = 1476474, upload-time = "2026-05-07T21:48:16.057Z" }, +] + +[[package]] +name = "keras" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "h5py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "namex", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "optree", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "rich", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/e7/97a7664581b73e4f9ff1d3a767a493b6ac5d3e0ed1926bd2b6b2c8bbccd7/keras-3.14.1.tar.gz", hash = "sha256:ef479173102ad29db89b53c232efdc3fb5ad57c28bc27ead59f3e78a1eecd05b", size = 1263647, upload-time = "2026-05-07T21:43:35.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/03/184267c1d09783dd070f1ddfd0d4beb7503139dfc7bd75b422867cf282fd/keras-3.14.1-py3-none-any.whl", hash = "sha256:ebd2c14d2af3c9de18083604d408483996407fc7d2f9ebd1d565961f96608c29", size = 1628606, upload-time = "2026-05-07T21:43:32.737Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "latexcodec" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/dd/4270b2c5e2ee49316c3859e62293bd2ea8e382339d63ab7bbe9f39c0ec3b/latexcodec-3.0.1.tar.gz", hash = "sha256:e78a6911cd72f9dec35031c6ec23584de6842bfbc4610a9678868d14cdfb0357", size = 31222, upload-time = "2025-06-17T18:47:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "libclang" +version = "18.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612, upload-time = "2024-03-17T16:04:37.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045, upload-time = "2024-06-30T17:40:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641, upload-time = "2024-03-18T15:52:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207, upload-time = "2024-03-17T15:00:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943, upload-time = "2024-03-17T16:03:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972, upload-time = "2024-03-17T16:12:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606, upload-time = "2024-03-17T16:17:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494, upload-time = "2024-03-17T16:14:20.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083, upload-time = "2024-03-17T16:42:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112, upload-time = "2024-03-17T16:42:59.565Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "lit" +version = "18.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b4/d7e210971494db7b9a9ac48ff37dfa59a8b14c773f9cf47e6bda58411c0d/lit-18.1.8.tar.gz", hash = "sha256:47c174a186941ae830f04ded76a3444600be67d5e5fb8282c3783fba671c4edb", size = 161127, upload-time = "2024-06-25T14:33:14.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/b36f150fa7c5bcc96a31a4d19a20fddbd1d965b6f02510b57a3bb8d4b930/lit-18.1.8-py3-none-any.whl", hash = "sha256:a873ff7acd76e746368da32eb7355625e2e55a2baaab884c9cc130f2ee0300f7", size = 96365, upload-time = "2024-06-25T14:33:12.101Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "magicgui" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "psygnal" }, + { name = "qtpy" }, + { name = "superqt", extra = ["iconify"] }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/9c/0d918ee0a9b31f16e9b76c1b86b81b5d4b7bc491354c76030b87790f0cab/magicgui-0.10.2.tar.gz", hash = "sha256:ae7a4cbb7ef2028b827b1877cf0b06743d756074fe6ef849391d62448ab7b65d", size = 20946219, upload-time = "2026-04-10T14:45:28.927Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/bb/5ba264d3ccc13294e74c48c3ef0c2e96b8b13765562628515654012cc47e/magicgui-0.10.2-py3-none-any.whl", hash = "sha256:0f304d4da1a4309ad15d38cb809ef73269cea73a3195ac944f38d7c56d8e0fd5", size = 128254, upload-time = "2026-04-10T14:45:27.173Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.8.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/4f/8487737a74d8be4ab5fbe6019b0fae305c1604cf7209500969b879b5f462/matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea", size = 35934425, upload-time = "2024-04-04T01:47:18.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/c0/1f88491656d21a2fecd90fbfae999b2f87bc44d439ef301ec8e0e4a937a0/matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014", size = 7603557, upload-time = "2024-04-04T01:47:46.363Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/aa059a4fb8154d5875a5ddd33f8d0a42d77c0225fe4325e9b9358f39b0bf/matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106", size = 7497421, upload-time = "2024-04-04T01:47:54.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/67/ded5217d42de1532193cd87db925c67997d23c68b20c3eaa9e4c6a0adb67/matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10", size = 11377985, upload-time = "2024-04-04T01:48:04.955Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/061f97211f942101070a46fecd813a6b1bd83590ed7b07c473cabd707fe7/matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0", size = 11608003, upload-time = "2024-04-04T01:48:16.25Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d3/5d0bb1d905e219543fdfd7ab04e9d641a766367c83a5ffbcea60d2b2cf2d/matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef", size = 9535368, upload-time = "2024-04-04T01:48:26.265Z" }, + { url = "https://files.pythonhosted.org/packages/62/5a/a5108ae3db37f35f8a2be8a57d62da327af239214c9661464ce09ee32d7d/matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338", size = 7656037, upload-time = "2024-04-04T01:48:34.761Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/62250ea25780d4b59c2c6044ec161235c47cc05a18d0ec0a05657de75b7d/matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661", size = 7606117, upload-time = "2024-04-04T01:48:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/60/12d4f27b859a74359306662da69c2d08826a2b05cfe7f96e66b490f41573/matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c", size = 7500108, upload-time = "2024-04-04T01:48:50.21Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/9e4f7f34dccf2d2768504410410db8d551c940457a2bec658dc4fa3b5aa2/matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa", size = 11382998, upload-time = "2024-04-04T01:49:01.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/e363612ac1a514abfb5505aa209dd5b724b3232a6de98710d7759559706a/matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71", size = 11613309, upload-time = "2024-04-04T01:49:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/32/4c/63164901acadb3ada55c5e0fd6b7f29c9033d7e131302884cd735611b77a/matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b", size = 9546019, upload-time = "2024-04-04T01:49:23.752Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d5/6227732ecab9165586966ccb54301e3164f61b470c954c4cf6940654fbe1/matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae", size = 7658174, upload-time = "2024-04-04T01:49:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/91/eb/65f3bd78ce757dadd455c220273349428384b162485cd8aa380b61a867ed/matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616", size = 7604083, upload-time = "2024-04-04T01:49:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/da/2b/2bb6073ca8d336da07ace7d98bf7bb9da8233f55876bb3db6a5ee924f3e9/matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732", size = 7496013, upload-time = "2024-04-04T01:49:48.174Z" }, + { url = "https://files.pythonhosted.org/packages/61/cd/976d3a9c10328da1d2fe183f7c92c45f1e125536226a6eb3a820c4753cd1/matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb", size = 11376749, upload-time = "2024-04-04T01:49:58.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ba/412149958e951876096198609b958b90a8a2c9bc07a96eeeaa9e2c480f30/matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30", size = 11600837, upload-time = "2024-04-04T01:50:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4f/e5b56ca109d8ab6bae37f519f15b891fc18809ddb8bc1aa26e0bfca83e25/matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25", size = 9538883, upload-time = "2024-04-04T01:50:19.268Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ca/e7bd1876a341ed8c456095962a582696cac1691cb6e55bd5ead15a755c5d/matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a", size = 7659712, upload-time = "2024-04-04T01:50:26.938Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[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 = "ml-dtypes" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/47/09ca9556bf99cfe7ddf129a3423642bd482a27a717bf115090493fa42429/ml_dtypes-0.2.0.tar.gz", hash = "sha256:6488eb642acaaf08d8020f6de0a38acee7ac324c1e6e92ee0c0fea42422cb797", size = 698948, upload-time = "2023-06-06T15:14:43.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/9f/3c133f83f3e5a7959345585e9ac715ef8bf6e8987551f240032e1b0d3ce6/ml_dtypes-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df6a76e1c8adf484feb138ed323f9f40a7b6c21788f120f7c78bec20ac37ee81", size = 1154492, upload-time = "2023-06-06T15:14:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/19/05/7a6480a69f8555a047a56ae6af9490bcdc5e432658208f3404d8e8442d02/ml_dtypes-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc29a0524ef5e23a7fbb8d881bdecabeb3fc1d19d9db61785d077a86cb94fab2", size = 1012633, upload-time = "2023-06-06T15:14:14.055Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1d/d5cf76e5e40f69dbd273036e3172ae4a614577cb141673427b80cac948df/ml_dtypes-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08c391c2794f2aad358e6f4c70785a9a7b1df980ef4c232b3ccd4f6fe39f719", size = 1017764, upload-time = "2023-06-06T15:14:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/51/c430b4f5f4a6df00aa41c1ee195e179489565e61cfad559506ca7442ce67/ml_dtypes-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:75015818a7fccf99a5e8ed18720cb430f3e71a8838388840f4cdf225c036c983", size = 938593, upload-time = "2023-06-06T15:14:17.473Z" }, + { url = "https://files.pythonhosted.org/packages/15/da/43bee505963da0c730ee50e951c604bfdb90d4cccc9c0044c946b10e68a7/ml_dtypes-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e70047ec2c83eaee01afdfdabee2c5b0c133804d90d0f7db4dd903360fcc537c", size = 1154491, upload-time = "2023-06-06T15:14:19.199Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/01570d615d16f504be091b914a6ae9a29e80d09b572ebebc32ecb1dfb22d/ml_dtypes-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36d28b8861a8931695e5a31176cad5ae85f6504906650dea5598fbec06c94606", size = 1012631, upload-time = "2023-06-06T15:14:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/d57c2d22e4801edeb7f3e7939214c0ea8a28c6e16f85208c2df2145e0213/ml_dtypes-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e85ba8e24cf48d456e564688e981cf379d4c8e644db0a2f719b78de281bac2ca", size = 1017764, upload-time = "2023-06-06T15:14:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/08/89/c727fde1a3d12586e0b8c01abf53754707d76beaa9987640e70807d4545f/ml_dtypes-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:832a019a1b6db5c4422032ca9940a990fa104eee420f643713241b3a518977fa", size = 938744, upload-time = "2023-06-06T15:14:25.77Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9e/76b84f77c7afee3b116dc8407903a2d5004ba3059a8f3dcdcfa6ebf33fff/ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5", size = 397975, upload-time = "2024-09-13T19:06:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/03/7b/32650e1b2a2713a5923a0af2a8503d0d4a8fc99d1e1e0a1c40e996634460/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24", size = 2182570, upload-time = "2024-09-13T19:06:46.189Z" }, + { url = "https://files.pythonhosted.org/packages/16/86/a9f7569e7e4f5395f927de38a13b92efa73f809285d04f2923b291783dd2/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354", size = 2160365, upload-time = "2024-09-13T19:06:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/9a3afb437702503514f3934ec8d7904270edf013d28074f3e700e5dfbb0f/ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f", size = 126633, upload-time = "2024-09-13T19:06:50.656Z" }, + { url = "https://files.pythonhosted.org/packages/d1/76/9835c8609c29f2214359e88f29255fc4aad4ea0f613fb48aa8815ceda1b6/ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975", size = 397973, upload-time = "2024-09-13T19:06:51.748Z" }, + { url = "https://files.pythonhosted.org/packages/7e/99/e68c56fac5de973007a10254b6e17a0362393724f40f66d5e4033f4962c2/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9", size = 2185134, upload-time = "2024-09-13T19:06:53.197Z" }, + { url = "https://files.pythonhosted.org/packages/28/bc/6a2344338ea7b61cd7b46fb24ec459360a5a0903b57c55b156c1e46c644a/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752", size = 2163661, upload-time = "2024-09-13T19:06:54.519Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d3/ddfd9878b223b3aa9a930c6100a99afca5cfab7ea703662e00323acb7568/ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6", size = 126727, upload-time = "2024-09-13T19:06:55.897Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077, upload-time = "2024-09-13T19:06:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554, upload-time = "2024-09-13T19:06:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488, upload-time = "2024-09-13T19:07:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462, upload-time = "2024-09-13T19:07:04.916Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "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 = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "msgpack-numpy" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/94/61e8aee142733ebfdc400a05bdac6e1763c4514bba3b42743d223f388450/msgpack-numpy-0.4.8.tar.gz", hash = "sha256:c667d3180513422f9c7545be5eec5d296dcbb357e06f72ed39cc683797556e69", size = 10923, upload-time = "2022-06-09T03:43:08.739Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl", hash = "sha256:773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da", size = 6919, upload-time = "2022-06-09T03:43:06.82Z" }, +] + +[[package]] +name = "myst-nb" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "ipykernel" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jupyter-cache" }, + { name = "myst-parser" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/b4/ff1abeea67e8cfe0a8c033389f6d1d8b0bfecfd611befb5cbdeab884fce6/myst_nb-1.4.0.tar.gz", hash = "sha256:c145598de62446a6fd009773dd071a40d3b76106ace780de1abdfc6961f614c2", size = 82285, upload-time = "2026-03-02T21:14:56.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/93/0a378b48488879a1d925b42a804edfc6e0cd0ef854220f2dce738a46e7e9/myst_nb-1.4.0-py3-none-any.whl", hash = "sha256:0e2c86e7d3b82c3aa51383f82d6268f7714f3b772c23a796ab09538a8e68b4e4", size = 82555, upload-time = "2026-03-02T21:14:55.652Z" }, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, +] + +[[package]] +name = "namex" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649, upload-time = "2025-05-26T23:17:38.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905, upload-time = "2025-05-26T23:17:37.695Z" }, +] + +[[package]] +name = "napari" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "app-model" }, + { name = "appdirs" }, + { name = "cachey" }, + { name = "certifi" }, + { name = "dask", extra = ["array"] }, + { name = "imageio" }, + { name = "jsonschema" }, + { name = "lazy-loader" }, + { name = "magicgui" }, + { name = "napari-console" }, + { name = "napari-plugin-engine" }, + { name = "napari-svg" }, + { name = "npe2" }, + { name = "numpy" }, + { name = "numpydoc" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pint", version = "0.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pint", version = "0.25.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pygments" }, + { name = "pyopengl" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyyaml" }, + { name = "qtpy" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["data"], marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, extra = ["data"], marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "superqt" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "toolz" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "vispy" }, + { name = "wrapt", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/f1573c137aeccb86f8a57cae2470b28e22f55b8fa9fa5a2c2621ff1457de/napari-0.6.6.tar.gz", hash = "sha256:8e1adfc737c55c2a619689fad8d9e4d115582e56f09096cf771816b0ec75c3a7", size = 3251386, upload-time = "2025-10-16T09:25:58.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/d8/61c29378edc7e75aa5f877b3d1b851ca357cb47b8dd4b6268d07abf7d9d3/napari-0.6.6-py3-none-any.whl", hash = "sha256:c65cc13de1901ec2bf8d11c665e8df9f39e678d98b76cf690d1094e9decaca69", size = 3537483, upload-time = "2025-10-16T09:25:55.679Z" }, +] + +[[package]] +name = "napari-console" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "qtconsole" }, + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/6e1fcd9aa9ac4746ce2b44050ea8f7192d883f4d3da4e7ff08589ac3ad3b/napari_console-0.1.4.tar.gz", hash = "sha256:e185e4d36d8171ae23ca383dc69c38df76592a984d6c99ad08372d188a1fbb9b", size = 20152, upload-time = "2025-10-15T14:24:18.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/72/2067f28fd0ae87978f3b61e8ec30c1d085bbed03f64eb58e43949d526b3a/napari_console-0.1.4-py3-none-any.whl", hash = "sha256:565df1fa15db579552af9e9d9d3883067c00191be282ad47d80f9b0d50b4e5ad", size = 9786, upload-time = "2025-10-15T14:24:17.677Z" }, +] + +[[package]] +name = "napari-deeplabcut" +version = "0.3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask-image" }, + { name = "matplotlib" }, + { name = "napari" }, + { name = "natsort" }, + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pandas" }, + { name = "pyside6" }, + { name = "pyyaml" }, + { name = "qtpy" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "shiboken6" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/3e3956a7278c17ba4b263d24a270404cd782b1bcec3de6f860cbdeca057f/napari_deeplabcut-0.3.1.0.tar.gz", hash = "sha256:c4bc3a3643dd984f2045e59d5c06f299c7388fce1832ee7478ee0be7fddc5872", size = 1941735, upload-time = "2026-05-19T10:46:46.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/13/b43e8f1bc5d279178509c6b6929bb8c84ad16d73be5da0afc2452dc8d60a/napari_deeplabcut-0.3.1.0-py3-none-any.whl", hash = "sha256:84ee5c3cf8a2eecb0d8a59ec522d77e690987b77d33862a35b8dd4d9522253ac", size = 1194984, upload-time = "2026-05-19T10:46:44.313Z" }, +] + +[[package]] +name = "napari-plugin-engine" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/72/c653308edaf7f7c84d82e388a1c46bc8f26c385027af58b5bf728f600b47/napari_plugin_engine-0.2.1.tar.gz", hash = "sha256:46829cf02f368c8f2f1aa8b998ec73bcf14a2c1f5c15abd94b82154d7aef510d", size = 55809, upload-time = "2026-02-10T08:31:20.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/bc/2509813cddd0e02736121e21bef54deeec7f0f89af2c6096d753ee1feb09/napari_plugin_engine-0.2.1-py3-none-any.whl", hash = "sha256:de30babe6fd9477816f037207938a2da7faeddc0e9e8663cb29f3d74235b6dc5", size = 33849, upload-time = "2026-02-10T08:31:19.037Z" }, +] + +[[package]] +name = "napari-svg" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "vispy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/90/802f8288d16c1513b908d644779e733461a53b6c1a2c7561f1464c9f1516/napari_svg-0.2.1.tar.gz", hash = "sha256:031f13b34b0948afbdcb11eb00728fe32ef7e4e3aa3905f923001d6871a08ad9", size = 17533, upload-time = "2025-01-14T07:26:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ae/0eeb22806c4157a9199f65a93374e5ff5c4d2cc1411b5d25053bcd9e6b91/napari_svg-0.2.1-py3-none-any.whl", hash = "sha256:9eaa54fbbf9bfd5078b67b7d1edc9eccfd872dab89fd586374909fef4ed89a49", size = 16458, upload-time = "2025-01-14T07:26:29.328Z" }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[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 = "ndindex" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/92/4b9d2f4e0f3eabcfc7b02b48261f6e5ad36a3e2c1bbdcc4e3b7b6c768fa6/ndindex-1.10.1.tar.gz", hash = "sha256:0f6113c1f031248f8818cbee1aa92aa3c9472b7701debcce9fddebcd2f610f11", size = 271395, upload-time = "2025-11-19T20:40:08.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/71/aff23bd84111d038efdcdaea4d218b463a0b2129ff49f30613cbc6f535ff/ndindex-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8644c76e74c0fbbdaa54752de30b7c6b98b1e8f6c05f0c6228632a29c862d83f", size = 172022, upload-time = "2025-11-19T20:38:12.429Z" }, + { url = "https://files.pythonhosted.org/packages/99/a6/adcc17b685b24362983b00f965ee5c8607f74e7c68049a20facbd7ceb0b6/ndindex-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9a211ec2198994cb3600cd46adb335a740f27e4d406b40d48ed7b98d2d2a89b", size = 171057, upload-time = "2025-11-19T20:38:13.846Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/b0b1bde7818d2ccd5c288802c1f24b69705e03f3975bc948c005eccab25a/ndindex-1.10.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdb86a4176f2ae23bd4bcd0401ca35d5dad2d1ed0d0dca1ff64480ebe41b75d9", size = 498925, upload-time = "2025-11-19T20:38:17.214Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/55c3800048ef5310de542f188e1aad00e0b1d37713230c0eae980e88c895/ndindex-1.10.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ce3bd0882572269ca09285112cf38ce84baa2aaa5891551af968ca7c18f84bb", size = 495662, upload-time = "2025-11-19T20:38:20.026Z" }, + { url = "https://files.pythonhosted.org/packages/48/a4/0103c3ee3778d7079c3ff7dd879c79362afe3a7e9d3b8dcdaa25b49ca413/ndindex-1.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d6442ecce9b395aade5e9f2431e169e01393953a069f6d2d53a63b6c94d1d06", size = 1471263, upload-time = "2025-11-19T20:38:21.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/5a/eaa38b18757c3d8e7b2438faa5001a02f193b51a68a5558d6066f3c407e6/ndindex-1.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bada24abee6bc6ca438b2e6b68a752fc9b58b67bdcb54008e2bc6330ecb0a777", size = 1522878, upload-time = "2025-11-19T20:38:23.064Z" }, + { url = "https://files.pythonhosted.org/packages/a3/93/a40920c849fa128c9439bc3eb0add814696216dde235497eaa415f14d5e7/ndindex-1.10.1-cp310-cp310-win32.whl", hash = "sha256:bc236d1612714cbd80610cf25a6ef92584ff1402e9d5a5c50e926195716f7d22", size = 149268, upload-time = "2025-11-19T20:38:25.12Z" }, + { url = "https://files.pythonhosted.org/packages/85/d9/baf1655d0b2d36eb46134fddf7dd0ef0093203c9c91d17f8ce01b9060366/ndindex-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:4cea15cff221e76abd12e3e940c26124184735cf421c229307f5db6742e14dd7", size = 157151, upload-time = "2025-11-19T20:38:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d9/c94ab6151c9fdd199c2b560f23e3759a9fb86a7a1275855e0b97291bf05a/ndindex-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e2ad917bcdf8dc5ba1e21f01054c991d26862d4d01c3c203a50e907096d558ac", size = 172128, upload-time = "2025-11-19T20:38:28.977Z" }, + { url = "https://files.pythonhosted.org/packages/3a/34/880c4073750766e44492d51280d025f28e36475394ca3d741b0a4adad4b0/ndindex-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e851990a68937db5f485cd9f3e760c1fd47fa0f2a99f63a5e2cc880908faf3bb", size = 171423, upload-time = "2025-11-19T20:38:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1e/0342da55dabe4075efc2b2ab91a6a22ed3047c5bd511ef771a7a3f822c90/ndindex-1.10.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27385939f317b55773ea53f6bf9334810cf1d66206034c0a6a6f2a88f2001c3c", size = 519590, upload-time = "2025-11-19T20:38:32.464Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cb/7a02b6f29b15a16cd0002f4591d14493eff8e9236f7ca4c02ee4d4bcefbd/ndindex-1.10.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf3ca16efcdfbb8800aa88fbab1bc6528e6a0504bcb9cf7af4cb9d50e9f5d9", size = 516676, upload-time = "2025-11-19T20:38:34.276Z" }, + { url = "https://files.pythonhosted.org/packages/67/d5/38da808f968a54b0fead2d7e15ca011d3df93c96a07f4914e8ef3974506e/ndindex-1.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3307817bdc92846b18f309fae3582856f567dd6e0742fb0b41ac68682bfc4e2a", size = 1491141, upload-time = "2025-11-19T20:38:35.785Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/8c66ef982a01ae4cbdabba679a2bc711f262cedf23bfb9682293146f8a98/ndindex-1.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae73cd2d66b09ef2f2a7d7f93bad396d6abf168d1ee825e403c6c5fb8ae1341c", size = 1543876, upload-time = "2025-11-19T20:38:37.456Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/7c7e3a3c6e81b4284fd0d53cbaec51d9e5b90df26dd78e9bde06cb307217/ndindex-1.10.1-cp311-cp311-win32.whl", hash = "sha256:890bb92f0a779e6f16bdbcc8bd2e06c32bcc0239e5893ba246114eb924aecaaa", size = 149149, upload-time = "2025-11-19T20:38:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/99e1fb0effdef74b883be615ea0053ebcea28a53fd8b896263f4e99b0113/ndindex-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:1827a40301405b44ad709e388c5b48cf35cd90a67f77e63f0f17d87f6000fa81", size = 157246, upload-time = "2025-11-19T20:38:40.197Z" }, + { url = "https://files.pythonhosted.org/packages/65/90/774ddd08b2a1b41faa56da111f0fbfeb4f17ee537214c938ef41d61af949/ndindex-1.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87f83e8c35a7f49a68cd3a3054c406e6c22f8c1315f3905f7a778c657669187e", size = 177348, upload-time = "2025-11-19T20:38:41.768Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ee/a423e857f5b45da3adc8ddbcfbfd4a0e9a047edce3915d3e3d6e189b6bd9/ndindex-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf9e05986b2eb8c5993bce0f911d6cedd15bda30b5e35dd354b1ad1f4cc3599d", size = 176561, upload-time = "2025-11-19T20:38:43.06Z" }, + { url = "https://files.pythonhosted.org/packages/1f/40/139b6b050ba2b2a0bb40e0381a352b1eb6551302dcb8f86fb4c97dd34e92/ndindex-1.10.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046c1e88d46b2bd2fd3483e06d27b4e85132b55bc693f2fca2db0bb56eea1e78", size = 542901, upload-time = "2025-11-19T20:38:44.43Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/defd665dbbeb2fffa077491365ed160acaec49274ce8d4b979f55db71f18/ndindex-1.10.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03cf1e6cdac876bd8fc92d3b65bb223496b1581d10eab3ba113f7c195121a959", size = 546875, upload-time = "2025-11-19T20:38:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/6d54d48e8eaee25cdab70d3e4c4f579ddb0255e4f1660040d5ad55e029c6/ndindex-1.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:752e78a5e87911ded117c57a7246596f26c9c6da066de3c2b533b3db694949bb", size = 1510036, upload-time = "2025-11-19T20:38:47.444Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/e28ba3b98eacd18193176526526b34d7d70d2a6f9fd2b4d8309ab5692678/ndindex-1.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9dd58d91220b1c1fe516324bfcf4114566c98e84b1cbbe416abe345c75bd557", size = 1571849, upload-time = "2025-11-19T20:38:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/8f/63/83fff78a3712cb9f478dd84a19ec389acf6f8c7b01dc347a65ae74e6123d/ndindex-1.10.1-cp312-cp312-win32.whl", hash = "sha256:3b0d9ce2c8488444499ab6d40e92e09867bf4413f5cf04c01635de923f44aa67", size = 149792, upload-time = "2025-11-19T20:38:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/52/fd/a5e3c8c043d0dddea6cd4567bfaea568f022ac197301882b3d85d9c1e9b3/ndindex-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c026dbbf2455d97ce6456d8a50b349aee8fefa11027d020638c89e9be2c9c4c", size = 158164, upload-time = "2025-11-19T20:38:52.242Z" }, + { url = "https://files.pythonhosted.org/packages/60/ea/03676266cb38cc671679a9d258cc59bfc58c69726db87b0d6eeafb308895/ndindex-1.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:157b5c34a1b779f5d27b790d9bd7e7b156d284e76be83c591a3ba003984f4956", size = 176323, upload-time = "2025-11-19T20:38:53.528Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2d350439031b108b0bb8897cad315390c5ad88c14d87419a54c2ffa95c80/ndindex-1.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f99b3e89220da3244d03c9c5473669c7107d361c129fd9b064622744dee1ce15", size = 175584, upload-time = "2025-11-19T20:38:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/a51b7c6f7159718a6a0a694fc1058b94d793c416d9a4fd649f1924cce5f8/ndindex-1.10.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6928e47fb008903f2e41309b7ff1e59b16abbcd59e2e945454571c28b2433c9e", size = 524127, upload-time = "2025-11-19T20:38:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/d8f19f0b8fc9c5585b50fda44c05415da0bdc5fa9c9c69011015dac27880/ndindex-1.10.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69a2cb1ac7be955c3c77f1def83f410775a81525c9ce2d4c0a3f2a61589ed47", size = 528213, upload-time = "2025-11-19T20:39:00.882Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/77d9d037e871a3faa8579b354ca2dd09cc5bbf3e085d9e3c67f786d55ee3/ndindex-1.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cb76e0f3f235d8b1c768b17e771de48775d281713795c3aa045e8114ad61bdda", size = 1492172, upload-time = "2025-11-19T20:39:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/ad13676fc9312e0aa1a80a7c04bcb0b502b877ed4956136117ad663eced0/ndindex-1.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7da34a78410c14341d5fff73be5ce924bd36500bf7f640fc59b8607d3a0df95e", size = 1552614, upload-time = "2025-11-19T20:39:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/e6e6fd81423810c07ae623c4d36e099f42a812994977e8e3bfa182c02472/ndindex-1.10.1-cp313-cp313-win32.whl", hash = "sha256:9599fcb7411ffe601c367f0a5d4bc0ed588e3e7d9dc7604bdb32c8f669456b9e", size = 149330, upload-time = "2025-11-19T20:39:05.727Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d3/830a20626e2ec0e31a926be90e67068a029930f99e6cfebf2f9768e7b7b1/ndindex-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:ef3ef22390a892d16286505083ee5b326317b21c255a0c7f744b1290a0b964a6", size = 157309, upload-time = "2025-11-19T20:39:07.394Z" }, + { url = "https://files.pythonhosted.org/packages/4a/73/3bdeecd1f6ec0ad81478a53d96da4ba9be74ed297c95f2b4fbe2b80843e1/ndindex-1.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:72af787dcee3661f36fff9d144d989aacefe32e2c8b51ceef9babd46afb93a18", size = 181022, upload-time = "2025-11-19T20:39:10.487Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b1/0d97ba134b5aa71b5ed638fac193a7ec4d987e091e2f4e4162ebdaacbda1/ndindex-1.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa60637dfae1ee3fc057e420a52cc4ace38cf2c0d1a0451af2a3cba84d281842", size = 181289, upload-time = "2025-11-19T20:39:11.793Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d7/1df02df24880ce3f3c8137b6f3ca5a901a58d9079dcfd8c818419277ff87/ndindex-1.10.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ebdba2fade3f6916fe21fd49e2a0935af4f58c56100a60f3f2eb26e20baee7", size = 632517, upload-time = "2025-11-19T20:39:13.259Z" }, + { url = "https://files.pythonhosted.org/packages/34/96/b509c2b14e9b10710fe6ab6ba8bda1ee6ce36ab16397ff2f5bbb33bbbba3/ndindex-1.10.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:346a4bf09f5771548665c8206e81daadb6b9925d409746e709894bdd98adc701", size = 616179, upload-time = "2025-11-19T20:39:14.757Z" }, + { url = "https://files.pythonhosted.org/packages/38/e3/f89d60cf351c33a484bf1a4546a5dee6f4e7a6a973613ffa12bd316b14ad/ndindex-1.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:23d35696f802548143b5cc199bf2f171efb0061aa7934959251dd3bae56d038c", size = 1588373, upload-time = "2025-11-19T20:39:16.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/002fc1e6a4abeef8d92e9aa2e43aea4d462f6b170090f7752ea8887f4897/ndindex-1.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a91e1a0398120233d5c3b23ccb2d4b78e970d66136f1a7221fa9a53873c3d5c5", size = 1636436, upload-time = "2025-11-19T20:39:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/28b1ad78c787ac8fafd6e26419a80366617784b1779e3857fa687492f6bc/ndindex-1.10.1-cp313-cp313t-win32.whl", hash = "sha256:78bfe25941d2dac406391ddd9baf0b0fce163807b98ecc2c47a3030ee8466319", size = 158780, upload-time = "2025-11-19T20:39:20.454Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/b81060607a19865bb8be8d705b1b3e8aefb8747c0fbd383e38b4cae4bd71/ndindex-1.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:08bfdc1f7a0b408d15b3ce61d141ebbebdb47a25341967e425e104c5bd512a5c", size = 167485, upload-time = "2025-11-19T20:39:21.733Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/aac1131e9f3a5635ba7b0312c3bfa610511ab4108f85c0d914a32887aa00/ndindex-1.10.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b5297f207ebc068c7cdf9e3cd7b95aa5c9ec04295d0a7e56b529f66787d4685", size = 176478, upload-time = "2025-11-19T20:39:23.747Z" }, + { url = "https://files.pythonhosted.org/packages/1a/05/a0d8ca0432c84550bc17af6d6479a803936895b8b8403a1216c5a55475fb/ndindex-1.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c5e9762452b163e33cfb6e821f86e45ba0b53bdfcd23ab5d57b48a8f566898cb", size = 175480, upload-time = "2025-11-19T20:39:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/09/4a/028ab78a9f29fd2a7e86a90337cde4658eaa77b425c63045d83a1d2e4f26/ndindex-1.10.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf80241b40adffdc3276b2c9fb63a96c6c98b4a9d941892738de8add65083962", size = 528125, upload-time = "2025-11-19T20:39:26.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/bd823b345fb06c83ade6ef1c1933521d4357cd04490e684d4fa30126926c/ndindex-1.10.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf5855881884b8467dfcf45764ccf2e4279075be14b155b89c96994bb08d2e6f", size = 527328, upload-time = "2025-11-19T20:39:28.292Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/40b9c15588cbf9dde43c4fb88a31dd1f636a913fa29649f18f8e3ebca36a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e81a9bd36fe054b6c9fcc53d26bc9a28cf15d1ab52a0f5b854f894116f3a54e1", size = 1497508, upload-time = "2025-11-19T20:39:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/24/8f/b8048f7837d2e9dff0af507b398307fa84a2aa9ea3db71b4aa800b21da4a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:588e8875d836a93b3cd9af482c8074bb02288ae1aff92cf277e1f02d9ae0f992", size = 1552625, upload-time = "2025-11-19T20:39:32.404Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/0ecb53c7e690a44769f2f92a843723ccb1d0ce080d93ba1ea811304cca12/ndindex-1.10.1-cp314-cp314-win32.whl", hash = "sha256:28741daca5926adff402247cd406f453ed5bb6042e82d6855938f805190e5ce9", size = 151237, upload-time = "2025-11-19T20:39:34.847Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4e/197982fa8b4e6e6b9d15c38505c41076d1c552921f09f4d35acbbbbc0b70/ndindex-1.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:59a3222befc0f7cdc85fb9b90a567ae890f70a864bdeb660517e9ebcb36bf1bc", size = 158925, upload-time = "2025-11-19T20:39:37.149Z" }, + { url = "https://files.pythonhosted.org/packages/24/ad/116b6154046a69fc04e2d4490905801d3839a3f21290c0b4d49b1044e251/ndindex-1.10.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967b87b88dadb62555ec1039695c347254eccb8ca3d124c0e5dbe084c525fa93", size = 181724, upload-time = "2025-11-19T20:39:38.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/00/3ce4351366c890bcc87a5e9f1f90102547962eef356ac7c799bfdd0dddce/ndindex-1.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c67dde588c0fb89d872931a4ed5f9b4d21c1c70a3d92fdf0812a1de154239816", size = 181653, upload-time = "2025-11-19T20:39:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/a6fda696a2f02a3f8dd2ee9d816cb2edff6423bf0110a4876cc3b1259732/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65ca639a7abf72d79f22424f4abd18dece1f289a2b7b028a0ca455edd2168d4", size = 630898, upload-time = "2025-11-19T20:39:41.495Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/eb2e5d067d4c054451e33eaece74cbdcb58236dc60516e73d783dae34c7e/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3634a8df43e7928122225a3d64d850c8957bd1edf2e403907deacb478af27b", size = 614419, upload-time = "2025-11-19T20:39:43.254Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/261bfb49eb7920c2a7314cacba5821930a529911dce48c7c6cd786096a5a/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d581f931e61f182478f18bdf5edd3955899df5da4892ed0d5de547a4cfd5b6f", size = 1587517, upload-time = "2025-11-19T20:39:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/ec/37/084a332ecdf8b0049151bd78001a7baf2daf7f500d043beb8a1f95d0f4e3/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:78ce45106ebf67aeba99714818c721d8fd5fb9534daebd2565665a2d64b50fc9", size = 1635372, upload-time = "2025-11-19T20:39:47.231Z" }, + { url = "https://files.pythonhosted.org/packages/28/f4/716580fbb03018ab1daa86ed12c1925c67e79689db5fee82393e840758a2/ndindex-1.10.1-cp314-cp314t-win32.whl", hash = "sha256:fe5341e24dc992b09c258456ac90a09a6d25efdc2cb86dcc91d32c8891e1df9a", size = 162186, upload-time = "2025-11-19T20:39:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/4d/20/28f669c09a470e7f523b0cc10b94336664d9648594015e3f2a1ec29047b1/ndindex-1.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:37f87f0e7690ae0324334740e0661d6297f2e62c9bf925127d249fb7eddd0ad8", size = 171077, upload-time = "2025-11-19T20:39:50.108Z" }, +] + +[[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.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "npe2" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "platformdirs" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pydantic-extra-types" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tomli-w" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/0a/1a6ed44b038484a064a0ac8e5f96aae6ddb8768789145cf7e65e699b324b/npe2-0.8.2.tar.gz", hash = "sha256:6e41cc1f2c873257d864980dd281b5bb649a84cef02feeb0cdda1a9d23fd8f8b", size = 122768, upload-time = "2026-04-04T20:55:11.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a6/f318fbc7bbb62361d36cb694f399f765f57fb282fdadc3aa2fef4dafd62d/npe2-0.8.2-py3-none-any.whl", hash = "sha256:80eff5fef50352cf0f3407c4c1122a7ae186aede40a21fd595cf91988cd55a9d", size = 93921, upload-time = "2026-04-04T20:55:10.552Z" }, +] + +[[package]] +name = "numba" +version = "0.65.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1b/3c5a7daf683a95465bf23504bcd1a2d5db8cd5e5e276ca87505d020dffe9/numba-0.65.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9d993ed0a257aa4116e6f553f114004bcfdee540c7276ab8ea48f650d514c452", size = 2680870, upload-time = "2026-04-24T02:02:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/1831836814018a898e7d252aebe09c0f3ce1f26d145b68264b4ae0be6822/numba-0.65.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f098109f361681e57295f7e84d8ab2426902539a141811de0703ace52826981", size = 3739780, upload-time = "2026-04-24T02:02:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/a813ddc81def09e257d2b1f67521982ce4b06204a87268796ffc8187271c/numba-0.65.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973fd8173f2312815e6b7aaae887c4ce8a817eeff46a4f8840b828305b75bc95", size = 3446722, upload-time = "2026-04-24T02:02:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/ee1d8b3becda384fe0552221641e05aa668a35e8a77470db4db7f6475000/numba-0.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:c63aa0c4193694026452da55d0ef9d85156c1a7a333454c103bb30dec81b7bf8", size = 2747539, upload-time = "2026-04-24T02:02:16.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7020d74b19cdb8cff16506542fdd510756e28c5e7f3bd0b7f574f0f42272fcd9", size = 2680563, upload-time = "2026-04-24T02:02:18.414Z" }, + { url = "https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420", size = 3745018, upload-time = "2026-04-24T02:02:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/49/aa/4361698f35bf63bff67dfe6c90493731177f48ede954f77b0588731537bc/numba-0.65.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ed425a43b0a5f9772f2f4e2dd0bbd12eabecae1af0b24efcfd4e053f012aac6", size = 3450962, upload-time = "2026-04-24T02:02:22.449Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:df40a5028a975b9ea66f6a2a3f7abbdbd541a863070e34ed367aff21141248e4", size = 2747417, upload-time = "2026-04-24T02:02:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac3f1e77c352dd0ea9712732c2d8f9ca507717435eec5b5013bf138ac33c4a08", size = 2681371, upload-time = "2026-04-24T02:02:26.105Z" }, + { url = "https://files.pythonhosted.org/packages/69/47/a415af0283e4db0398104c6d1c11c9861a98dc67a7aa442a7769ed5d6196/numba-0.65.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52bc6f3ceb8fcaff9b2ae26b4c6b1e9fee39db8d355534c0fe4f39a901246b84", size = 3802467, upload-time = "2026-04-24T02:02:27.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/36/246f73ec99cfeab2f2cb2ce7d4218766cc36a2da418901223f4f4da9c813/numba-0.65.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ca10b3463bae0bd70589726fe3c77d01d6b5fc86bee54bcdf9fb6b47c28977", size = 3502628, upload-time = "2026-04-24T02:02:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/db/9e/3c679b2ee078425b9e99a91e44f8d132a6830d8ccce5227bc5e9181aeed8/numba-0.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:5971c632be2a2351500431f46213821dba8d02b18a9f7d02fd36bd2743e41a6a", size = 2750611, upload-time = "2026-04-24T02:02:31.477Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3", size = 2681344, upload-time = "2026-04-24T02:02:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, +] + +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/91/ccd504cbe5b88d06987c77f42ba37a13ef05065fdab4afe6dcfeb2961faf/numexpr-2.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0fab3fd06a04f6b86102552b26aa5d85e20ac7d8296c15764c726eeabae6cc8", size = 163200, upload-time = "2025-10-13T16:16:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/f3/89/6b07977baf2af75fb6692f9e7a1fb612a15f600fc921f3f565366de01f4a/numexpr-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:64ae5dfd62d74a3ef82fe0b37f80527247f3626171ad82025900f46ffca4b39a", size = 152085, upload-time = "2025-10-13T16:16:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/c2/c5775541256c4bf16b4d88fa1cffa74a0126703e513093c8774d911b0bb7/numexpr-2.14.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:955c92b064f9074d2970cf3138f5e3b965be673b82024962ed526f39bc25a920", size = 449435, upload-time = "2025-10-13T16:13:16.257Z" }, + { url = "https://files.pythonhosted.org/packages/34/d4/d1a410901c620f7a6a3c5c2b1fc9dab22170be05a89d2c02ae699e27bd3f/numexpr-2.14.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75440c54fc01e130396650fdf307aa9d41a67dc06ddbfb288971b591c13a395b", size = 440197, upload-time = "2025-10-13T16:14:44.109Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c8/fa85f0cc5c39db587ba4927b862a92477c017ee8476e415e8120a100457b/numexpr-2.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dde9fa47ed319e1e1728940a539df3cb78326b7754bc7c6ab3152afc91808f9b", size = 1414125, upload-time = "2025-10-13T16:13:19.882Z" }, + { url = "https://files.pythonhosted.org/packages/08/72/a58ddc05e0eabb3fa8d3fcd319f3d97870e6b41520832acfd04a6734c2c0/numexpr-2.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76db0bc6267e591ab9c4df405ffb533598e4c88239db7338d11ae9e4b368a85a", size = 1463041, upload-time = "2025-10-13T16:14:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/bdd1862302bb71a78dba941eaf7060e1274f1cf6af2d1b0f1880bfcb289b/numexpr-2.14.1-cp310-cp310-win32.whl", hash = "sha256:0d1dcbdc4d0374c0d523cee2f94f06b001623cbc1fd163612841017a3495427c", size = 166833, upload-time = "2025-10-13T16:17:03.543Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/26773a246716922794388786529e5640676399efabb0ee217ce034df9d27/numexpr-2.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:823cd82c8e7937981339f634e7a9c6a92cb2d0b9d0a5cf627a5e394fffc05377", size = 160068, upload-time = "2025-10-13T16:17:05.191Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195, upload-time = "2025-10-13T16:16:31.212Z" }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088, upload-time = "2025-10-13T16:16:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838, upload-time = "2025-10-13T16:17:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069, upload-time = "2025-10-13T16:17:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794, upload-time = "2025-10-13T16:16:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203, upload-time = "2025-10-13T16:16:39.907Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860, upload-time = "2025-10-13T16:13:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574, upload-time = "2025-10-13T16:15:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306, upload-time = "2025-10-13T16:13:42.813Z" }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145, upload-time = "2025-10-13T16:15:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996, upload-time = "2025-10-13T16:17:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189, upload-time = "2025-10-13T16:17:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524, upload-time = "2025-10-13T16:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919, upload-time = "2025-10-13T16:16:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972, upload-time = "2025-10-13T16:13:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953, upload-time = "2025-10-13T16:15:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199, upload-time = "2025-10-13T16:13:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "numpydoc" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/45/9e/2f562daf80eb8f7a685fb7bea4fda71f6048e4f359d6fdd1b6e70206cb2f/nvidia_cublas-13.1.1.3-py3-none-win_amd64.whl", hash = "sha256:b6cdce694e47ff6aadf0a69df1cab6628d696f5ff56e8d16af50309d855fa20f", size = 404358158, upload-time = "2026-04-08T18:47:26.987Z" }, +] + +[[package]] +name = "nvidia-cublas-cu11" +version = "11.10.3.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/41/fdeb62b5437996e841d83d7d2714ca75b886547ee8017ee2fe6ea409d983/nvidia_cublas_cu11-11.10.3.66-py3-none-manylinux1_x86_64.whl", hash = "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded", size = 317097917, upload-time = "2022-08-11T17:32:30.789Z" }, + { url = "https://files.pythonhosted.org/packages/7a/08/57e6b6481af73590259a9600c32a68eb853966e354fca147cde17ed9ea27/nvidia_cublas_cu11-11.10.3.66-py3-none-win_amd64.whl", hash = "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e", size = 311065222, upload-time = "2022-08-03T21:16:08.044Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/61/7d7b3c70186fb651d0fbd35b01dbfc8e755f69fd58f817f3d0f642df20c3/nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af", size = 567544208, upload-time = "2025-03-07T01:53:30.535Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/b74b10025c1205695c5676373f2edd3e87a7202cc62ead0dfbc373b0f6ea/nvidia_cuda_cupti-13.0.85-py3-none-win_amd64.whl", hash = "sha256:683f58d301548deeefcb8f6fac1b8d907691b9d8b18eccab417f51e362102f00", size = 7736776, upload-time = "2025-09-04T08:38:08.38Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu11" +version = "11.7.101" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/9d/dd0cdcd800e642e3c82ee3b5987c751afd4f3fb9cc2752517f42c3bc6e49/nvidia_cuda_cupti_cu11-11.7.101-py3-none-manylinux1_x86_64.whl", hash = "sha256:e0cfd9854e1f2edaa36ca20d21cd0bdd5dcfca4e3b9e130a082e05b33b6c5895", size = 11845698, upload-time = "2022-08-03T20:58:36.733Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/f314475cc4740b6138daf9c6496b165bab1f07c161bd4ac5e69285ab07d6/nvidia_cuda_cupti_cu11-11.7.101-py3-none-win_amd64.whl", hash = "sha256:7cc5b8f91ae5e1389c3c0ad8866b3b016a175e827ea8f162a672990a402ab2b0", size = 8461264, upload-time = "2022-08-03T21:14:45.554Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/83f5426095d93694ae39fe1311431b5d5a9bb82e48bf0dd8e19be2765942/nvidia_cuda_cupti_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e", size = 7015759, upload-time = "2025-03-07T01:51:11.355Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/4a/af/345fedb9f4c76c84ab4fa445b36bd4048a4d9db60e6bc76b4f913ff4b852/nvidia_cuda_nvrtc-13.0.88-py3-none-win_amd64.whl", hash = "sha256:6bcd4e7f8e205cbe644f5a98f2f799bef9556fefc89dd786e79a16312ce49872", size = 76807835, upload-time = "2025-09-04T08:39:15.274Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu11" +version = "11.7.99" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/25/922c5996aada6611b79b53985af7999fc629aee1d5d001b6a22431e18fec/nvidia_cuda_nvrtc_cu11-11.7.99-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03", size = 21011023, upload-time = "2022-09-21T23:12:53.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8d/0709ba16c2831c17ec1c2ea1eeb89ada11ffa8d966d773cce0a7463b22bb/nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7", size = 21010447, upload-time = "2022-08-03T20:59:13.991Z" }, + { url = "https://files.pythonhosted.org/packages/9e/10/c9fc448f33d439981d6a74b693526871c4ef13e8d81a7b4de12e3a12a1b9/nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3", size = 17040212, upload-time = "2022-08-03T21:15:19.801Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/45/51/52a3d84baa2136cc8df15500ad731d74d3a1114d4c123e043cb608d4a32b/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909", size = 73586838, upload-time = "2025-03-07T01:52:13.483Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/b7/94/6b867483bec07da24ffa32736c79fabb94ef3a7af4d787a9d4a974868576/nvidia_cuda_runtime-13.0.96-py3-none-win_amd64.whl", hash = "sha256:f79298c8a098cec150a597c8eba58ecdab96e3bdc4b9bc4f9983635031740492", size = 2927037, upload-time = "2025-10-09T09:04:23.782Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu11" +version = "11.7.99" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/92/89cf558b514125d2ebd8344dd2f0533404b416486ff681d5434a5832a019/nvidia_cuda_runtime_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31", size = 849253, upload-time = "2022-08-03T20:58:27.979Z" }, + { url = "https://files.pythonhosted.org/packages/32/2c/d89ea2b4051fbabff8d2edda8c735dabae6d5d1b8d5215f9749d38dcdb72/nvidia_cuda_runtime_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7", size = 991354, upload-time = "2022-08-03T21:14:37.958Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/a515b7600ad361ea14bfa13fb4d6687abf500adc270f19e89849c0590492/nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8", size = 944318, upload-time = "2025-03-07T01:51:01.794Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu11" +version = "8.5.0.96" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/30/66d4347d6e864334da5bb1c7571305e501dcb11b9155971421bb7bb5315f/nvidia_cudnn_cu11-8.5.0.96-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7", size = 557141533, upload-time = "2022-09-21T21:55:09.845Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/4d28d4706946f89fffe3f87373a079ae95dc17f9c0fcd840fe570c67e36b/nvidia_cudnn_cu11-8.5.0.96-py3-none-manylinux1_x86_64.whl", hash = "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108", size = 557140881, upload-time = "2022-08-10T00:14:42.613Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/3d/90/0bd6e586701b3a890fd38aa71c387dab4883d619d6e5ad912ccbd05bfd67/nvidia_cudnn_cu12-9.10.2.21-py3-none-win_amd64.whl", hash = "sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e", size = 692992268, upload-time = "2025-06-06T21:55:18.114Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/78/39/21507455b1bca8b5702a9e9fc6ce73735f216f558dac2c9ede58e4d456b8/nvidia_cudnn_cu13-9.20.0.48-py3-none-win_amd64.whl", hash = "sha256:af8139732b99c0118be65ea5aac97f0d46018f8c552889e49d2fb0c6261a4a24", size = 350712614, upload-time = "2026-03-09T19:31:11.398Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/f8af21a2ed1beed337a6a02c5a28aeb85441f4d578ec3d529543c775ea4b/nvidia_cufft-12.0.0.61-py3-none-win_amd64.whl", hash = "sha256:2abce5b39d2f5ae12730fb7e5db6696533e36c26e2d3e8fd1750bdd2853364eb", size = 213342123, upload-time = "2025-09-04T08:40:51.145Z" }, +] + +[[package]] +name = "nvidia-cufft-cu11" +version = "10.9.0.58" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/79/b912a77e38e41f15a0581a59f5c3548d1ddfdda3225936fb67c342719e7a/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux1_x86_64.whl", hash = "sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81", size = 168405414, upload-time = "2022-10-03T23:29:47.505Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/a2ad9951d57c3cc23f4fa6d84b146afd9f375ffbc744b38935930ac4393f/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux2014_aarch64.whl", hash = "sha256:34b7315104e615b230dc3c2d1861f13bff9ec465c5d3b4bb65b4986d03a1d8d4", size = 111231060, upload-time = "2024-08-17T00:00:57.04Z" }, + { url = "https://files.pythonhosted.org/packages/64/c8/133717b43182ba063803e983e7680a94826a9f4ff5734af0ca315803f1b3/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e21037259995243cc370dd63c430d77ae9280bedb68d5b5a18226bfc92e5d748", size = 168405419, upload-time = "2024-08-17T00:02:03.562Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b4/e432a74f8db0e84f734dc14d36c0e529225132bf7e239da21f55893351a6/nvidia_cufft_cu11-10.9.0.58-py3-none-win_amd64.whl", hash = "sha256:c4d316f17c745ec9c728e30409612eaf77a8404c3733cdf6c9c1569634d1ca03", size = 172237004, upload-time = "2022-10-03T23:39:58.288Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/ce1629f1e478bb5ccd208986b5f9e0316a78538dd6ab1d0484f012f8e2a1/nvidia_cufft_cu12-11.3.3.83-py3-none-win_amd64.whl", hash = "sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7", size = 192216559, upload-time = "2025-03-07T01:53:57.106Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, + { url = "https://files.pythonhosted.org/packages/99/27/72103153b1ffc00e09fdc40ac970235343dcd1ea8bd762e84d2d73219ffa/nvidia_curand-10.4.0.35-py3-none-win_amd64.whl", hash = "sha256:65b1710aa6961d326b411e314b374290904c5ddf41dc3f766ebc3f1d7d4ca69f", size = 55242481, upload-time = "2025-08-04T10:30:41.831Z" }, +] + +[[package]] +name = "nvidia-curand-cu11" +version = "10.2.10.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/11/af78d54b2420e64a4dd19e704f5bb69dcb5a6a3138b4465d6a48cdf59a21/nvidia_curand_cu11-10.2.10.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:eecb269c970fa599a2660c9232fa46aaccbf90d9170b96c462e13bcb4d129e2c", size = 54628716, upload-time = "2022-08-03T21:13:08.944Z" }, + { url = "https://files.pythonhosted.org/packages/45/76/b98f30e058c9bbd9a56eb9b1102b9aab775704bad9286bf8e3998147e2e9/nvidia_curand_cu11-10.2.10.91-py3-none-win_amd64.whl", hash = "sha256:f742052af0e1e75523bde18895a9ed016ecf1e5aa0ecddfcc3658fd11a1ff417", size = 54342083, upload-time = "2022-08-03T21:17:21.537Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, + { url = "https://files.pythonhosted.org/packages/b9/75/70c05b2f3ed5be3bb30b7102b6eb78e100da4bbf6944fd6725c012831cab/nvidia_curand_cu12-10.3.9.90-py3-none-win_amd64.whl", hash = "sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec", size = 62765309, upload-time = "2025-03-07T01:54:20.478Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "nvidia-cusparse", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, + { url = "https://files.pythonhosted.org/packages/99/ef/332a0101260ca78a1daef046bf0b06199e8ed4dac1d2aa698289c358169c/nvidia_cusolver-12.0.4.66-py3-none-win_amd64.whl", hash = "sha256:16515bd33a8e76bb54d024cfa068fa68d30e80fc34b9e1090813ea9362e0cb65", size = 193551444, upload-time = "2025-09-04T08:41:46.813Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu11" +version = "11.4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/77/66149e3153b19312fb782ea367f3f950123b93916a45538b573fe373570a/nvidia_cusolver_cu11-11.4.0.1-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:72fa7261d755ed55c0074960df5904b65e2326f7adce364cbe4945063c1be412", size = 102594907, upload-time = "2022-09-21T23:13:22.001Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/272f9aa7838e545b47878e4aec4f09b0fecf17dbd312cf5c5dc398b0637f/nvidia_cusolver_cu11-11.4.0.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:700b781bfefd57d161443aff9ace1878584b93e0b2cfef3d6e9296d96febbf99", size = 102592389, upload-time = "2022-08-03T21:13:24.499Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/f682b0b001562d16664bd7b015165cf2c2d392a8d0506472f28b2833953e/nvidia_cusolver_cu11-11.4.0.1-py3-none-win_amd64.whl", hash = "sha256:00f70b256add65f8c1eb3b6a65308795a93e7740f6df9e273eccbba770d370c4", size = 100095353, upload-time = "2022-08-03T21:17:36.992Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/76ca8551b8a84146ffa189fec81c26d04adba4bc0dbe09cd6e6fd9b7de04/nvidia_cusolver_cu12-11.7.3.90-py3-none-win_amd64.whl", hash = "sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34", size = 256720438, upload-time = "2025-03-07T01:54:39.898Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, + { url = "https://files.pythonhosted.org/packages/02/b0/b043d6f3480f102f885cf87fc3ffd3edcb5e23b855025a50e2ef4d059185/nvidia_cusparse-12.6.3.3-py3-none-win_amd64.whl", hash = "sha256:cbcf42feb737bd7ec15b4c0a63e62351886bd3f975027b8815d7f720a2b5ea79", size = 143783033, upload-time = "2025-09-04T08:42:12.391Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu11" +version = "11.7.4.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/6f/6d032cc1bb7db88a989ddce3f4968419a7edeafda362847f42f614b1f845/nvidia_cusparse_cu11-11.7.4.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:a3389de714db63321aa11fbec3919271f415ef19fda58aed7f2ede488c32733d", size = 173182291, upload-time = "2022-08-03T21:13:55.407Z" }, + { url = "https://files.pythonhosted.org/packages/15/3e/d32da819d918b0b9ef3fa89ed8238d2c3b8e315ac32441229783a4b0c4ce/nvidia_cusparse_cu11-11.7.4.91-py3-none-win_amd64.whl", hash = "sha256:304a01599534f5186a8ed1c3756879282c72c118bc77dd890dc1ff868cad25b9", size = 172505954, upload-time = "2022-08-03T21:18:07.425Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/62/07/f3b2ad63f8e3d257a599f422ae34eb565e70c41031aecefa3d18b62cabd1/nvidia_cusparse_cu12-12.5.8.93-py3-none-win_amd64.whl", hash = "sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd", size = 284937404, upload-time = "2025-03-07T01:55:07.742Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d8/a6b0d0d0c2435e9310f3e2bb0d9c9dd4c33daef86aa5f30b3681defd37ea/nvidia_cusparselt_cu12-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075", size = 271020911, upload-time = "2025-02-26T00:14:47.204Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/31/83/f3647ce26916c94a6ca4ff1810623e2c405cff2dea6e78d29516b2514df9/nvidia_cusparselt_cu13-0.8.1-py3-none-win_amd64.whl", hash = "sha256:dccbd362f91a7b9024d1f55ee9f548ac065027ff15d8c8b0db889ab3a8f31215", size = 156885108, upload-time = "2025-09-05T18:51:35.958Z" }, +] + +[[package]] +name = "nvidia-nccl-cu11" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/92/914cdb650b6a5d1478f83148597a25e90ea37d739bd563c5096b0e8a5f43/nvidia_nccl_cu11-2.14.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:5e5534257d1284b8e825bc3a182c6f06acd6eb405e9f89d49340e98cd8f136eb", size = 177099966, upload-time = "2022-08-31T23:13:15.915Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, + { url = "https://files.pythonhosted.org/packages/e4/01/07530b0e37546231052e30234540289c42eaffa486f1a34a87fed340157b/nvidia_nvjitlink-13.0.88-py3-none-win_amd64.whl", hash = "sha256:634e96e3da9ef845ae744097a1f289238ecf946ce0b82e93cdce14b9782e682f", size = 36035115, upload-time = "2025-09-04T08:43:03.001Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d7/34f02dad2e30c31b10a51f6b04e025e5dd60e5f936af9045a9b858a05383/nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f", size = 268553710, upload-time = "2025-03-07T01:56:24.13Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d2/50/0e2220f8620a177de994211186ffc5bfa9f2ce1e1282797f8f90096f9f88/nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519", size = 137066, upload-time = "2025-09-04T08:39:25.649Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu11" +version = "11.7.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/d5/09493ff0e64fd77523afbbb075108f27a13790479efe86b9ffb4587671b5/nvidia_nvtx_cu11-11.7.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:b22c64eee426a62fc00952b507d6d29cf62b4c9df7a480fcc417e540e05fd5ac", size = 98579, upload-time = "2022-08-03T20:59:22.605Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e1/37b5a5da8ec3594890356e9d60617feb36cfea1223ac511a78c615870916/nvidia_nvtx_cu11-11.7.91-py3-none-win_amd64.whl", hash = "sha256:dfd7fcb2a91742513027d63a26b757f38dd8b07fecac282c4d132a9d373ff064", size = 65886, upload-time = "2022-08-03T21:15:27.865Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/9f/99/4c9c0c329bf9fc125008c3b54c7c94c0023518d06fc025ae36431375e1fe/nvidia_nvtx_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e", size = 56492, upload-time = "2025-03-07T01:52:24.69Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, +] + +[[package]] +name = "openvino-dev" +version = "2022.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/99/9e55ddb1abce5ff7def768470810044a6de48a5da99b9195498ad75dfe8a/openvino_dev-2022.1.0-7019-py3-none-any.whl", hash = "sha256:3d911b31a89e92dcb92a29026cfb396920211aca9525ea50032419f10169098d", size = 5774377, upload-time = "2022-03-22T13:07:17.333Z" }, +] + +[[package]] +name = "opt-einsum" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, +] + +[[package]] +name = "optree" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531, upload-time = "2026-05-06T02:32:39.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/48/53367634a0ab6c2f0e502d83f8d6e27b70b6848ff1e1ff9cf042d1e1f1a0/optree-0.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1b28b0d89def1b4554051f3de2a1ed81e20216b6454a59a0d16c9f55c08cff77", size = 398400, upload-time = "2026-05-06T02:30:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/350c82cd77a510f0f495e38a6f333b4b45a413dbc224142bc59975bc09d6/optree-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14f959bc6bea6e0532f9239c67ea6952f3b8d0755ea9b4dd498284b649275aba", size = 370049, upload-time = "2026-05-06T02:30:33.065Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/81b660daea2a75f574549e62c198d0b4e8e148b5de6f5f72e90a5cc1c334/optree-0.19.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1687c962bb1691525178a6e90dde5840197cd7a7ad914b407eb7b635f15d47cb", size = 390143, upload-time = "2026-05-06T02:30:34.585Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ec/ee009b5a31227b089d72fec2af3bb6bc0efd95bbe87ffe46f11061b9d371/optree-0.19.1-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d7edead66cace8b3b905488e391b38487614f75ae4fa7f3b612c7fe0e54b8a90", size = 445740, upload-time = "2026-05-06T02:30:35.891Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/2fe8ac73b7e979f3ed477ad99b7e034a11207d728b84ed2f52da259e7cda/optree-0.19.1-cp310-cp310-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9fb767746231ff279d273e8ff71af2a8f89c0c3870ca367c45fd4526d331ae4b", size = 446631, upload-time = "2026-05-06T02:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/489fb272de36e0233149d46887879deb9497edc4a0214674bd2a80b8d4ec/optree-0.19.1-cp310-cp310-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75fe038a1bed44f487a084af7a978874c51bba55f850bc12bf8068f3242463d6", size = 442053, upload-time = "2026-05-06T02:30:38.24Z" }, + { url = "https://files.pythonhosted.org/packages/90/09/1f0bc2b584a51702407592bbccfe2b404187f6f5ee5b4b0c112a73e1a7ec/optree-0.19.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fff5fd89a9b333d91a05a7ca2e66c8e6632d0bdbc94c1725a341b77001f09511", size = 425490, upload-time = "2026-05-06T02:30:39.432Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f4/d8685b55323c1f42695c1ed647d6541ee9c289eb821abc6e0cb84b0e4f72/optree-0.19.1-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d4bac18638fa56efd2377cf8c43e17cd083aa566e69a31ce10f7fdaefd9676a3", size = 390552, upload-time = "2026-05-06T02:30:40.506Z" }, + { url = "https://files.pythonhosted.org/packages/07/84/ee12e234ddcf4fd4b7893ce03ec37f3c3edabdac911fd5384aa3f5c04c05/optree-0.19.1-cp310-cp310-win32.whl", hash = "sha256:ef2409d4efda1c5a6eb69f83ffff89fb04d5607fd056704552ec359fb865cd6c", size = 303117, upload-time = "2026-05-06T02:30:41.61Z" }, + { url = "https://files.pythonhosted.org/packages/91/b5/4e23965aacae04eb4cf42cd8108405a6628e645ee3ab759277e03063af0e/optree-0.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:01c88294235b118b7478b5e80d360e5f110977cdf79f84d61dae21c2eb1d4cdd", size = 327866, upload-time = "2026-05-06T02:30:42.664Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/4671a78193f96e86c1343fe04324091e163973d0058b292c10bc3387bb70/optree-0.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a496b864fe1fe0b5ea23d1ee3d1ef958d910704661808db2b2d2e16a0cfac96c", size = 414314, upload-time = "2026-05-06T02:30:43.812Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/7decea24656f416d61fa57b7113b1fbdbc042b7ab421399a84e1755676a1/optree-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1667e502e0eda9477925fb17c2ad879b199a2283ac98f18e6453692819b7811", size = 385006, upload-time = "2026-05-06T02:30:44.895Z" }, + { url = "https://files.pythonhosted.org/packages/af/2e/9d1bd2527481681c4399beeeabba11dca36b16ec814579f2e8cc6bc2af96/optree-0.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:42e367a9d81e57c31a23247094727987a2f64b708901233a42a24d44d24e93f6", size = 406124, upload-time = "2026-05-06T02:30:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/df/29/cdb40de6307809fa8e9452e4f9a65881a3140d01d9d589a07e9d054d8e1c/optree-0.19.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:96fad6c7b3a6fde3a0c8655fd003359cd247f8400749217502591a5ffc328699", size = 466772, upload-time = "2026-05-06T02:30:47.766Z" }, + { url = "https://files.pythonhosted.org/packages/cb/15/4645e1816e815a1306bbb7e3e2e6ba124f6dc325f8088a2db69301219a0c/optree-0.19.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3a900df0ffb9b8259961b337289754531a7e0a5de2f681e9c80866b6a7cb74e", size = 466203, upload-time = "2026-05-06T02:30:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d2/5758c76bdd7034b721d84c7f0fd911f3b39dcb489eeb27f674aaae8a5f5c/optree-0.19.1-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:27c8dc0f89ade9233aa7ed25ce15991da188e6950eb17cc0c313fc1f327c5b0b", size = 465030, upload-time = "2026-05-06T02:30:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/09/b9/f668bc51129c0fec7728ae8b43180417fe1c1fe99f71d302739f6cc50944/optree-0.19.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38f2e503fad50aff58cade85db448002d4adc72f4b3b50dcc7f3ef4bcd3b0173", size = 447141, upload-time = "2026-05-06T02:30:51.42Z" }, + { url = "https://files.pythonhosted.org/packages/a4/08/a7b8862e4465bf250c3ccc78db4d10b9a2cf90ce4db3681cbdf7eb076fb7/optree-0.19.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:5dc35cb31540ab6ed9850b0f8865ccd400994ebd51fcf0c156cc772073f43c04", size = 410016, upload-time = "2026-05-06T02:30:52.695Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/04b71a34cf5e663a1df029acceb5efc8a96c8dc4b0b6af6e98486638e913/optree-0.19.1-cp311-cp311-win32.whl", hash = "sha256:d32b1261be71211f77837e839e43a3e3e8fc57707091d2454d0a88590fb6abe8", size = 311810, upload-time = "2026-05-06T02:30:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/22/64/3cc7b08cb1c0f1949895f9490217ca8db6ced7f3bf75c65a5bf31c07bf1e/optree-0.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd28a527bb363a1d7d28e8b2fb62816ace6743418bb86e9c5f27ea6877dcdf6c", size = 337620, upload-time = "2026-05-06T02:30:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/85f4b05765287658529f09ede10461224161dcf0e29e6fce1ae488451cfe/optree-0.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:7853b58aa084e882ea078f390936bd92e46972eb8f9b5e654360b6480ca7283b", size = 349337, upload-time = "2026-05-06T02:30:56.647Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a7/cb5567029a608a296b0ca224025d03bba0365b41df19085b9b580191f6f2/optree-0.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:96e5c7c3b9144f08ae40c3d9848cfbcfa36b6bead0f8215ad071d5922ee6c4a5", size = 424023, upload-time = "2026-05-06T02:30:57.732Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a1/3651fb32fa8617108204aa4056d283af742020e0987d106f41402005d800/optree-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d9d198343e1e6ced18bef0cbff84091c1877964fc4a121df33f18840e073a01", size = 394782, upload-time = "2026-05-06T02:30:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1e/676470909aa64d7aba7c5edf83b171dc83b7af901d9ebb8e6d7512fe913a/optree-0.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a1202371d9fe3aa75f3e886b1f871aac4991a655aadb65e54f58a3ae9388ab2", size = 413157, upload-time = "2026-05-06T02:31:00.339Z" }, + { url = "https://files.pythonhosted.org/packages/f4/41/1a4c58f2af5742b9d9e21ea9e45c6c3c49463b5e2a0537e84ead1e9597ca/optree-0.19.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d41ccc4c20bfeae01d1d221c057a6d026e84e32229664952eddcdbe4b9b71417", size = 476923, upload-time = "2026-05-06T02:31:01.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/f62167bd9d6f6c948b191a0943923404678d47100f777f4a8fb37816e6f8/optree-0.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d934f240b109c6891dd06b2e30400b123b8a4b6ed31dcd0db2ae2378d30a6e8", size = 475385, upload-time = "2026-05-06T02:31:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/30/5e/5323c5fa3024fdd900bdd8f14621139ed844c2247bf1a26e7cf5c1116188/optree-0.19.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddeefb7ca799c09647e332ebc1a5f6c09888a5a0e51f2dff4ca55e65b42a8c14", size = 474406, upload-time = "2026-05-06T02:31:04.023Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/54e4c47e61a51504a5224c933722e0c8a69925aacec4c08175e9675aeb81/optree-0.19.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0ce49f64f804f7f35f2f9c2a21e3ba94c090199fccdcfd40e3ded4426c5c175", size = 457596, upload-time = "2026-05-06T02:31:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/bba07c0b769586c6bd54e81f1f734cad103dbe30abbadee940fe7d3e330e/optree-0.19.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e0f02600832ab8d0f6c934dcb5c339e17a36938d477641a45798e02625ebe107", size = 417900, upload-time = "2026-05-06T02:31:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8f/6ae994bb47f9394b33912a14593f9247737dd6c3303811550e5a3e918107/optree-0.19.1-cp312-cp312-win32.whl", hash = "sha256:f10d58c1a17e1b32f9d9b5e1b9d1ad964d99c1113d9df0b9f62f2fe7dde19909", size = 317302, upload-time = "2026-05-06T02:31:08.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/97/d7e3ec79dcdde81f785a0446acf75fea77723f5ca4b98556350d7877986f/optree-0.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:06f5c8a4cf356a1a276ce5cec1be44719ed260690f79c036d04b4d427e801258", size = 341362, upload-time = "2026-05-06T02:31:09.689Z" }, + { url = "https://files.pythonhosted.org/packages/33/97/813afb84a81fd8ae65444730907c05f0775fd6c79d3359c9e84bd3370445/optree-0.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:a33bd23fc5c67ecb9ff491b75fde10cd9b53f47f8a876de842090e8c7a2437e1", size = 351838, upload-time = "2026-05-06T02:31:11.086Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7b/0f2f3c9d55dda5127624daf68ff802ab624b739dd4b32aef505dac0c8e02/optree-0.19.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:f144cfd65fb17c6aa2c51818614eb009e6052d3d6ace91f7e570b1318cdcac4c", size = 929090, upload-time = "2026-05-06T02:31:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/670d260dfd0532d64272dd6f7edd540a09d7040c0342b6cc6cf773568ea4/optree-0.19.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:39a006735d2a0a68751a3bc33d670184fddcd86db63b0293e1e819739e8105e4", size = 391528, upload-time = "2026-05-06T02:31:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/96/46c15e80b0c97e2ba6aba11339008a37cabc5ccf55c31c6c60aecdb79638/optree-0.19.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d2cb43c36638f469f5d8f4cf638e914de90c62242d8bed29f1b4487e0346ab94", size = 398231, upload-time = "2026-05-06T02:31:15.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/39/9d7d22cdaeb9a40ace2485f91c5b7c5f3a7f688575e2621e436561211cc1/optree-0.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e70faa00ab69331f49f8337d45021bed09ae2265d1db72eea9d7817af2b73c64", size = 429852, upload-time = "2026-05-06T02:31:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/79/4c/1da9e8375e7b7fd9671dc5987682b042f6412c4d6fd9da03296403818d9f/optree-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1c5d21176b670407f4555aae40711668832599c4fb0627000c5ce3ed0d6e2dae", size = 398688, upload-time = "2026-05-06T02:31:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/cd2d178099618093f5a9fd1c9de80af2b428879922eae1e9f27f1002c8be/optree-0.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f658fa46305b2bdccdc5bb2cb07818aeaef88a1085499deda5be48a0a58d2971", size = 417560, upload-time = "2026-05-06T02:31:19.391Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/f22ff5632083b5032caa80208dd202f8e963ed4aac11afa0a0f6a307fd68/optree-0.19.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:e757079d44a00319447f43df5c51e55bf9b62d9f05eea0e2db5ff7c7ca5ec71d", size = 482937, upload-time = "2026-05-06T02:31:20.799Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d4/7499d28be8b11eb40668262d27802119fe7e6ec4cd8816b76a1acd7b08f5/optree-0.19.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9690c132822d9dee479cf7dff8cc52a67c8af42a4f7529d21f0f4f1d99e4c84e", size = 477864, upload-time = "2026-05-06T02:31:22.077Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6e/6c6fa6f1159ac68f4ee7666610127fb4c14d47a2fa7a0a48de3aecc24d4b/optree-0.19.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:544b70958dbd7e732bc6874e0180c609c9052115937d0ec28123bb49c1a574aa", size = 478319, upload-time = "2026-05-06T02:31:23.266Z" }, + { url = "https://files.pythonhosted.org/packages/68/b5/8a2427bbe4ee59e2ce26a14125728e3b48c7030c80984ba07d0e5d804d37/optree-0.19.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dde5b756946c1f1458aeab248a7a9b0c01bb06b5787de9f06d52ad38b745557", size = 462379, upload-time = "2026-05-06T02:31:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0c/a073eeaea4d4f68e02d5883ed8268746a296e6749e3c46e0124ca45f306c/optree-0.19.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:f1d7838e8b1b62258abd73a5911afad1153ed76822070558c3ba7e0bb5b44192", size = 423061, upload-time = "2026-05-06T02:31:25.652Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/637b151d071ca94aea0087322f470ce84c5828ef6b9c0de7dc7b4420a1cf/optree-0.19.1-cp313-cp313-win32.whl", hash = "sha256:9870d33ec50cca0c46c2b431cea24c6247457da15fd4ad66ccb8ab78145c1490", size = 317439, upload-time = "2026-05-06T02:31:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/49b8a8d9e94c57c6fa5008953f84a1c36a4119a3b90dcb7df745f1f05a00/optree-0.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:aa0845b725bcd0029e179cf9b4bc2cc016c7358e56fc7c0d2c43bf4d514c96cf", size = 343906, upload-time = "2026-05-06T02:31:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/1ae0a9685f5301f454f01d2490065b98df6956f90b1b2fd1cea9daa6d820/optree-0.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:6f0b1efc177bed6495f78d39d5aa495ccb31cc20bcf64bb1b806ca4c919f4049", size = 353146, upload-time = "2026-05-06T02:31:29.976Z" }, + { url = "https://files.pythonhosted.org/packages/9c/77/4c8108cbce2c8ae2aa4b6adc7874082882e32cf131cb64b3a4411f50dec4/optree-0.19.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b964bcdb5cfe367cdf56447e80ba5a49123098d8c4e8e68b41c20890eec6e58e", size = 469723, upload-time = "2026-05-06T02:31:31.425Z" }, + { url = "https://files.pythonhosted.org/packages/64/33/ce9b54646ed4ab5773a9dc59767dadfe3de8bb2e97a3ed19205b995a7a31/optree-0.19.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ccec0ee5a565eb5aa4fe30383016a358627ea23d968ec8ab28b1f2ce4ce3d8", size = 437071, upload-time = "2026-05-06T02:31:33.027Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/04260128a726e3550b49467a65bff859452897144b68bae54b2f2e5c27f1/optree-0.19.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:672588408906051d3e9a99aca6c0af93c6e0b638137a701418088eaa0bb6c719", size = 433503, upload-time = "2026-05-06T02:31:34.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/99/6a4cc29389667efa089a0c476b7c36b7d0a66e10dd2d8c2d19c776977566/optree-0.19.1-cp313-cp313t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d16cef4d0555d49ce221d80249f1285a2d3faf932e451c3ce6cb8ccb6a846767", size = 496305, upload-time = "2026-05-06T02:31:35.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/506aa1a64abce69e2f4cec9cdac3da0cae207cf04c5e70e7f143bf8b29d8/optree-0.19.1-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc2db0b449baff53aa7e583306101de0ade5e5ae9e6fce78400eb2319bbd23dc", size = 492759, upload-time = "2026-05-06T02:31:37.265Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/2210de9a68722007fe007da3cae1a5971b92fc8113b5eecef66a04637959/optree-0.19.1-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:76b3e9e5d37e6b05ec82fff91758c8c0e27e159b35faea4b33d5eb975d720257", size = 495447, upload-time = "2026-05-06T02:31:38.505Z" }, + { url = "https://files.pythonhosted.org/packages/d9/61/40c3463e52914d552c66c760ae15e673137c4cc1d1d9f8da0d745656193a/optree-0.19.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03faa8e23fdaf3a18f9a1568c2c0eb0641a6aa05baf3a20639bd11fb34664700", size = 475564, upload-time = "2026-05-06T02:31:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/1603680fa924e68e5697c1229510c0645db0a9c633a12d1a9bfdbfc9cb74/optree-0.19.1-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:a9b9c7e9148ec470124dc4c1d1cd1485dbeb35973357b5911b181a79090426d2", size = 442414, upload-time = "2026-05-06T02:31:40.908Z" }, + { url = "https://files.pythonhosted.org/packages/a5/58/34820bab11f28ba6b03fe9e151880ad591b43f26648f058c94451fbdfc3a/optree-0.19.1-cp313-cp313t-win32.whl", hash = "sha256:ab8ad9803376d553a2958471b6bb6842b7e15888e19cc6aeb76da96c6afd948d", size = 348644, upload-time = "2026-05-06T02:31:42.038Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2b/0be3f8b9765f366e3e12d0590e9c6514de110d0c5b3b9002f49e56bf15b1/optree-0.19.1-cp313-cp313t-win_amd64.whl", hash = "sha256:afd4abeb2783b2367093287bc6268ac9af244b20c8d9b01696ccfe817483b66c", size = 382445, upload-time = "2026-05-06T02:31:43.166Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/8c0882cdd42e28a23c1998297c8ad1202194510cbba8b050251429c641c0/optree-0.19.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b9120510d3f951e268e417a3f64f335bc1c539e1e80bff2129ddc6fb60ac7b56", size = 388040, upload-time = "2026-05-06T02:31:44.661Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/4e16e26375c56c9e40760697af4e2b72f196c2099e96cc783b63dcc862a8/optree-0.19.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:e1951ddc870f67430310fd17393971c30510ee9fd290525b44c12afe25f3c307", size = 927808, upload-time = "2026-05-06T02:31:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/87/ff1c6bb6b79a5d0b70b83f7ae8b78811a406a749b3ae4478a2122a7afb66/optree-0.19.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ae9d42718ebf985cdad3182364b5cf829193b8fd2c6d993fbb4111d38e2bdf96", size = 390981, upload-time = "2026-05-06T02:31:47.38Z" }, + { url = "https://files.pythonhosted.org/packages/82/25/fc648710102960f87d18cd8fc8a24afe14a5ec7827c64dfb1340230c0794/optree-0.19.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:930268ebfdebca43a8808f6293910d6ade2fe7c84fa784692017d7120d285226", size = 397756, upload-time = "2026-05-06T02:31:48.76Z" }, + { url = "https://files.pythonhosted.org/packages/24/f6/a7bf5d75a6481038bbb61846d87d43124d63741385796ef7b37d326f46bd/optree-0.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b2757c5d922aab76cfc9b870c373fb35209c2094e3c912733b326c043e85a0c6", size = 427424, upload-time = "2026-05-06T02:31:49.838Z" }, + { url = "https://files.pythonhosted.org/packages/49/cc/14dd93887295859457e507fc46a847b68ae8f20c42b2fde4d8a749c94bbc/optree-0.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b17a7b70ff8bd406c2142914c5ab0a57f8bcfb9f52181f7012e32406bbdbfdda", size = 398242, upload-time = "2026-05-06T02:31:51.262Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/ac51aa118dd918761519fbc031865b1d6f850453e9a7ac0c3da21109c4f0/optree-0.19.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:987bba55366917d9829f45b5ee86499ecc87a30e9103072db9ab8d67f9958179", size = 419568, upload-time = "2026-05-06T02:31:52.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/41/25144e61f76278b9e0a5d4189c7083fe853164c5f7328a1f5aac43d964c2/optree-0.19.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d3bba2af7a5fce0c25e99024688e68dfe9be41e3d6e92720febbefdc879fba38", size = 482797, upload-time = "2026-05-06T02:31:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/2c76c7ce937323988770c41126e0e380bcb73a816f68a767f23b5c33aced/optree-0.19.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dae6c247cc8751bd2f167951468769f5c98f8cfdae31c0db0f2eb4145a6ec560", size = 479794, upload-time = "2026-05-06T02:31:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ca/bd9553f94bec0bc7860f10ae177c14ca265ab19ddb463122be22fa335ee8/optree-0.19.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17a986fd91ccdc18bb7b587ca1f508c1761580a93517e6db33a13b22e46acb9b", size = 481084, upload-time = "2026-05-06T02:31:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1a/4834b1f2fb1847412353d7342eb7a1d001a4f3bd9d24155e057135a4aa44/optree-0.19.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d0e1493429ae1d1a5e34855774ee604c974a8f76656bd0e562cdbf9466c9b1f", size = 462955, upload-time = "2026-05-06T02:31:57.829Z" }, + { url = "https://files.pythonhosted.org/packages/f4/88/598fb91c06fee3d8b08568779b011225dc2b66140927bd0b2b2d9b40a566/optree-0.19.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:f61a01ed9991193ed6f3db8e956ede05218190a32ca2ddfb71cfc40c8daba1d5", size = 423754, upload-time = "2026-05-06T02:31:59.291Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/83c64ecadc686e08310fc9c20bc0bbe6453e89b69257e08887818dac7886/optree-0.19.1-cp314-cp314-win32.whl", hash = "sha256:b0c920579bddc3b18a0e051850f017618e24efcc19ba83dcd415cf74db5fd904", size = 325214, upload-time = "2026-05-06T02:32:00.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/c3/4f2f318b98465376bbb7a06a33da553c688b3ed39dafbb8307f824eef74a/optree-0.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:50d77b91a8cd01adf422472b7edf39fc445b0268816176a868a385d28f8367c2", size = 351654, upload-time = "2026-05-06T02:32:01.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ab/55d7508e87055c730fe7207cfd0c45183a07ddf1f91d9e73d017a7f8c1f4/optree-0.19.1-cp314-cp314-win_arm64.whl", hash = "sha256:c682ab6711b7a623503711fa661a2bba7886e1c21dc06c3b7febba101b458051", size = 361610, upload-time = "2026-05-06T02:32:03.003Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/4f7facd482d56079b7adb8ce3fede19f41629bc0463e8ee25907f1dba36c/optree-0.19.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:068edb89fadd94f6f57fdb51f4ad2c764b5a0bfd00903c55ffe433c2863a8037", size = 469130, upload-time = "2026-05-06T02:32:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/92/60/f7539012aa8a7488c1e34f66b76eadc384c3152dd9800973f1b5fe045dfd/optree-0.19.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a609c90e4f64e4f3e2b5b3cc022210314834737e0e61a745485e33b33eae773b", size = 437286, upload-time = "2026-05-06T02:32:05.527Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3f/a5f8fb3ec3840f885de52d7a793ba57ace17990e3a9b3797218425ffe842/optree-0.19.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfae64c4c371640a4b3e2a9e3e6aa3a3e8cdf2da5247a88fef5b632614b948a6", size = 431954, upload-time = "2026-05-06T02:32:06.83Z" }, + { url = "https://files.pythonhosted.org/packages/68/dc/6d0ef14bc82bd54046c1a066d25fa6854123a6b29fd691f1f95dec3ab45f/optree-0.19.1-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:470742544ff2d4b63843023f38dcfb83e82c3a9877c783dee0e69cbb974de6d1", size = 494631, upload-time = "2026-05-06T02:32:08.038Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9a/9e183c610c414cba581f9afda7610589d89cae229d627b14f8480425d975/optree-0.19.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1a74e0656ccef45b1fec07b9d964ce97f3def8bab73711f56175076c4259884f", size = 491786, upload-time = "2026-05-06T02:32:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/266b9de8eb5b16bfe7010c90c55840517d5d61ee6e0ca64901440296d97a/optree-0.19.1-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55841132ba8a34dbbd85e0c2cf990602384eea0e4638df986cd3266482f4a17", size = 490876, upload-time = "2026-05-06T02:32:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/42a8ca6277ef93d47ab0986e30a25134206afe0c6e6c3425c8736b2677ba/optree-0.19.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5f8383952f18d5a4ec6b248d8ae6fe27012434ad9750aa33a821ad4846da5af", size = 475079, upload-time = "2026-05-06T02:32:12.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/91/e363f4adda292f891ca0cf5748010fea955737bdf494cc11d4c3bcda6935/optree-0.19.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:de8acbed5965beae6f6b0456fcb8d1afaea1fe300810739e88645e22138849bc", size = 440119, upload-time = "2026-05-06T02:32:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/489d22ef3cadb2f5f3bbd6e6099d17b5a521ff533e086f78f005c3358017/optree-0.19.1-cp314-cp314t-win32.whl", hash = "sha256:312048e69dc88de26915674f961bf38980a765a6b48ead2f1672858a39402c41", size = 357465, upload-time = "2026-05-06T02:32:15.424Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/7f48b7034ff75d2eb3e94e2196709ddbf762798fb621f9508899fa66b44e/optree-0.19.1-cp314-cp314t-win_amd64.whl", hash = "sha256:60e9345405d7b06cafdf1b1dd2e2261ceddddce10f35729240f90e2bab845a0b", size = 397783, upload-time = "2026-05-06T02:32:16.853Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/6d6f93416c66820cb8753e65b5ff43c47480af9c4911bd2b8406ff0f7f27/optree-0.19.1-cp314-cp314t-win_arm64.whl", hash = "sha256:4e103e212d1e8fe0399ed076eff80a905fb14929729bbd994d3660110a27a252", size = 396064, upload-time = "2026-05-06T02:32:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d4/ffeedc86f8b91e5c17994f38bd1f7aa2e20f9b70a6d3ae906af16414626c/optree-0.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3f4f1c276fa06227cdaf58349d22a3231b3dd3d47de1f90a86222ebf831fc397", size = 417543, upload-time = "2026-05-06T02:32:32.592Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/80fb1b289940e34858cb89f05bc7ce23d6d1272886c2f78bc7e3ab1a306b/optree-0.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:77d93eafbd0046c7350bc592ab8e3814abbd39a6d716b5b1e5d652cc486f445c", size = 390184, upload-time = "2026-05-06T02:32:34.273Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/f31784a7a2dcc0c1f84b691afc552ea5b26db5f56657692a12954a828db4/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3507ae5db5827eef3da42d04c5a41df649cedc2e42d5d39dc0f869d36915a00b", size = 409025, upload-time = "2026-05-06T02:32:35.817Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a5/647b93eb16244cc7f6dfccc025ac495245e306ff4cb8f9ad15718219141a/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:732c4581fb666869b8b391ec4ca13d2729795f9abe72b5aec2e582bcbea1975d", size = 449514, upload-time = "2026-05-06T02:32:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/d251c9338771ef0f9db8e538bd77810100c495734b57494464c7e223f0d0/optree-0.19.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e12ee3776a16f6feaa8263b92469ad546b870af71d50602745855d8449219221", size = 341586, upload-time = "2026-05-06T02:32:38.308Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[package.optional-dependencies] +hdf5 = [ + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +performance = [ + { name = "bottleneck" }, + { name = "numba" }, + { name = "numexpr" }, +] + +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + +[[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 = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "pims" +version = "0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "slicerator" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz", hash = "sha256:55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b", size = 87779, upload-time = "2024-06-10T19:20:42.842Z" } + +[[package]] +name = "pint" +version = "0.24.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "flexcache", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flexparser", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "platformdirs", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/bb/52b15ddf7b7706ed591134a895dbf6e41c8348171fb635e655e0a4bbb0ea/pint-0.24.4.tar.gz", hash = "sha256:35275439b574837a6cd3020a5a4a73645eb125ce4152a73a2f126bf164b91b80", size = 342225, upload-time = "2024-11-07T16:29:46.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl", hash = "sha256:aa54926c8772159fcf65f82cc0d34de6768c151b32ad1deb0331291c38fe7659", size = 302029, upload-time = "2024-11-07T16:29:43.976Z" }, +] + +[[package]] +name = "pint" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "flexcache", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flexparser", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "platformdirs", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/d08c41a8c004e1d437ef467e7c4f9c3295cd784eba48ed5d1d01f94b1dad/protobuf-4.25.9.tar.gz", hash = "sha256:b0dc7e7c68de8b1ce831dacb12fb407e838edbb8b6cc0dc3a2a6b4cbf6de9cff", size = 381040, upload-time = "2026-03-25T23:09:36.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/e9/59435bd04bdd46cb38c42a336b22f9843e8e586ff83c35a5423f8b14704e/protobuf-4.25.9-cp310-abi3-win32.whl", hash = "sha256:bde396f568b0b46fc8fbfe9f02facf25b6755b2578a3b8ac61e74b9d69499e03", size = 392879, upload-time = "2026-03-25T23:09:21.32Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/42a5c7f1001783d2b5bfcecde10127f09010f78982c86ae409122ce3ece6/protobuf-4.25.9-cp310-abi3-win_amd64.whl", hash = "sha256:3683c05154252206f7cb2d371626514b3708199d9bcf683b503dabf3a2e38e06", size = 413900, upload-time = "2026-03-25T23:09:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/0074a0a9eb01f3d1c4648ca5e81b22090c811b210b61df9018ac6d6c5cda/protobuf-4.25.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:9560813560e6ee72c11ca8873878bdb7ee003c96a57ebb013245fe84e2540904", size = 394826, upload-time = "2026-03-25T23:09:25.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/b2dba856f64c36b2a06c67be1472de98cca07a2322d0f0cbf03279a40e5b/protobuf-4.25.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:999146ef02e7fa6a692477badd1528bcd7268df211852a3df2d834ba2b480791", size = 294191, upload-time = "2026-03-25T23:09:26.613Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/53f18822017b8bda6bd8bb4e02048e911fdc79a3dafdc83ab994fe922a84/protobuf-4.25.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:438c636de8fb706a0de94a12a268ef1ae8f5ba5ae655a7671fcda5968ba3c9be", size = 295178, upload-time = "2026-03-25T23:09:27.839Z" }, + { url = "https://files.pythonhosted.org/packages/16/28/d5065b212685875d3924bcdb3201cbf467cb4d58a18aa19a8dfd99ea80a9/protobuf-4.25.9-py3-none-any.whl", hash = "sha256:d49b615e7c935194ac161f0965699ac84df6112c378e05ec53da65d2e4cbb6d4", size = 156822, upload-time = "2026-03-25T23:09:34.957Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psygnal" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/79/20c3e23e75272e9ddf018097cf872ab088bccba978888472656629efa4a3/psygnal-0.15.1.tar.gz", hash = "sha256:f64f62dee2306fc1c22050a59b6c6cdad126e04b0cf50e393ff858a1da719096", size = 123147, upload-time = "2026-01-04T16:38:41.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/44/ab13cb6147d010258826a43e574ad94599af0de29df13795fff9efee656c/psygnal-0.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ee55e3997f796fd84d4fdbd829bb1b19d323e087c43d072744604a3016c8851", size = 587322, upload-time = "2026-01-04T16:38:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/68c042a607ca613e9450dfee99cc5c2a4d10d95392fb1de2ba932dd0a605/psygnal-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:912bcf110bfe7b4aa121d24987b6a58afb967ff090a049dad136eaf3cbcc7bea", size = 576207, upload-time = "2026-01-04T16:38:06.183Z" }, + { url = "https://files.pythonhosted.org/packages/4b/86/123c7b169ad32994a0cd801cd1f11c1a2be84555807e9c8a8a4682c67a9f/psygnal-0.15.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2e860c11fe075fd80c93a24081c577ef7ec5c9da41f0e75990aa4cccf3f79cf", size = 864261, upload-time = "2026-01-04T16:38:07.895Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/886cec7bec2f27fe453cfa32bfcaac08a83aab2a04895af68f93e1c493b8/psygnal-0.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5b8bebcf99699ef50b6ef572868a490f6d191dc4466e5bd9818ca27e17cd581", size = 872582, upload-time = "2026-01-04T16:38:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/21/a3/da972a05568ee8a9dc6c6567bee2c0cc5af8c681baebcb9fdbbf3cceb4f7/psygnal-0.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:06e0a90490e1205620d97ac52fbbe3282a22b126a26d02b3e1196bb46de16c7a", size = 411043, upload-time = "2026-01-04T16:38:11.588Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a7/69495410025cc4298765545ce3b8c635cd4c8d3a362b7fbbc15b80e9fc8f/psygnal-0.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1adc41515f648696990964433f1e25d8dfd306813a3645366c85e01986ba57a0", size = 581002, upload-time = "2026-01-04T16:38:12.753Z" }, + { url = "https://files.pythonhosted.org/packages/75/1f/19a8126ccf3cd3974ba5d08a435a049b666961d90f5848ba83599d7a29de/psygnal-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:38ff18455b2ac73d4e8eea82ef298ce904b52e4dfdc603a24380c9c440e37519", size = 567775, upload-time = "2026-01-04T16:38:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/b1348880d603edb82128a721397a1ddcf3dfcf5384fe5689db6e471118ae/psygnal-0.15.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c923c322eeefb1140886927cfe7bda7c32341087e290e812b9c69a624ab72d54", size = 855961, upload-time = "2026-01-04T16:38:15.612Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/3da2d6f3583bd1a849f7faa2fd3492b14bfda05012519ceaea5992658af0/psygnal-0.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2714ddaa41ea3134c0ee91cebd5fb11a88f254ea1d5948806ab0ad5f8be603d5", size = 862721, upload-time = "2026-01-04T16:38:17.059Z" }, + { url = "https://files.pythonhosted.org/packages/4d/14/6fc7e97fdecf7e8c5c105684bab784920312a3259800d8b53e3cf8783f42/psygnal-0.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:877516056a5a383427a647fff2fad5179eaa3e12de2c083c273e748435414aef", size = 415696, upload-time = "2026-01-04T16:38:18.355Z" }, + { url = "https://files.pythonhosted.org/packages/76/65/b7bbca96bc477aa9ac2264e5907b2f4ccfcd1319f776dd1f35eec06cc2f4/psygnal-0.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d56f0f35eaf4a21f660de76885222faf9e8c7112454528d3394d464f3d4d1a3", size = 598340, upload-time = "2026-01-04T16:38:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/56577465a1b42a5e6780bb5fab53fb68f8bfd72f0131ed397576529af724/psygnal-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0febcf757a1323d9b8bd75735ee3569213d8110012a7bf0f478e85c5ab459fc6", size = 575311, upload-time = "2026-01-04T16:38:21.137Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/f642ac08104049383076f83480ed412c9626e068769a1c34873c595bec0e/psygnal-0.15.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5e4837dfbfa4974dabe0795e32be9aadcd87603adf734738ce1114f72238a05", size = 889770, upload-time = "2026-01-04T16:38:22.629Z" }, + { url = "https://files.pythonhosted.org/packages/de/43/e571fa40b72780abed080ef829e5ad98017b6fe48d28c15a2404e006b676/psygnal-0.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07b4c4e03bbf4e8cad7e25f4fbc1ba9575fb9c3d14991bc7edfeb8b09c8d6d54", size = 881105, upload-time = "2026-01-04T16:38:23.896Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/ef3ab825eb08eaecbbceeeb56383694fe64ce399dbfd1d0767bb85688785/psygnal-0.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:4f0ce91b9c18e92281bf2c3fc4bb4e808d90f0b023d0a37b302d354188520338", size = 418969, upload-time = "2026-01-04T16:38:25.731Z" }, + { url = "https://files.pythonhosted.org/packages/46/21/5a142165d27063abf5921807d3c3d973f5d44ab414a13b210839a43ead4d/psygnal-0.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2087aadc9404f007f79c2899e329932869e362c50de58b90631c5f49b4768cc5", size = 596768, upload-time = "2026-01-04T16:38:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/25/c1712931d61c118691e73daf29ef708c679ea9ba187c797dd5deee360411/psygnal-0.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f3bf68ca42569dfdce20c6cf915d34b78b9e3ddddacb9f78728224fda6946b4", size = 574808, upload-time = "2026-01-04T16:38:28.779Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4f/3593e5adb88a188c798604aed95fbc1479f30230e7f51e8f2c770e6a3832/psygnal-0.15.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e9fca977f5335deea39aed22e31d9795983e4f243e59a7d3c4105793adb7693d", size = 885616, upload-time = "2026-01-04T16:38:30.081Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/14779ed4c3a1d71fa1a9a87ecfb184ad3335dd64681067f77c1c47b14ae9/psygnal-0.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c85b7d05b92ccbec47c75ab8a5545eda462e81a492c82424aba5ab81a3ad89d", size = 876516, upload-time = "2026-01-04T16:38:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bc/4f771e3cdcde4db4023dbf36d6f0aab44e02b9de719353c22954b655e2ff/psygnal-0.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac0e693b29e0a429e97315a52313321855bef6140e9975b7ae78b4d93c8fbb42", size = 419172, upload-time = "2026-01-04T16:38:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2e/975bd61727578d88df62797f78390965ca7905780cf01eb59cb095a13638/psygnal-0.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:803fc33c4280c822c6f4b22e6c3ea7c4483e190f3cc69e69350098b3799476f3", size = 595706, upload-time = "2026-01-04T16:38:34.139Z" }, + { url = "https://files.pythonhosted.org/packages/b8/55/e487f1d91497eb75e86c3fdfef69a21b1cab24d023383dd7648b08797d6a/psygnal-0.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4f53b4b83355b0a785b745987fd04e59bbf169a9028ed81a68ca7e05fb76d458", size = 575133, upload-time = "2026-01-04T16:38:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2f/f286355accd0e68d3eef52e63c8b9ab6ba33ec3107177719a036b3319657/psygnal-0.15.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcbca12190f5aa65c1f8fb04a81fa6f4463c5f5dde25cd74c3a56ceff6f37b02", size = 889565, upload-time = "2026-01-04T16:38:37.003Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dc/40c6026c88d7f9220ecc913afe0501045a512c9b82f9b7e036bb089dc287/psygnal-0.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ac399566852fe4354ce26a1acbe12319232e8c2b615fe5ad1e114c547095cf6", size = 880863, upload-time = "2026-01-04T16:38:38.381Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/b4f45ec3057c473b5622fc002b3a636a698c34d3a0917a064ff5247f1984/psygnal-0.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:d3a03055f331ce91d44581c71edb79938ccc133a94af2ce7ad3a18fa57ac7be5", size = 423654, upload-time = "2026-01-04T16:38:39.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/49/7742544684bee728ec123515d2694cee859aa2a705951a461230b00f18cc/psygnal-0.15.1-py3-none-any.whl", hash = "sha256:4221140e633e45b076953c64bcb9b41a744833527f9a037c1ca98bc270798cbf", size = 90638, upload-time = "2026-01-04T16:38:40.841Z" }, +] + +[[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 = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, + { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, + { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, + { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybtex" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "latexcodec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f5/f30da9c93f0fa6d619332b2f69597219b625f35780473a05164a9981fd9a/pybtex-0.26.1.tar.gz", hash = "sha256:2e5543bea424e60e9e42eef70bff597be48649d8f68ba061a7a092b2477d5464", size = 692991, upload-time = "2026-04-03T13:05:39.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/f6/775eb92e865b28cdb4ad1f2bed7a5446197516f76b58a950faa3be3fd08d/pybtex-0.26.1-py3-none-any.whl", hash = "sha256:e26c0412cc54f5f21b2a6d9d175762a2d2af9ccf3a8f651cdb89ec035db77aa1", size = 126134, upload-time = "2026-04-03T13:05:40.623Z" }, +] + +[[package]] +name = "pybtex-docutils" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9", size = 6385, upload-time = "2023-08-22T06:43:20.513Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + +[[package]] +name = "pyconify" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/7f/94d424dc756a6287271cf40cf1b2a44c10e3f137bf3246a2b4a7416ca3d3/pyconify-0.2.1.tar.gz", hash = "sha256:8dd53757d9fbed41711434460932b2b5dbc25da720cd9f9a44af0187b2dfc07d", size = 22478, upload-time = "2025-02-06T13:20:53.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/40/50dd2e8bfec81676e4619903bd452c10dc0d8efac1533e79e67cc76759b5/pyconify-0.2.1-py3-none-any.whl", hash = "sha256:d3b53eee1f8a2d60c1d135610f42e789774dbe71c6d8af68af0a21d3b3ec9eb7", size = 19459, upload-time = "2025-02-06T13:20:51.613Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-compat" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/7e/43400b6e0800065a982efcfa3e87c8f8d247d60ea75ca1a9d01702e050f8/pydantic_compat-0.1.2.tar.gz", hash = "sha256:c5c5bca39ca2d22cad00c02898e400e1920e5127649a8e860637f15566739373", size = 12838, upload-time = "2023-10-24T23:25:09.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/65/2edf586ff7b3dfc520977c6529c9b718c86ef8459ece088f1ef1f74bf1d4/pydantic_compat-0.1.2-py3-none-any.whl", hash = "sha256:37a4df48565a35aedc947f0fde5edbdff270a30836d995923287292bb59d5677", size = 13092, upload-time = "2023-10-24T23:25:08.155Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "accessible-pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "babel", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "beautifulsoup4", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "accessible-pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "babel", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "beautifulsoup4", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyopengl" +version = "3.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyside6" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/15/1fa71e44d3b345b9eca82d38cf7e6d7505168b978fe98b3610c0e25d8c0e/pyside6-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:6fd9fbbc14e2a5707df611bfacbf3f71283c637b8a29b28708801eeb28bfcb69", size = 554720, upload-time = "2025-09-30T12:02:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/1e9841cc46196c55ac3eac0b8e08044a88cc70c8cc29e9dc1e33b2ced2b7/pyside6-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6485aebec8eba4e55d1ec1cebe68ca1413589880cc8ccd8a49acae852ec6cfb3", size = 554849, upload-time = "2025-09-30T12:02:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/6aeb7124a5c08ac537776d7d0be1011cf22ccfc6f95cf901fe1eb1a16e91/pyside6-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:8f4ff61b24a64153373b68a96339bd765fc010d02c4d98d0f6dba2a6c9686e11", size = 554846, upload-time = "2025-09-30T12:02:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/29/4eb0ab29ee7d60da4c5fefdb514c22ae3c91c3f855f46608cfbe23816518/pyside6-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:71b41b9ebd1c044c3777f2b32278d3919f07bda4f15c504bc165b643bc3cec01", size = 561082, upload-time = "2025-09-30T12:02:12.411Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c2/b936d50974a14846ccb883cbce0034ec431e1013af7f2aa29d3746a1565f/pyside6-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:33fb70addd02e1adaa45573485c431bca43cf12bb7b2596535b824ff169138ce", size = 545910, upload-time = "2025-09-30T12:02:13.746Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/56/714c55e4514ec6603be8126355baf416e507557a73e7fc76870e6f5c20b9/pyside6_addons-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:189c9a9a2fdaffa95e91731f5c0afdc47ba231f5f683d3f8977b22c233749ba4", size = 316906068, upload-time = "2025-09-30T12:03:28.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/fe/d5c67665f866b8859d02aa1a859f101a1b2fd348cb61746a3e16fd98fb20/pyside6_addons-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:68932327e1c33d729d79b2b94242f97b77601efe0427e757cd3fd588939ea479", size = 167175405, upload-time = "2025-09-30T12:04:11.793Z" }, + { url = "https://files.pythonhosted.org/packages/66/f2/66da0d8ba8e4eb934d2bb042f2199664d2366c121844e36376f724b53fd9/pyside6_addons-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:61f4f4859bd1711eea2202fe364a701597140ed4f3900ddf1f90d91ae631fdf9", size = 163102125, upload-time = "2025-09-30T12:04:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fb/849599666942ce35e9f250bce1abc7b1248eccdcbe8b3fe697a0f98a1326/pyside6_addons-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:0aece2a81ccf16ef9b750b09601b6876aa116bbc700e848ce82df42906f04c5c", size = 160681555, upload-time = "2025-09-30T12:05:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2d/e71face0149519d6ded4e77b5c5e266c094ce5ef8c5c96c2af2f0afadf3c/pyside6_addons-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:3b8749c9c2b297f53c980192e89ce38ac59019f290a2669fe1abb3128f9a09d9", size = 33759045, upload-time = "2025-09-30T12:05:48.373Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/a9/51549844900837c70e5c89cbd840956c2533732ef4f05c2b656e35ca8182/pyside6_essentials-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ad3664ff0ced9f92ed7872e512c86328894d29f262e6c3400400232a36dda357", size = 134586559, upload-time = "2025-09-30T12:06:22.681Z" }, + { url = "https://files.pythonhosted.org/packages/85/e8/9396cf11a60f80175bb3c5c1d498d84e87b7af653ab4ea001acf821a3981/pyside6_essentials-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c70d5544e892b201a677b615156fab6a0fef865e7fc287f55a0eae00a682e83f", size = 97495307, upload-time = "2025-09-30T12:06:49.213Z" }, + { url = "https://files.pythonhosted.org/packages/57/a5/0187b2845a6fa534217988f9351c0b85ba1c04e8fd31f1e3c13ba1c4386e/pyside6_essentials-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e22e517c592657e291c0cbe7d04078ab415cb225188d7e895f8f7adcdba755d2", size = 95199568, upload-time = "2025-09-30T12:07:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/52/b558c79bc32310361a37d1e894c2251a3557daec380be5f6d2a9223e8ef3/pyside6_essentials-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:21f98077c135864473089e59a6a0bd828e64c6644b3dd7267b102da4a2ee8f21", size = 73593457, upload-time = "2025-09-30T12:07:33.98Z" }, + { url = "https://files.pythonhosted.org/packages/d2/22/4ec828f6360e6e9bcd6fde2dec20fa4865fe1a77cb6bac6812f7d0aa55b3/pyside6_essentials-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:2e34081933e005686d79265cc04370a28fea3844ab63d432e493adcd4465070c", size = 54301788, upload-time = "2025-09-30T12:07:49.504Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[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 = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "qdarkstyle" +version = "3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/1c/00ca31b13727ade22d1b42b61dc86056493a72f01912082a61cb34e5abf6/QDarkStyle-3.1.tar.gz", hash = "sha256:600584d625343e0ddd128de08393d3c35637786a49827f174d29aa7caa8279c1", size = 698602, upload-time = "2022-05-30T15:56:52.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/59/01f454d0eacb6670c77add68611b7a572455ae69ba902d270ed761869f87/QDarkStyle-3.1-py2.py3-none-any.whl", hash = "sha256:679a38fcd040de9fac8b8cae483310302fdb12c8d912845249c41dc54974a9b2", size = 870167, upload-time = "2022-05-30T15:56:49.502Z" }, +] + +[[package]] +name = "qtconsole" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython-pygments-lexers" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "qtpy" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/28/4070eb0bacb99bc00bf60173fda25fb6a559d6600040035ba96c196d3647/qtconsole-5.7.2.tar.gz", hash = "sha256:27b485b9161925924c1d8e78e66bb342e6e3bc49bf675d0a67b49bad9c291521", size = 436661, upload-time = "2026-03-25T02:24:38.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/60/aba9f3c3f1f48c7fbdf03bed45b576a916cd09c08242939b5294b85e37b4/qtconsole-5.7.2-py3-none-any.whl", hash = "sha256:e1d1f6a792123363626e643a7a4ee561217773571043992693fba7eccfa89f95", size = 125883, upload-time = "2026-03-25T02:24:36.95Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lazy-loader", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[package.optional-dependencies] +data = [ + { name = "pooch", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lazy-loader", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[package.optional-dependencies] +data = [ + { name = "pooch", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +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/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { 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 = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/a2/2e6c090db384cc515069f4f85542bd5baf6786852073020ea73d4a76d3ea/sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978", size = 452946, upload-time = "2026-05-13T13:34:52.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" }, + { url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "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 = "shiboken6" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/ed/eac552326349629f8c3a89b57a754a860d9665aea78c172777395905d36b/shiboken6-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:e9b240828790b8e21a50e66449a5aa8b99f9b8a538c80c1a325fa04f8364985e", size = 400999, upload-time = "2025-09-30T12:00:08.123Z" }, + { url = "https://files.pythonhosted.org/packages/be/82/c1c6932f9849bc5e75c93c38a29419505a6e3e0037261e28f3e7ecbf2751/shiboken6-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f3f5337a3a8fc660ba1462265bd9a2bdda9588f8d90fbc3d5ac4ce3134c11e59", size = 204927, upload-time = "2025-09-30T12:00:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fc/026f4c8660494e513fd8c6d95d4d694d490795c6880f1fd23ec996a83e13/shiboken6-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:f66e82510e3a3170a42e3ef865329559b69811ce83102ad1ee57920319427c10", size = 200684, upload-time = "2025-09-30T12:00:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/f6bb3fc89623dd01ac67cd6be357e585ea5081536b275008b6c8d6fa7c28/shiboken6-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:f1498176d2d5bcade7d662e1fc5143980fb008b6c78b966289d29a4ae86cd0c6", size = 1166242, upload-time = "2025-09-30T12:00:13.447Z" }, + { url = "https://files.pythonhosted.org/packages/c1/66/097a5f80ed1058071466c8c4fd72f94886851f6e03028f980ef0c42a7b54/shiboken6-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:0c87bcfd483a1980794e03cf5ec6b2061691a1b075f92b78edb0d13847139ecf", size = 1727949, upload-time = "2025-09-30T12:00:15.23Z" }, +] + +[[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 = "slicerator" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/52/f38586b82b2935f8b59a09b0a79c545a22ed062e728c9418bafeb51f61e0/slicerator-1.1.0.tar.gz", hash = "sha256:44010a7f5cd87680c07213b5cabe81d1fb71252962943e5373ee7d14605d6046", size = 38283, upload-time = "2022-04-07T18:54:08.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl", hash = "sha256:167668d48c6d3a5ba0bd3d54b2688e81ee267dc20aef299e547d711e6f3c441a", size = 10274, upload-time = "2022-04-07T18:54:07.029Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx-book-theme" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, +] + +[[package]] +name = "sphinx-book-theme" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl", hash = "sha256:709605d308e1991c5ef0cf19c481dbe9084b62852e317fafab74382a0ee7ccfa", size = 455936, upload-time = "2026-03-09T23:20:28.788Z" }, +] + +[[package]] +name = "sphinx-comments" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/75/5bbf29e83eaf79843180cf424d0d550bda14a1792ca51dcf79daa065ba93/sphinx-comments-0.0.3.tar.gz", hash = "sha256:00170afff27019fad08e421da1ae49c681831fb2759786f07c826e89ac94cf21", size = 7960, upload-time = "2020-08-12T00:07:31.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl", hash = "sha256:1e879b4e9bfa641467f83e3441ac4629225fc57c29995177d043252530c21d00", size = 4591, upload-time = "2020-08-12T00:07:30.297Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + +[[package]] +name = "sphinx-external-toc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "sphinx-multitoc-numbering" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/85bcd2f1c142e580a1394c18920506d9399b8e8e97e4899bbee9c74a896e/sphinx_external_toc-1.1.0.tar.gz", hash = "sha256:f81833865006f6b4a9b2550a2474a6e3d7e7f2cb23ba23309260577ea65552f6", size = 37194, upload-time = "2026-01-16T13:15:59.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/80/1704c9179012e289dee2178354e385277ea51f4fa827c4bf7e36c77b0f4b/sphinx_external_toc-1.1.0-py3-none-any.whl", hash = "sha256:26c390b8d85aa641366fed2d3674910ec6820f48b91027affef485a2655ad7d0", size = 30609, upload-time = "2026-01-16T13:15:57.926Z" }, +] + +[[package]] +name = "sphinx-jupyterbook-latex" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/29/18a1fc30e9315e72f068637079169525069a7c0b2fbe51cf689af0576214/sphinx_jupyterbook_latex-1.0.0.tar.gz", hash = "sha256:f54c6674c13f1616f9a93443e98b9b5353f9fdda8e39b6ec552ccf0b3e5ffb62", size = 11945, upload-time = "2023-12-11T15:37:25.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl", hash = "sha256:e0cd3e9e1c5af69136434e21a533343fdf013475c410a414d5b7b4922b4f3891", size = 13319, upload-time = "2023-12-11T15:37:23.25Z" }, +] + +[[package]] +name = "sphinx-multitoc-numbering" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/1e/577bae038372885ebc34bd8c0f290295785a0250cac6528eb6d50e4b92d5/sphinx-multitoc-numbering-0.1.3.tar.gz", hash = "sha256:c9607671ac511236fa5d61a7491c1031e700e8d498c9d2418e6c61d1251209ae", size = 4542, upload-time = "2021-03-15T12:01:43.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl", hash = "sha256:33d2e707a9b2b8ad636b3d4302e658a008025106fe0474046c651144c26d8514", size = 4616, upload-time = "2021-03-15T12:01:42.419Z" }, +] + +[[package]] +name = "sphinx-thebe" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/fd/926ba4af1eb2708b1ac0fa4376e4bfb11d9a32b2a00e3614137a569c1ddf/sphinx_thebe-0.3.1.tar.gz", hash = "sha256:576047f45560e82f64aa5f15200b1eb094dcfe1c5b8f531a8a65bd208e25a493", size = 20789, upload-time = "2024-02-07T13:31:57.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl", hash = "sha256:e7e7edee9f0d601c76bc70156c471e114939484b111dd8e74fe47ac88baffc52", size = 9030, upload-time = "2024-02-07T13:31:55.286Z" }, +] + +[[package]] +name = "sphinx-togglebutton" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "setuptools" }, + { name = "sphinx" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/be/169a0b0a8ad9588e8697c85e1d489aaaca7416073c2fc0267c360af5aae9/sphinx_togglebutton-0.4.5.tar.gz", hash = "sha256:c870dfbd3bc6e119b50ff9a37a64f8991902269e856728931c7d89877e8d4b3d", size = 18101, upload-time = "2026-03-27T13:50:41.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl", hash = "sha256:74eac6d2426110c3e1e6f989a98e07d7823141a335df1ad8a9d637bdf6a7af62", size = 44907, upload-time = "2026-03-27T13:50:40.94Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-bibtex" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, + { name = "pybtex-docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/6a/8e0b2c2420286389e7fed78ff361ec30e2f1d58c8560af8d64df5e7b61e0/sphinxcontrib_bibtex-2.7.0.tar.gz", hash = "sha256:fee700f7aae29bb8f654c62913f00d34ac44fc0b8ca0fa67ac922ff4453addee", size = 120669, upload-time = "2026-05-06T09:29:24.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c0/d28e62407f4733bbe0169287bc012f0ac3b4a2021066b285570654119c8b/sphinxcontrib_bibtex-2.7.0-py3-none-any.whl", hash = "sha256:28cf0ec7a957d1c7548d5749317ed472ce877e1b629f430f88e3789aa51f87b1", size = 40287, upload-time = "2026-05-06T09:29:23.253Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/75/3a1cc926da8c563c58ddc124a7b3fe5ccadcae96c96e3a6f8ac3653a210a/sphinxcontrib_mermaid-2.0.2.tar.gz", hash = "sha256:f09576c78ca93fa0e3034fd9c45aaffa7c44ab449de9c43b8b8d262afe52bc66", size = 19265, upload-time = "2026-05-05T13:59:02.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/93be7e0f7fa915a576859b3bfac7a7baa3303181c44d7db7eefbd3e8a69f/sphinxcontrib_mermaid-2.0.2-py3-none-any.whl", hash = "sha256:d862e514991279fb4816302c5cfe167d2557bf3ce7125ae0cb47dac80a0f46ce", size = 14094, upload-time = "2026-05-05T13:59:01.585Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[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 = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, +] + +[[package]] +name = "superqt" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "qtpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/fe/919245507b15b4633cd40292d69c3b6afa8a650bf3ec33f3329d26314a3c/superqt-0.8.2.tar.gz", hash = "sha256:faa3bd00ef1c9b209b1f31b154dd41c596fec3824f61a70c251fb5569acd7ba6", size = 110190, upload-time = "2026-05-18T14:33:47.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/b9/748c3cbcdba20ef01c055a35905cad5c771cc1df29be11b9a82da6fd8e0d/superqt-0.8.2-py3-none-any.whl", hash = "sha256:ce8400ae7b577d090368aae5bdbabb06cd6bc893f6ca73485bb686835bf20943", size = 101595, upload-time = "2026-05-18T14:33:45.462Z" }, +] + +[package.optional-dependencies] +iconify = [ + { name = "pyconify" }, +] + +[[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 = "tables" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "blosc2", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numexpr", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "py-cpuinfo", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/96708a84e9fcd29d1f684d56d4c38a23d29b1c934599a072a49f27ccfa71/tables-3.10.1.tar.gz", hash = "sha256:4aa07ac734b9c037baeaf44aec64ec902ad247f57811b59f30c4e31d31f126cf", size = 4762413, upload-time = "2024-08-17T09:57:47.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/69/a768ec8104ada032c9be09f521f548766ddd0351bc941c9d42fa5db001de/tables-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca9d11a570ca1bc57f0845e54e55c3093d5a1ace376faee639e09503a73745b", size = 6823691, upload-time = "2024-08-17T09:56:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2d/074bc14b39de9b552eec02ee583eff2997d903da1355f4450506335a6055/tables-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b62881cb682438d1e92b9178db42b160638aef3ca23341f7d98e9b27821b1eb4", size = 5471221, upload-time = "2024-08-17T09:56:54.84Z" }, + { url = "https://files.pythonhosted.org/packages/4a/30/29411ab804b5ac4bee25c82ba38f4e7a8c0b52c6a1cdbeea7d1db33a53fe/tables-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cf1bfd8b0e0195196205fc8a134628219cff85d20da537facd67a291e6b347", size = 7170201, upload-time = "2024-08-17T09:56:59.011Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7d/3165c7538b8e89b22fa17ad68e04106cca7023cf68e94011ae7b3b6d2a78/tables-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f0e6dd45b91d99bf3976c8655c48fe3816baf390b9098e4fb2f0fdf9da7078", size = 7571035, upload-time = "2024-08-17T09:57:03.115Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/985a23d2cf27aad383301a5e99e1851228a1941b868515612b5357bded5f/tables-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90542ec172d1d60df0b796c48ad446f2b69a5d5cd3077bd6450891b854d1ffb", size = 6311650, upload-time = "2024-08-17T09:57:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/dc/04/957264eb35e60251830a965e2d02332eb36ed14fbd8345df06981bbf3ece/tables-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8917262a2bb3cd79d37e108557e34ec4b365fdcc806e01dd10765a84c65dab6", size = 6790492, upload-time = "2024-08-17T09:57:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/b2/19/eb7af9d92aaf6766f5fedfce11a97ab03cf39856561c5f562dc0c769a682/tables-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f93f6db623b484bb6606537c2a71e95ee34fae19b0d891867642dd8c7be05af6", size = 5506835, upload-time = "2024-08-17T09:57:13.883Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/897324e1ad543ca439b2c91f04c406f3eeda6e7ff2f43b4cd939f05043e4/tables-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01ca51624bca1a87e703d6d6b796368bc3460ff007ea8b1341be03bedd863833", size = 7166960, upload-time = "2024-08-17T09:57:17.463Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5c/3f21d1135bf60af99ac79a17bbffd333d69763df2197ba04f47dd30bbd4e/tables-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9372516c76be3a05a573df63a69ce38315d03b5816d2a1e89c48129ec8b161b0", size = 7568724, upload-time = "2024-08-17T09:57:23.02Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/3ee6b66263902eccadc4e0e23bca7fb480fd190904b7ce0bea4777b5b799/tables-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:09190fb504888aeacafb7739c13d5c5a3e87af3d261f4d2f832b1f8407be133a", size = 6312200, upload-time = "2024-08-17T09:57:26.322Z" }, + { url = "https://files.pythonhosted.org/packages/95/ec/ea6c476e33602c172c797fe8f8ab96d007d964137068276d142b142a28e5/tables-3.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7090af37909e3bf229d5599fa442633e5a93b6082960b01038dc0106e07a8da", size = 6791597, upload-time = "2024-08-17T09:57:29.598Z" }, + { url = "https://files.pythonhosted.org/packages/74/02/a967a506e9204e3328a8c03f67e6f3c919defc8df11aba83ae5b2abf7b0f/tables-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:203ed50c0c5f30f007df7633089b2a567b99856cd25d68f19d91624a8db2e7ad", size = 5474779, upload-time = "2024-08-17T09:57:32.43Z" }, + { url = "https://files.pythonhosted.org/packages/c3/26/925793f753664ec698b2c6315c818269313db143da38150897cf260405c2/tables-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e36ce9f10471c69c1f0b06c6966de762558a35d62592c55df7994a8019adaf0c", size = 7130683, upload-time = "2024-08-17T09:57:36.181Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/2b34f22284459e940a84e71dba19b2a34c7cc0ce3cdf685923c50d5b9611/tables-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f233e78cc9fa4157ec4c3ef2abf01a731fe7969bc6ed73539e5f4cd3b94c98b2", size = 7531367, upload-time = "2024-08-17T09:57:39.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/27/5a23830f611e26dd7ee104096c6bb82e481b16f3f17ccaed3075f8d48312/tables-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:34357d2f2f75843a44e6fe54d1f11fc2e35a8fd3cb134df3d3362cff78010adb", size = 6295046, upload-time = "2024-08-17T09:57:43.561Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/e7c25df877e054b05f146d6ccb920bcdbe8d39b35a0962868b80547532c7/tables-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6fc5b46a4f359249c3ab9a0a0a2448d7e680e68cffd63fdf3fb7171781edd46e", size = 6824253, upload-time = "2024-11-09T19:26:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/091865d75090a24493bd1b66e52d72f4d9627ff42983a13d4dcd89455d02/tables-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ecabd7f459d40b7f9f5256850dd5f43773fda7b789f827de92c3d26df1e320f", size = 5499587, upload-time = "2024-11-09T19:26:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/9dac8af333149fa01add439f710d4a312b70faf81c2f59a16b8bfaebb75e/tables-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40a4ee18f3c9339d9dd8fd3777c75cda5768f2ff347064a2796f59161a190af8", size = 7128236, upload-time = "2024-11-09T19:26:15.716Z" }, + { url = "https://files.pythonhosted.org/packages/89/fd/62f31643596f6ab71fc6d2a87acdee0bc01a03fbe1a7f3f6dc0c91e2546d/tables-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757c6ea257c174af8036cf8f273ede756bbcd6db5ac7e2a4d64e788b0f371152", size = 7527953, upload-time = "2024-11-09T19:26:20.229Z" }, +] + +[[package]] +name = "tables" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "blosc2", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numexpr", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "py-cpuinfo", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/a3/d213ebe7376d48055bd55a29cd9f99061afa0dcece608f94a5025d797b0a/tables-3.11.1.tar.gz", hash = "sha256:78abcf413091bc7c1e4e8c10fbbb438d1ac0b5a87436c5b972c3e8253871b6fb", size = 4790533, upload-time = "2026-03-01T11:43:36.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/bb/4a9cde6628563388db26fa86c64adb0f2475a757e72af0ec185fd520b72f/tables-3.11.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:eb30684c42a77bbecdef2b9c763c4372b0ddc9cc5bd8b2a2055f2042eee67217", size = 7045977, upload-time = "2026-03-01T11:42:48.605Z" }, + { url = "https://files.pythonhosted.org/packages/78/74/6568c8d3aabf9982ab89fe3e378afbd7aad4894bde4570991a3246169ef4/tables-3.11.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0367d2e3df0f10ea63ccf4279f3fe58e32ec481767320301a483e2b3cd83efc", size = 6264947, upload-time = "2026-03-01T11:42:53.192Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/ec228901fca4c996306b17f5c60a4105144df0bbd07b3a4a816f91f37b4a/tables-3.11.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56bf6fb9132ead989b7e76695d7613d6d08f071a8019038d6565ba90c66b9f3e", size = 6903733, upload-time = "2026-03-01T11:42:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/c2dc674ea70fa9a4819417289a9c0d3e4780835beeed573eb66964cfb763/tables-3.11.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e78fe190fdeb4afe430b79651bae2a4f341904eb85aa8dbafe5f1caee1c7f67", size = 7241357, upload-time = "2026-03-01T11:43:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/a59b62af4127790c618eb11c06c106706e07509a3fb9e346b2a3ffa74419/tables-3.11.1-cp311-abi3-win_amd64.whl", hash = "sha256:7fa6cb03f6fe55ae4f85e89ec5450e5c40cc4c52d8c3b60eb157a445c2219e89", size = 6526565, upload-time = "2026-03-01T11:43:08.58Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/561c82496e7c8c15ebf19b53b12c0ef91b322a66869db762db9711102764/tables-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a4bbd95036a4d0cc5c86c1f87fbb490b4c53cd70982f1c01b3ed6dcb3085cbb9", size = 7111409, upload-time = "2026-03-01T11:43:13.424Z" }, + { url = "https://files.pythonhosted.org/packages/84/18/bac920aee8239b572c506459607c6dd8742bc6275a43d51d2dd6ae1a1541/tables-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e3cfe79484351f7216eb8f3767bfa1217bfd271b04428f79cfa7ef6d7491919d", size = 6380142, upload-time = "2026-03-01T11:43:17.213Z" }, + { url = "https://files.pythonhosted.org/packages/59/3c/f4a694aa744d2b14d536e172c28dd70c84445f4787083a82d6d44a39e39f/tables-3.11.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a9c35f87fcb6a48c79fbc4e3ab15ca8f6053c4ce13063d6ca2ec36cbb58f40f", size = 7014135, upload-time = "2026-03-01T11:43:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/94d4320d6c0fe5bd55230eec90cd142d58cda37b7cce00a318ac2a6abd93/tables-3.11.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cf3218b76ba78d156d6ee75c19fb757d50682f6c7b4905370441afbfc9d77f3", size = 7349293, upload-time = "2026-03-01T11:43:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/f7/02/a0f61a602ce2f2be8cc2e6146cc51acdaa8a1bb9b823b3863e70d3e0505d/tables-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a6f7a3b82dbf0ae0f30de635ca88bb42dd87938b0950369d0ee4289c52ae6de2", size = 6854713, upload-time = "2026-03-01T11:43:31.934Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-auth", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-auth-oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/a2/66ed644f6ed1562e0285fcd959af17670ea313c8f331c46f79ee77187eb9/tensorboard-2.14.1-py3-none-any.whl", hash = "sha256:3db108fb58f023b6439880e177743c5f1e703e9eeb5fb7d597871f949f85fd58", size = 5508920, upload-time = "2023-09-27T23:37:16.71Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/41/dccba8c5f955bc35b6110ff78574e4e5c8226ad62f08e732096c3861309b/tensorboard-2.17.1-py3-none-any.whl", hash = "sha256:253701a224000eeca01eee6f7e978aea7b408f60b91eb0babdb04e78947b773e", size = 5502989, upload-time = "2024-08-14T18:10:47.657Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/de/021c1d407befb505791764ad2cbd56ceaaa53a746baed01d2e2143f05f18/tensorboard-2.18.0-py3-none-any.whl", hash = "sha256:107ca4821745f73e2aefa02c50ff70a9b694f39f790b11e6f682f7d326745eab", size = 5503036, upload-time = "2024-09-25T21:21:50.169Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "markdown", marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pillow", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "tensorboard-data-server", marker = "python_full_version < '3.13'" }, + { name = "werkzeug", marker = "python_full_version < '3.13'" }, +] +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 = "tensorflow" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-estimator", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.37.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/51/ad9ebf4ef29754b813a057d64a0634feb12aef27cabcbdb7433dc5cd4cb4/tensorflow-2.14.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:318b21b18312df6d11f511d0f205d55809d9ad0f46d5f9c13d8325ce4fe3b159", size = 229634719, upload-time = "2023-09-26T22:51:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e0/1db7b4b382e7d654dd176ee3e09af201f0735ea1a3233c087c3e63f054e9/tensorflow-2.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:927868c9bd4b3d2026ac77ec65352226a9f25e2d24ec3c7d088c68cff7583c9b", size = 2108, upload-time = "2023-09-26T22:53:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4a/40/da089d1cabd9141543dfeb462e16f6c6741a76ac326174f168b7ce53d54f/tensorflow-2.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3870063433aebbd1b8da65ed4dcb09495f9239397f8cb5a8822025b6bb65e04", size = 2122, upload-time = "2023-09-26T22:56:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7a/c7762c698fb1ac41a7e3afee51dc72aa3ec74ae8d2f57ce19a9cded3a4af/tensorflow-2.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c9c1101269efcdb63492b45c8e83df0fc30c4454260a252d507dfeaebdf77ff", size = 489833115, upload-time = "2023-09-26T22:53:51.969Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c3/17c6aa1dd5bc8cea5bf00d0c3a021a5dd1680c250861cc877a7e556e4b9b/tensorflow-2.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:0b7eaab5e034f1695dc968f7be52ce7ccae4621182d1e2bf6d5b3fab583be98c", size = 2099, upload-time = "2023-09-26T22:55:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/22/50/1e211cbb5e1f52e55eeae1605789c9d24403962d37581cf0deb3e6b33377/tensorflow-2.14.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:00c42e7d8280c660b10cf5d0b3164fdc5e38fd0bf16b3f9963b7cd0e546346d8", size = 229677851, upload-time = "2023-09-26T22:51:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/de/ea/90267db2c02fb61f4d03b9645c7446d3cbca6d5c08522e889535c88edfcd/tensorflow-2.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c92f5526c2029d31a036be06eb229c71f1c1821472876d34d0184d19908e318c", size = 2106, upload-time = "2023-09-26T22:53:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/92/ba/0b9dc0a69e518cca919587fd32ec22a81c99bcdf94c8482f00440fff72d0/tensorflow-2.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c224c076160ef9f60284e88f59df2bed347d55e64a0ca157f30f9ca57e8495b0", size = 2122, upload-time = "2023-09-26T22:56:28.206Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/25e76075081ea98ec48f23929cefee58be0b42212e38074a9ec5c19e838c/tensorflow-2.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80cabe6ab5f44280c05533e5b4a08e5b128f0d68d112564cffa3b96638e28aa", size = 489875759, upload-time = "2023-09-26T22:54:19.219Z" }, + { url = "https://files.pythonhosted.org/packages/80/6f/57d36f6507e432d7fc1956b2e9e8530c5c2d2bfcd8821bcbfae271cd6688/tensorflow-2.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:0587ece626c4f7c4fcb2132525ea6c77ad2f2f5659a9b0f4451b1000be1b5e16", size = 2099, upload-time = "2023-09-26T22:55:30.95Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/81/d658100476affe0c68ccfb0ad72813ef487578187cadba1c896a26cac8e0/tensorflow-2.17.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:61f45ca991cf3dddf0b1069674c455fdbf38edf749dab962bb4bb8a3f99fb25f", size = 236135046, upload-time = "2024-10-24T22:57:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/dc5fa720f2381f6dfeb955dfc330261e183ef91e57128cb39aecb8440d1c/tensorflow-2.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aa202e17894dcb0582283e5a5c703391d793ccce11c5c02b1fe8f839ae09f3c", size = 223903722, upload-time = "2024-10-24T22:57:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/ca18413eb576ee0231a1fe6e9d9499afcef614fd94154e0aaf14f32ba3eb/tensorflow-2.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618cc21b0adf695fc8b4323f56ccd17c1408379422e1a177481d4fd8523fa8e", size = 601258535, upload-time = "2024-10-24T22:57:50.405Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/28abaa2e4e6df19d77a2d41cc2b62589250c403dcd505c45433919526fd7/tensorflow-2.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c23498b370c9d6b2521722b6acc89fab61e6a593e886df16bed3075584bf1c7", size = 7521, upload-time = "2024-10-24T22:58:03.64Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/92db1ecf5550f8937c21c28f84228959adb9ccba5f19e283ec7f22825b11/tensorflow-2.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:595c220b0febe2295a150e0d870c742a75c56b145a63ed878f78d64aa43c6ca6", size = 236176925, upload-time = "2024-10-24T22:58:08.728Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/d1f227b56a82cf168d4d5b3fa6094857079f36891af4bc851281afa5cc23/tensorflow-2.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc41bc3e31d205dcf6e4b8afc0514de05445303d93fade03549085ef8c45a2b2", size = 223945776, upload-time = "2024-10-24T22:58:18.161Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/f5b1f4b2c08295ae1cb8760d1fb6043485459f0d8c107dd900e76a6ba25d/tensorflow-2.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68841e17e573301d8ba9192f929b8096b0b341567cb81414096305c723de68d0", size = 601300475, upload-time = "2024-10-24T22:58:31.181Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e6/8dbd20c4942b578aa18ef61e8d7858ddbd3650bbea731539c1fdadbaa466/tensorflow-2.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:a6bd9474f1e0dedb7deb331c8e93cf2d5997da8781a1949f75c4a7cf8923d2e3", size = 7518, upload-time = "2024-10-24T22:58:42.649Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/29bfb68a1cfd61649cd51f5f08474a9eab343c2402f8998af6f1f6120aa5/tensorflow-2.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d43698039fd057ee6d7c3f908c4e9a6e310be6e77adda419bae3fee5ca7192fe", size = 236283570, upload-time = "2024-10-24T22:58:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/3a/86/3af00483ff5215e52ebd9d5be2987798a6985f60db413091522a97459c2c/tensorflow-2.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:695741b80ea9b2603a8d3f423051476c02a9cf48f4721d18c4dafbe0a8a5ca91", size = 224039295, upload-time = "2024-10-24T22:58:55.317Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/c81ec3b1ca9980cba8a8123a4df0a3f3b96ae4f49d024cd0c4175aa443c0/tensorflow-2.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3452ab09940cbc3be896641b256855ba92154b79a90c864c5b2be79db78a64", size = 601418583, upload-time = "2024-10-24T22:59:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/5d/53/0eafd55dd5802c60ac69f6ad01dec61cdb894b591a1f3e471e2a01edbe99/tensorflow-2.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:faf032fae35de0071f20850abd23d78e0e3ae116ce7fbfb9d034da2acc900543", size = 7521, upload-time = "2024-10-24T22:59:21.763Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.37.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/e8/d5d54e18ff6fe67c75c3c65415c98ecd31bd0ff7613d47a1390f062993b5/tensorflow-2.18.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8da90a9388a1f6dd00d626590d2b5810faffbb3e7367f9783d80efff882340ee", size = 239373575, upload-time = "2024-10-25T00:14:11.549Z" }, + { url = "https://files.pythonhosted.org/packages/5a/58/99ba9d580c218fd866e6044b10915eb415f60af38c03dca6ff2df7f83337/tensorflow-2.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:589342fb9bdcab2e9af0f946da4ca97757677e297d934fcdc087e87db99d6353", size = 231677108, upload-time = "2024-10-25T00:14:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/d4/80/1567ccc375ccda4d28af28c960cca7f709f7c259463ac1436554697e8868/tensorflow-2.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb77fae50d699442726d1b23c7512c97cd688cc7d857b028683d4535bbf3709", size = 615262200, upload-time = "2024-10-25T00:14:39.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/63/5ca1b06cf17dda9c52927917a7911612953a7d91865b1288c7f9eac2b53b/tensorflow-2.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f5a8b4e6273f488dc069fc3ac2211b23acd3d0437d919349c787fa341baa8a", size = 7519, upload-time = "2024-10-25T00:14:52.683Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/556c4159675c1a30e077ec2a942eeeb81b457cc35c247a5b4a59a1274f05/tensorflow-2.18.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:453cb60638a02fd26316fb36c8cbcf1569d33671f17c658ca0cf2b4626f851e7", size = 239492146, upload-time = "2024-10-25T00:14:56.741Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3d/45956345442e3a7b335df6f13d068121d8454c243f31b1f44244705ac584/tensorflow-2.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85f1e7369af6d329b117b52e86093cd1e0458dd5404bf5b665853f873dd00b48", size = 231839918, upload-time = "2024-10-25T00:15:05.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/76/c55967ac9968ddaede25a4dce37aba37e9030656f02c12676151ce1b6f22/tensorflow-2.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b8dd70fa3600bfce66ab529eebb804e1f9d7c863d2f71bc8fe9fc7a1ec3976", size = 615407268, upload-time = "2024-10-25T00:15:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/271e77c22724f370c24c705f394b8035b4d27e4c2c6339f3f45ab9b8258e/tensorflow-2.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e8b0f499ef0b7652480a58e358a73844932047f21c42c56f7f3bdcaf0803edc", size = 7516, upload-time = "2024-10-25T00:15:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/4cc283db323fd723f630e2454b2857054d2c81ff5012c1857659e72470f1/tensorflow-2.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ec4133a215c59314e929e7cbe914579d3afbc7874d9fa924873ee633fe4f71d0", size = 239565465, upload-time = "2024-10-25T00:15:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/56/e4/55aaac2b15af4dad079e5af329a79d961e5206589d0e02b1e8da221472ed/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4822904b3559d8a9c25f0fe5fef191cfc1352ceca42ca64f2a7bc7ae0ff4a1f5", size = 231898760, upload-time = "2024-10-25T00:15:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/50/29/61ce80da0bfea3948326697dd1d832d28c863c9dacf90a27ee80fd4c1d31/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdd65ea7e064064283dd78d529dd621257ee617218f63681935fd15817c6286", size = 615520727, upload-time = "2024-10-25T00:16:01.386Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f1/828bbccc84a72db960a7d116f55f3f6aec9f5658f5d32ce9db20142d5742/tensorflow-2.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:a701c2d3dca5f2efcab315b2c217f140ebd3da80410744e87d77016b3aaf53cb", size = 7520, upload-time = "2024-10-25T00:16:12.418Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/5e/955a719c2359430a6fa6ec596bafc903b31285844ef44ae53e83bb91ac62/tensorflow-2.18.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8baba2b0f9f286f8115a0005d17c020d2febf95e434302eaf758f2020c1c4de5", size = 239430540, upload-time = "2025-03-12T00:11:40.574Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/4631df86b7880c18ce221b16e9f6f08e8100143d99d68bd6612d8ec534f8/tensorflow-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd7284768f5a6b10e41a700e8141de70756dc62ed5d0b93360d131ccc0a6ba8", size = 231774989, upload-time = "2025-03-12T00:11:50.957Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/43ed0e0accc63747cb9b6250cbef6515a449f848d4eda0af9d591ac1cea9/tensorflow-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f929842999d60e7da67743ae5204b477259f3b771c02e5e437d232267e49f18", size = 615365234, upload-time = "2025-03-12T00:12:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e8/27/75d313117d8a9f8aadb8b9121cc33d44793a2d704c3b3f5866e632821b82/tensorflow-2.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:db1d186c17b6a7c51813e275d0a83e964669822372aa01d074cf64b853ee76ac", size = 368995257, upload-time = "2025-03-12T00:12:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/57e2acd11a2587cc5c0a6612a389a57f3bda3cd60d132934cb7a9bb00a81/tensorflow-2.18.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:661029cd769b311db910b79a3a6ef50a5a61ecc947172228c777a49989722508", size = 239549037, upload-time = "2025-03-12T00:12:38.202Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b3/902588dcffbc0603893f1df482840ff9c596430155d62e159bc8fc155230/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a6485edd2148f70d011dbd1d8dc2c775e91774a5a159466e83d0d1f21580944", size = 231937898, upload-time = "2025-03-12T00:12:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/05d862ebeaaf63343dffc4f97dab62ac493e8c2bbc6b1a256199bcc78ed4/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9f87e5d2a680a4595f5dc30daf6bbaec9d4129b46d7ef1b2af63c46ac7d2828", size = 615510377, upload-time = "2025-03-12T00:13:03.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/5f5ade4be81e521a16e143234747570ffd0d1a90e001ecc2688aa54bb419/tensorflow-2.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:99223d0dde08aec4ceebb3bf0f80da7802e18462dab0d5048225925c064d2af7", size = 369183850, upload-time = "2025-03-12T00:13:24.786Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/1cad54f8634897ad9421de8f558df9aa63d3f2747eb803ce5dbb2db1ef5b/tensorflow-2.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:98afa9c7f21481cdc6ccd09507a7878d533150fbb001840cc145e2132eb40942", size = 239622377, upload-time = "2025-03-12T00:13:36.89Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/35a3542a91f4ffd4cf01e72d7f0fb59596cd5f467ff64878b0caef8e0f31/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ba52b9c06ab8102b31e50acfaf56899b923171e603c8942f2bfeb181d6bb59e", size = 231996787, upload-time = "2025-03-12T00:13:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/64/42/812539a8878c242eb0bacf106f5ea8936c2cc4d7f663868bd872a79772ac/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442d2a774811789a8ad948e7286cb950fe3d87d3754e8cc6449d53b03dbfdaa6", size = 615623178, upload-time = "2025-03-12T00:14:03.541Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/9c5e935b76eebdf46df524980d49700a9c9af56abc8c62bfd93f57709563/tensorflow-2.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:210baf6d421f3e044b6e09efd04494a33b75334922fe6cf11970e2885172620a", size = 369234070, upload-time = "2025-03-12T00:14:23.423Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "astunparse", marker = "python_full_version < '3.13'" }, + { name = "flatbuffers", marker = "python_full_version < '3.13'" }, + { name = "gast", marker = "python_full_version < '3.13'" }, + { name = "google-pasta", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "h5py", marker = "python_full_version < '3.13'" }, + { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "python_full_version < '3.13'" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "opt-einsum", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "requests", marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "six", marker = "python_full_version < '3.13'" }, + { name = "tensorboard", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "termcolor", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/0e/9408083cb80d85024829eb78aa0aa799ca9f030a348acac35631b5191d4b/tensorflow-2.20.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e5f169f8f5130ab255bbe854c5f0ae152e93d3d1ac44f42cb1866003b81a5357", size = 200387116, upload-time = "2025-08-13T16:50:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ea91ac67a9fd36d3372099f5a3e69860ded544f877f5f2117802388f4212/tensorflow-2.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02a0293d94f5c8b7125b66abf622cc4854a33ae9d618a0d41309f95e091bbaea", size = 259307122, upload-time = "2025-08-13T16:50:47.909Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9e/0d57922cf46b9e91de636cd5b5e0d7a424ebe98f3245380a713f1f6c2a0b/tensorflow-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7abd7f3a010e0d354dc804182372779a722d474c4d8a3db8f4a3f5baef2a591e", size = 620425510, upload-time = "2025-08-13T16:51:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/74/b5/d40e1e389e07de9d113cf8e5d294c04d06124441d57606febfd0fb2cf5a6/tensorflow-2.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a69ac2c2ce20720abf3abf917b4e86376326c0976fcec3df330e184b81e4088", size = 331664937, upload-time = "2025-08-13T16:51:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/de33bd90dbddc8eede8f99ddeccfb374f7e18f84beb404bfe2cbbdf8df90/tensorflow-2.20.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5f964016c5035d09b85a246a6b739be89282a7839743f3ea63640224f0c63aee", size = 200507363, upload-time = "2025-08-13T16:51:28.27Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a3d455db88ab5b35ce53ab885ec0dd9f28d905a86a2250423048bc8cafa0/tensorflow-2.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e9568c8efcb05c0266be223e3269c62ebf7ad3498f156438311735f6fa5ced5", size = 259465882, upload-time = "2025-08-13T16:51:39.546Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/7df285ee8a88139fab0b237003634d90690759fae9c18f55ddb7c04656ec/tensorflow-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:481499fd0f824583de8945be61d5e827898cdaa4f5ea1bc2cc28ca2ccff8229e", size = 620570129, upload-time = "2025-08-13T16:51:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/9246d3c7e185a29d7359d8b12b3d70bf2c3150ecf1427ec1382290e71a56/tensorflow-2.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:7551558a48c2e2f6c32a1537f06c654a9df1408a1c18e7b99c3caafbd03edfe3", size = 331845735, upload-time = "2025-08-13T16:52:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/47712f425c09cc8b8dba39c6c45aee939c4636a6feb8c81376a4eae653e0/tensorflow-2.20.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:52b122f0232fd7ab10f28d537ce08470d0b6dcac7fff9685432daac7f8a06c8f", size = 200540302, upload-time = "2025-08-13T16:52:22.146Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109, upload-time = "2025-08-13T16:52:31.513Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547, upload-time = "2025-08-13T16:52:46.396Z" }, + { url = "https://files.pythonhosted.org/packages/f9/37/b97abb360b551fbf5870a0ee07e39ff9c655e6e3e2f839bc88be81361842/tensorflow-2.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:1590cbf87b6bcbd34d8e9ad70d0c696135e0aa71be31803b27358cf7ed63f8fc", size = 331887041, upload-time = "2025-08-13T16:53:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/af283f402f8d1e9315644a331a5f0f326264c5d1de08262f3de5a5ade422/tensorflow-2.20.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:197f0b613b38c0da5c6a12a8295ad4a05c78b853835dae8e0f9dfae3ce9ce8a5", size = 200671458, upload-time = "2025-08-13T16:53:16.568Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4c/c1aa90c5cc92e9f7f9c78421e121ef25bae7d378f8d1d4cbad46c6308836/tensorflow-2.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47c88e05a07f1ead4977b4894b3ecd4d8075c40191065afc4fd9355c9db3d926", size = 259663776, upload-time = "2025-08-13T16:53:24.507Z" }, + { url = "https://files.pythonhosted.org/packages/43/fb/8be8547c128613d82a2b006004026d86ed0bd672e913029a98153af4ffab/tensorflow-2.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa3729b0126f75a99882b89fb7d536515721eda8014a63e259e780ba0a37372", size = 620815537, upload-time = "2025-08-13T16:53:42.577Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9e/02e201033f8d6bd5f79240b7262337de44c51a6cfd85c23a86c103c7684d/tensorflow-2.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:c25edad45e8cb9e76366f7a8c835279f9169028d610f3b52ce92d332a1b05438", size = 332012220, upload-time = "2025-08-13T16:53:57.303Z" }, +] + +[[package]] +name = "tensorflow-estimator" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/da/4f264c196325bb6e37a6285caec5b12a03def489b57cc1fdac02bb6272cd/tensorflow_estimator-2.14.0-py2.py3-none-any.whl", hash = "sha256:820bf57c24aa631abb1bbe4371739ed77edb11361d61381fd8e790115ac0fd57", size = 440664, upload-time = "2023-09-11T18:41:50.481Z" }, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f1/30c558bf7e795d02415e6e836f6190367130e0dfb8de585b8b81ddde5c7f/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a71421f8d75a093b6aac65b4c8c8d2f768c3ca6215307cf8c16192e62d992bcf", size = 1642401, upload-time = "2023-02-25T19:31:32.234Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/802488d94fdd14832d9832bd1fbd89cebac2519270d99ce1fe1c75eeb4b2/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359134ecbd3bf938bb0cf65be4526106c30da461b2e2ce05446a229ed35f6832", size = 2381672, upload-time = "2023-02-25T19:31:34.644Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/b18c800a17a10abeae3e8aa420fb452646c8f501bac82d61626437c48b0e/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b658b33567552f155af2ed848130f787bfda29381fa78cd905d5ee8254364f3c", size = 2572150, upload-time = "2023-02-25T19:31:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/437068ed6b44162d54addb8ac0ddfe9e406d07ac6f9c8a6cf96869ec2262/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:961353b38c76471fa296bb7d883322c66b91415e7d47087236a6706db3ab2758", size = 1486315, upload-time = "2023-02-25T19:31:38.297Z" }, + { url = "https://files.pythonhosted.org/packages/84/00/900ca310ff2e46eb3127f8f54af0b0000a6cc786be6a54dc2cfe841f4683/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8909c4344b0e96aa356230ab460ffafe5900c33c1aaced65fafae71d177a1966", size = 1642401, upload-time = "2023-02-25T19:31:40.204Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/0d44ef93add3432ce43f37fe0c205cc7b6fd685fca80054fb4a646a9dbe3/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e417faf8755aafe52d8f8c6b5ae5bae6e4fae8326ee3acd5e9181b83bbfbae87", size = 2381673, upload-time = "2023-02-25T19:31:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/3064195efa016fff942009fe965ecbbbbd7d70bf34ee22d4ff31a0f3443a/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c40e3c4ee1f8dda3b545deea6b8839192c82037d8021db9f589908034ad975", size = 2572150, upload-time = "2023-02-25T19:31:43.874Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/9566a313927be582ca99455a9523a097c7888fc819695bdc08415432b202/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:4bb37d23f21c434687b11059cb7ffd094d52a7813368915ba1b7057e3c16e414", size = 1486315, upload-time = "2023-02-25T19:31:45.641Z" }, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.37.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/a3/12d7e7326a707919b321e2d6e4c88eb61596457940fd2b8ff3e9b7fac8a7/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:249c12b830165841411ba71e08215d0e94277a49c551e6dd5d72aab54fe5491b", size = 2470224, upload-time = "2024-07-01T23:44:15.341Z" }, + { url = "https://files.pythonhosted.org/packages/1c/55/3849a188cc15e58fefde20e9524d124a629a67a06b4dc0f6c881cb3c6e39/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:257aab23470a0796978efc9c2bcf8b0bc80f22e6298612a4c0a50d3f4e88060c", size = 3479613, upload-time = "2024-07-01T23:44:17.445Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/9095c69e22c879cb3896321e676c69273a549a3148c4f62aa4bc5ebdb20f/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8febbfcc67c61e542a5ac1a98c7c20a91a5e1afc2e14b1ef0cb7c28bc3b6aa70", size = 4842078, upload-time = "2024-07-01T23:44:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/48/47b7d25572961a48b1de3729b7a11e835b888e41e0203cca82df95d23b91/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9679b36e3a80921876f31685ab6f7270f3411a4cc51bc2847e80d0e4b5291e27", size = 5085736, upload-time = "2024-07-01T23:44:21.034Z" }, + { url = "https://files.pythonhosted.org/packages/40/9b/b2fb82d0da673b17a334f785fc19c23483165019ddc33b275ef25ca31173/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:32c50ab4e29a23c1f91cd0f9ab8c381a0ab10f45ef5c5252e94965916041737c", size = 2470224, upload-time = "2024-07-01T23:44:23.039Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cc/16634e76f3647fbec18187258da3ba11184a6232dcf9073dc44579076d36/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b02f9c5f94fd62773954a04f69b68c4d576d076fd0db4ca25d5479f0fbfcdbad", size = 3479613, upload-time = "2024-07-01T23:44:24.399Z" }, + { url = "https://files.pythonhosted.org/packages/de/bf/ba597d3884c77d05a78050f3c178933d69e3f80200a261df6eaa920656cd/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e1f2796b57e799a8ca1b75bf47c2aaa437c968408cc1a402a9862929e104cda", size = 4842079, upload-time = "2024-07-01T23:44:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7c8ee5fe2fd8cb6392669ef16e71841133041fee8a330eff519ad9b36e4556", size = 5085736, upload-time = "2024-07-01T23:44:28.618Z" }, + { url = "https://files.pythonhosted.org/packages/70/83/4422804257fe2942ae0af4ea5bcc9df59cb6cb1bd092202ef240751d16aa/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ffebb6666a7bfc28005f4fbbb111a455b5e7d6cd3b12752b7050863ecb27d5cc", size = 2470224, upload-time = "2024-07-01T23:44:30.232Z" }, + { url = "https://files.pythonhosted.org/packages/43/9b/be27588352d7bd971696874db92d370f578715c17c0ccb27e4b13e16751e/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fe8dcc6d222258a080ac3dfcaaaa347325ce36a7a046277f6b3e19abc1efb3c5", size = 3479614, upload-time = "2024-07-01T23:44:32.316Z" }, + { url = "https://files.pythonhosted.org/packages/d3/46/962f47af08bd39fc9feb280d3192825431a91a078c856d17a78ae4884eb1/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbb33f1745f218464a59cecd9a18e32ca927b0f4d77abd8f8671b645cc1a182f", size = 4842077, upload-time = "2024-07-01T23:44:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/790d290c232bce9b691391cf16e95a96e469669c56abfb1d9d0f35fa437c/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:286389a203a5aee1a4fa2e53718c661091aa5fea797ff4fa6715ab8436b02e6c", size = 5085733, upload-time = "2024-07-01T23:44:36.663Z" }, +] + +[[package]] +name = "tensorflow-metal" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "(platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/da/463240cc8ff13a57119db62a676e2edca86fb93905a13527872dadf1926e/tensorflow_metal-1.2.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:bc735e36c97874e41f77ec2e7421ff745d2ec36ee641141c8091e4cc2dbcc819", size = 1357400, upload-time = "2025-01-31T00:52:54.577Z" }, + { url = "https://files.pythonhosted.org/packages/e0/09/91b253511cd59b9964672567f36b412daf3c70f75fcb5e84468fafa939ac/tensorflow_metal-1.2.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5fa7cee627031c14f45bd97ff0ef422cd6c3866199ff99cf29b94db6674ceb42", size = 1357400, upload-time = "2025-01-31T00:52:56.634Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/988b619322d5617a928e7f31cbb1ed8dd7f375f69dfa73dab26409a00382/tensorflow_metal-1.2.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4bece0ecb154b713b9f5ad4aec676a366d312822161e3cf0e1dea737c64cec04", size = 1357400, upload-time = "2025-01-31T00:52:57.924Z" }, +] + +[[package]] +name = "tensorpack" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "msgpack-numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyzmq", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tabulate", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tqdm", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/f0/edfda47ca6cc9ece30a893362c336b9121b691529e4cdf3b8732565be790/tensorpack-0.11.tar.gz", hash = "sha256:022b610e416e62e3575424cd08e60af27808a5fb6914294615391caf582cbd4f", size = 223526, upload-time = "2021-01-22T08:44:04.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/8c/63e5f5a4a04dea36b75850f9daa885ccbfad64bec1fae0ee4ca9f31b3eaa/tensorpack-0.11-py2.py3-none-any.whl", hash = "sha256:afcdaccf6e8e7d61c98970646d57b1c22372ddd712c462477a90f53e3994c4a1", size = 296324, upload-time = "2021-01-22T08:44:03.089Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/e7/883de3afc2032c89d6351fca7eb119ee197de21a2189e3889e9171d64862/tf_keras-2.14.1.tar.gz", hash = "sha256:4ae6871c4989720ea335d771b75b2e520d789ec1600302800bb5ed1a855af2fe", size = 1250245, upload-time = "2023-09-29T01:57:43.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/c7a98446afd921b7c4a0688e6eb30bf8f48040d069c349c772e7763636e6/tf_keras-2.14.1-py3-none-any.whl", hash = "sha256:903cf3f1739d3f92d2de80e07d2b36b32cc79f60b7affcee09a7f4721c02fca6", size = 1714936, upload-time = "2023-09-29T01:57:41.02Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/2b/d647100a2e80d159b020f1dbc2ef2c6787ed33c914951a63b3c88cd805d0/tf_keras-2.17.0.tar.gz", hash = "sha256:fda97c18da30da0f72a5a7e80f3eee343b09f4c206dad6c57c944fb2cd18560e", size = 1260098, upload-time = "2024-07-15T21:31:01.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/8b/75f7572ec0273ed8da50bc19defe08aaaafcc15fda3407db53f49acec814/tf_keras-2.17.0-py3-none-any.whl", hash = "sha256:cc97717e4dc08487f327b0740a984043a9e0123c7a4e21206711669d3ec41c88", size = 1724905, upload-time = "2024-07-15T21:30:58.941Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/a4/7d0acc28cde2b29b8114552ce3258dafdc6b2186d24bf8e912de713dd74f/tf_keras-2.18.0.tar.gz", hash = "sha256:ebf744519b322afead33086a2aba872245473294affd40973694f3eb7c7ad77d", size = 1260765, upload-time = "2024-10-24T22:58:06.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/ed/e08afca471299b04a34cd548e64e89d0153eda0e6cf9b715356777e24774/tf_keras-2.18.0-py3-none-any.whl", hash = "sha256:c431d04027eef790fcd3261cf7fdf93eb74f3cb32e05078b57b7f5a54bd53262", size = 1725427, upload-time = "2024-10-24T22:58:03.918Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.20.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/38/6060f6c7472439bb3890b9094d69d31d9f8d5da123b16c738773e70fff91/tf_keras-2.20.1.tar.gz", hash = "sha256:884be5938fb0b2b53b1583c1ae2b660ef87215377c29b5b6a77fd221b472aeaf", size = 1254487, upload-time = "2025-09-04T21:23:41.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/6b/d9a8202bfe5c9e3b078cf550bafab962aa9d6b1a1f1180f0065399d4c9b2/tf_keras-2.20.1-py3-none-any.whl", hash = "sha256:3f0e0a34d9a4c8758f24fdc1053e6e335f16ab5534c7d34f1899b8924779760c", size = 1694335, upload-time = "2025-09-04T21:23:40.153Z" }, +] + +[[package]] +name = "tf-slim" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/97/b0f4a64df018ca018cc035d44f2ef08f91e2e8aa67271f6f19633a015ff7/tf_slim-1.1.0-py2.py3-none-any.whl", hash = "sha256:fa2bab63b3925bd42601102e7f178dce997f525742596bf404fa8a6918e146ff", size = 352133, upload-time = "2020-05-07T22:18:55.976Z" }, +] + +[[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 = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, +] + +[[package]] +name = "timm" +version = "1.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.27.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/54/ece85b0eef3700c90db8271a43669b05a0ebbe2edb1962329c34374a297e/timm-1.0.27.tar.gz", hash = "sha256:315dfe63186ca9fb7ff941268941231fd5be259f2b4bb4afa28560ae1015cb9a", size = 2439861, upload-time = "2026-05-08T19:38:36.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/2e/26bab7686ff4aed48f8f5f6c23e2aa37b7a37ddd9effe3aa61e908fd518f/timm-1.0.27-py3-none-any.whl", hash = "sha256:5ff07c9ddf53cbada88eab1c93ff175c64cab683b5a2fddf863bcee985926f89", size = 2589280, upload-time = "2026-05-08T19:38:35.034Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "torch" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-cupti-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-nvrtc-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-runtime-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufft-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-curand-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusolver-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvtx-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "triton", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4d/17e07377c9c3d1a0c4eb3fde1c7c16b5a0ce6133ddbabc08ceef6b7f2645/torch-2.0.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ced00b3ba471856b993822508f77c98f48a458623596a4c43136158781e306a", size = 619913492, upload-time = "2023-05-08T16:35:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/4925decd863ce88ed9190a4bd872b01c146243ee68db08c72923984fe335/torch-2.0.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:359bfaad94d1cda02ab775dc1cc386d585712329bb47b8741607ef6ef4950747", size = 74032191, upload-time = "2023-05-08T17:03:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/c216fe520b877cf4fe03858c825cd2031ca3e81e455b89639c9b5ec91981/torch-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7c84e44d9002182edd859f3400deaa7410f5ec948a519cc7ef512c2f9b34d2c4", size = 172339532, upload-time = "2023-05-08T16:38:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/2e/27/5c912ccc490ec78585cd463198e80be27b53db77f02e7398b41305606399/torch-2.0.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:567f84d657edc5582d716900543e6e62353dbe275e61cdc36eda4929e46df9e7", size = 143409489, upload-time = "2023-05-08T16:41:49.802Z" }, + { url = "https://files.pythonhosted.org/packages/5a/77/778954c0aad4f7901a1ba02a129bca7467c64a19079108e6b1d6ce8ae575/torch-2.0.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:787b5a78aa7917465e9b96399b883920c88a08f4eb63b5a5d2d1a16e27d2f89b", size = 55830861, upload-time = "2023-05-08T17:01:35.636Z" }, + { url = "https://files.pythonhosted.org/packages/c8/21/25020cfdd9f564a72f400ee491610e50cb212e8add8031abaa959af6451e/torch-2.0.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e617b1d0abaf6ced02dbb9486803abfef0d581609b09641b34fa315c9c40766d", size = 619895009, upload-time = "2023-05-08T17:28:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/7273dea60a17c63d9eaef04ae8fee02351e0cb477e76df4ea211896ae124/torch-2.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b6019b1de4978e96daa21d6a3ebb41e88a0b474898fe251fd96189587408873e", size = 74042514, upload-time = "2023-05-08T17:29:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c8/f0dc8642e3ce0a3ae5f05e5149ab9df5375d569294f7be9a1ab1d95a1d76/torch-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbd68cbd1cd9da32fe5d294dd3411509b3d841baecb780b38b3b7b06c7754434", size = 172343262, upload-time = "2023-05-08T17:29:02.117Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/f20686a5ebd09c6feacced771cf4041a521c411c5bb10359580e9e491797/torch-2.0.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:ef654427d91600129864644e35deea761fb1fe131710180b952a6f2e2207075e", size = 143124494, upload-time = "2023-05-08T16:28:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/f901437d3e3ef6fe97adb1f372479626d994185b8fa06803f5bdf3bb90fd/torch-2.0.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:25aa43ca80dcdf32f13da04c503ec7afdf8e77e3a0183dd85cd3e53b2842e527", size = 55831241, upload-time = "2023-05-08T17:31:49.294Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-bindings", version = "12.9.4", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "fsspec", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufile-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-curand-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparselt-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvshmem-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-bindings", version = "13.2.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "fsspec", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cublas", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparselt-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvshmem-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b7/53fe0436586716ab7aecff41e26b9302d57c85ded481fd83a2cd741e6b4e/torch-2.12.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1834bd984f8a2f4f16bdfbeecca9146184b220aa46276bf5756735b5dae12812", size = 87981887, upload-time = "2026-05-13T14:55:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/d930eac44c30de06ed16f6d1ba4e785e1632532b50d8f0bf9bf699a4d0c7/torch-2.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d4d029801cb7b6df858804a2a21b00cc2aa0bf0ee5d2ab18d343c9e9e5681f35", size = 426355000, upload-time = "2026-05-13T14:54:31.944Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0c/c76b6a087820bab55705b94dfc074e520de9ae91f5ef90da2ecbf2a3ef12/torch-2.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d47e7dee68ac4cd7a068b26bcd6b989935427709fae1c8f7bd0019978f829e15", size = 532144998, upload-time = "2026-05-13T14:56:05.523Z" }, + { url = "https://files.pythonhosted.org/packages/4a/64/8a0d036e166a6aa85ee09bef072f3655d1ba5d5486a68d1b03b6813c01b3/torch-2.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:cf9839790285dd472e7a16aafcb4a4e6bf58ec1b494045044b0eefb0eb4bd1f2", size = 122949877, upload-time = "2026-05-13T14:55:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/131124fb95df03811b8260d1d43dcc5ee85ea1a344b964613d7efe77fb08/torch-2.12.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:10802fd383bbfed646212e765a72c37d2185205d4f26eb197a254e8ac7ddcb25", size = 87990344, upload-time = "2026-05-13T14:55:42.154Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/dda0dbd547dc549839824135f223792fd0e725f28ed0715dda366b7acaa2/torch-2.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c12592630aef72feaf18bd3f197ef587bbfa21131b31c38b23ab2e55fce92e36", size = 426362932, upload-time = "2026-05-13T14:54:15.295Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/a7dd5a3f9bdaa7842124e8e2359202b317c48d47d2fc5816fafdf2049adb/torch-2.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:415c1b8d0412f67551c8e89a2daca0fb3e56694af0281ba155eaa9da481f58b4", size = 532170085, upload-time = "2026-05-13T14:55:20.788Z" }, + { url = "https://files.pythonhosted.org/packages/12/1b/a61ce2004f9ab0ea8964a6e6168133a127795667639e2ff4f8f2bdb16a65/torch-2.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd37188ea325042cb1f6cafa56822b11ada2520c04791a52629b0af25bdfbfd9", size = 122953128, upload-time = "2026-05-13T14:54:52.744Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, +] + +[[package]] +name = "torchvision" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e7/3b43cce519d7236bbbdc31f468b43ae2084ff7db8cb162764311028d32a1/torchvision-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7754088774e810c5672b142a45dcf20b1bd986a5a7da90f8660c43dc43fb850c", size = 1501860, upload-time = "2023-05-08T17:32:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/4cd5133120e6cbcc2fa5c38c92f2f44a7486a9d2ae851e3d5a7e83f396d5/torchvision-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37eb138e13f6212537a3009ac218695483a635c404b6cc1d8e0d0d978026a86d", size = 1410848, upload-time = "2023-05-08T17:34:05.161Z" }, + { url = "https://files.pythonhosted.org/packages/87/0f/88f023bf6176d9af0f85feedf4be129f9cf2748801c4d9c690739a10c100/torchvision-0.15.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:54143f7cc0797d199b98a53b7d21c3f97615762d4dd17ad45a41c7e80d880e73", size = 6032483, upload-time = "2023-05-08T17:33:04.697Z" }, + { url = "https://files.pythonhosted.org/packages/16/5e/51c5fde550161edcfa3e131c51a8b4261775ebb2b118b3560116fa9f7a73/torchvision-0.15.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:1eefebf5fbd01a95fe8f003d623d941601c94b5cec547b420da89cb369d9cf96", size = 1249097, upload-time = "2023-05-08T17:33:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/cb1e7f25b6dda4e672ed8a3e7fbd073ec39e2ba6c378c3071ef2cd6100e1/torchvision-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:96fae30c5ca8423f4b9790df0f0d929748e32718d88709b7b567d2f630c042e3", size = 1195746, upload-time = "2023-05-08T17:33:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/2f3b2392ce7c4b856a5964803c4bc0bf0d5fc75ff7f6cc64cc2058c3e700/torchvision-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f35f6bd5bcc4568e6522e4137fa60fcc72f4fa3e615321c26cd87e855acd398", size = 1501857, upload-time = "2023-05-08T17:33:20.364Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/d713159642b36c42f5b6871330241070797ec89d3f8855eeb91c8baddddd/torchvision-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:757505a0ab2be7096cb9d2bf4723202c971cceddb72c7952a7e877f773de0f8a", size = 1410853, upload-time = "2023-05-08T17:33:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/4b/62/b6ec55347600b02b0a2a6596e673c69424aea7360c48343653866e66aa0d/torchvision-0.15.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:012ad25cfd9019ff9b0714a168727e3845029be1af82296ff1e1482931fa4b80", size = 6032450, upload-time = "2023-05-08T17:33:48.437Z" }, + { url = "https://files.pythonhosted.org/packages/66/e0/cd847d4d22be88a71d5d65f5809342e7ea7ded62230e7bde7420a2105e51/torchvision-0.15.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b02a7ffeaa61448737f39a4210b8ee60234bda0515a0c0d8562f884454105b0f", size = 1249109, upload-time = "2023-05-08T17:33:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/d5/26/a1e128500fb661d3ee7d99b97fb45d3b83e57091278c9babec859da7b87f/torchvision-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:10be76ceded48329d0a0355ac33da131ee3993ff6c125e4a02ab34b5baa2472c", size = 1195759, upload-time = "2023-05-08T17:33:11.008Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, +] + +[[package]] +name = "torchvision" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/15/2df874db140bbfe42f377e05e2dd38f2b9dc88414a6607eecc42073b2baa/torchvision-0.27.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0822b58d2c5d325cd0c7152b744acbd15f898c07572e2cfb70b075a865a4f6f9", size = 1758817, upload-time = "2026-05-13T14:57:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/10b1ff4087d35b7af7bd85ccb85fbc2573c6f1c2008cf8abfcaf605a10fc/torchvision-0.27.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9f44e35e6ec01caedacce9e941a5bf21fe424403321efac2507a201273653c5", size = 7830083, upload-time = "2026-05-13T14:57:18.336Z" }, + { url = "https://files.pythonhosted.org/packages/57/20/97dca91770235028ba7e9c598ca1fc48c297f1843af8102430f2adcd4335/torchvision-0.27.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:419c98a9275b27660cdce6d09080fd5974d1ec1d4a225f71439ebacb3b0c4e64", size = 7573816, upload-time = "2026-05-13T14:57:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/66fbf7f21f292d095a153ee142806646813e2055a69efe5854c28e7c3fb9/torchvision-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:2664d06acd64d328aa7689b0d0c81ee31e240e9977d8768816b4be7c66c03211", size = 3435489, upload-time = "2026-05-13T14:57:13.716Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d6/a7e71e981042d5c573e2e61891b9023b190c88adb75b18bed8594371250c/torchvision-0.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:df0c166b6bdf7c47f88e81e8b43bc085451d5c50d0c5d1691bc474c1227d6fed", size = 1758812, upload-time = "2026-05-13T14:57:16.662Z" }, + { url = "https://files.pythonhosted.org/packages/93/f9/f542fb7e4476603fb237ebdc64369a7d11f18eb5a129aa2559cbdb710aee/torchvision-0.27.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9bb9251f64b854124efed95d02953a89f7e2726c3ca662d7ea0151129157297f", size = 7831148, upload-time = "2026-05-13T14:57:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/7aa7cc2c9e8750027f6fb9ae3a7393ef43860bcdfe3966e2f71fee800e31/torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f44453f107c296d5446a79f7ac59733ad8bf5ddfa04c53805dfbae298a42a798", size = 7575519, upload-time = "2026-05-13T14:56:50.552Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/929b358b1a643849b81ec95569938044cc37dc65ab10c84eb6d82fe1bfbb/torchvision-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:b4aacff70ea4b7377f996f9048989c850d221fef33658ddbcae42aa5bd4ca11a", size = 3749475, upload-time = "2026-05-13T14:57:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" }, + { url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" }, + { url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, +] + +[[package]] +name = "triton" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cmake", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lit", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/ff6be541195daf77aa5c72303b2354661a69e717967d44d91eb4f3fdce32/triton-2.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505", size = 63268585, upload-time = "2023-03-20T17:32:17.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/4aa0179919306f9c2e3e5308f269d20c094b2a4e2963b656e9405172763f/triton-2.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1", size = 63278135, upload-time = "2023-03-20T17:32:25.22Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/97/dcd1f2a0f8336691bff74abc59b2ed9c69a0c0f8f65cd77109c49e05f068/triton-3.7.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223ac302091491436c248a34ee1e6c47a1026486579103c906ffd805be50cb89", size = 188367104, upload-time = "2026-05-07T19:04:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/c2ac4fd2d8809b7579d4a820a0f9e5de62a9bc8a757ed4b3abf4f7ee964a/triton-3.7.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c631b65668d4951213b948a413c0564184305b77bb45cc9d686d3e1ecc4701a3", size = 201313191, upload-time = "2026-05-07T18:45:58.444Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/5d842314bb6c78442cc60437928781701c6050b8d479bc2a1aed691d37ca/triton-3.7.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9e71fc392675fac364e0ecf4ef3f76f85b7f5433a16f4c3c5fe5f05a52c85fe", size = 188480277, upload-time = "2026-05-07T19:05:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/13/31/8315ea5f8dd18e60970b3022e3a8b93fd37e0b784fbbef86e10c8e6e5ca1/triton-3.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22bacffce443f54593dd20f05294d5a40622e0ea9ab632816f87154504356221", size = 201415942, upload-time = "2026-05-07T18:46:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "vispy" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freetype-py" }, + { name = "hsluv" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2e/ef2697f963111cf1bc83568bbe55f262b7c7c8a72948a6e802a7c236f2c1/vispy-0.15.2.tar.gz", hash = "sha256:d52d10c0697f48990555cea2a2bad3f9f5a772391856fda364ea4bbc69fd075c", size = 2513383, upload-time = "2025-05-19T13:26:41.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/2b/a483bf80575e047173d55f51115c38f9c43962cfd3247a861ce033913bee/vispy-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6bc8a49f4e0b27e19be0da318877666d733e1afc7231407e635f948a8aabb095", size = 1478710, upload-time = "2025-05-19T13:26:00.997Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8c9a3cb402037d6eda521492c04dddf6a00f600c85b650b33342573fe82f/vispy-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89a5d51cc980fed81f16373d515086d5b16da66868df8c1f76e71d4dfc17062c", size = 1472024, upload-time = "2025-05-19T13:26:03.055Z" }, + { url = "https://files.pythonhosted.org/packages/58/67/46111a528d63ad5308e4a547484c75e1c9982ec6a3709732ba0bcd343a7a/vispy-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7cbce48e14e2eeac491688be33b4d422d198515c67a1a87021622790309107e", size = 1845072, upload-time = "2025-05-19T13:26:04.226Z" }, + { url = "https://files.pythonhosted.org/packages/56/91/5c9d739410427c603fd0aa9e6a29b6c3e51edcc7f83f14bf243974808396/vispy-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8b0b6b55a781b6add58f946b95c368bfa203e7915fda234f4993c4db03d3c0", size = 1844568, upload-time = "2025-05-19T13:26:05.511Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8e/f07f9d7a341b5058a99e0700f7cab7d222b159154a392b335f0e59cbf636/vispy-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:0f2639656fd53ee4cbedf3e3caed0cef646f90e7ca4ec777ba18ad247234d8c9", size = 1467701, upload-time = "2025-05-19T13:26:07.193Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e7/3590e46cec1d51a8bba0b2d5442df698aec41ff5757e9e29e04aea3cbe12/vispy-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4f7adab04c0a90ca0a1155f09ecc425ca44bee713e6b2d4970d85fc010b05ee", size = 1478729, upload-time = "2025-05-19T13:26:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/bb/71/5871fe8d2f612502e5e148224426fb263431870a2f5fa5d2205fb9f2606a/vispy-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:725ab77b11bb3a4de5145a25b6e06f4555b0b7b1821baf7ffdd7f674f725dab2", size = 1472015, upload-time = "2025-05-19T13:26:10.246Z" }, + { url = "https://files.pythonhosted.org/packages/67/d8/067de34eac9aecf71a3291c54a738161caa9cbf30d62314b0500abff4117/vispy-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d107b07365bc1bdfa28fe6f8df05004cd73bfd6269230bef26b3862f2016fa64", size = 1872447, upload-time = "2025-05-19T13:26:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/af88bdb15b20a690b3d5409fc89fbdbb79095a593abb8a37094a42428760/vispy-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c67fb06df462b63ba95c77eb00a3d5b7d53e90f69987b0e216ab67ad7d6d00e", size = 1876232, upload-time = "2025-05-19T13:26:13.094Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4d/236c77644db2427f33330fd6372bac416262fd3de6771c555b969f67bd4f/vispy-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:46f8ca742958e19135cfa0f76ef1707666837ae6a9559a3fa3c2a3186967299e", size = 1467601, upload-time = "2025-05-19T13:26:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/31102425ea26bafab27bb5a675b499c57d29e72b1cc25fe3ae8facdd374e/vispy-0.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c8cbc22bc789f238423abadfcc3ba7589b85729be6b179b321720b511012fbee", size = 1478919, upload-time = "2025-05-19T13:26:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/8d/63/a601b8f1e8e418d3821d7b4a465c2eb6695ab8eccadfce886b99ffdfe92a/vispy-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf995b86dcc1eab265fa2bf9a8a5ca5a225b86c3ed4ac63900f0c5e90d5e8100", size = 1471915, upload-time = "2025-05-19T13:26:18.441Z" }, + { url = "https://files.pythonhosted.org/packages/79/1f/ca0deb4a876148c0fa515bf03d03e1dfad4553be8e6f2ab51433c074310b/vispy-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa64fcab1cd1730a10a1c9188e9cef8aefe6d099a46f2f87e2458bcef719918", size = 1866848, upload-time = "2025-05-19T13:26:19.6Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/dc6f335e54877f5d26ad4e4aac8f49b2d6e7719dee3e08f363d882c8aba4/vispy-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d38228cedd23876cb2359ff568d523a97284d225259d450d5e5e2129e98eb1", size = 1870433, upload-time = "2025-05-19T13:26:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/5ffa34d7300c35e8423d51789d594d35eaa7256d210f9909afa9fdd0aa37/vispy-0.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:a84531c1a89f8b9d3992eb0d0ab74f84884f49d1f46a8be3755c809d7507cb01", size = 1468068, upload-time = "2025-05-19T13:26:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/6664b7f0d28e0008a28111bae847a1fa3eedd409458bf01ca45a6e99da6f/vispy-0.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ac46aab46e208b919dcf0bb26869bcd083a0a1a4927bcaf41631ba38c247639", size = 1477880, upload-time = "2025-05-19T13:26:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6f/b9b36f841c5ff7320764f64822e79df3fea8a2c92270cda7f3a634d9a031/vispy-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e197a012503850a77d47177964d572edab964b2a8ea0f9d998c35b81325256c", size = 1471035, upload-time = "2025-05-19T13:26:24.554Z" }, + { url = "https://files.pythonhosted.org/packages/4c/af/b892a3c9b4be29755ce4d1fc17ecb8249446bdd0e17bb5b2a9cb39bcf0f4/vispy-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9eff397ecdbaf0052baae0563caa9e276272d6dc78fbaab3f5e51dbf5f7f92", size = 1860412, upload-time = "2025-05-19T13:26:26.212Z" }, + { url = "https://files.pythonhosted.org/packages/e8/13/8997d96bdc0a81f331dfd41b368935d79e8ea2917840266567e6dc40d684/vispy-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13ee0cb95bbff6d7b235e1a8f96e620eee15ccb6d5ad74902427ab7e56dc8ab", size = 1865860, upload-time = "2025-05-19T13:26:27.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/37/abb30db1853b69aed4c32813cf312f301ec3641b8846193be9a6d892d607/vispy-0.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:f5955821e9b452980490706648a702da8cb2b82e762b6d6589e5872118301b84", size = 1467882, upload-time = "2025-05-19T13:26:29.608Z" }, +] + +[[package]] +name = "wandb" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or extra == 'extra-10-deeplabcut-tf' or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/31/fe53d06b75ef0a7f2f0ee5931a89f7aedc27d233840b1839616860fed256/wandb-0.27.0.tar.gz", hash = "sha256:579e75300173059f9334e1f513a79ef15f6d9ea5c74e20d695633648cdd02031", size = 41090732, upload-time = "2026-05-14T03:44:08.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/5e/2c199e70e636ecfd217cde0bc7469f4511e1d03d0685eb92bfdfce391430/wandb-0.27.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:c156be4851485f3c4160cb6eb2e8991b4cdeffbccefc5636d33cf5e254847365", size = 24886476, upload-time = "2026-05-14T03:43:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cd/a617c871cd304a9804e56a7ec2ec2c65685bf0091a2b9f91910175a149e2/wandb-0.27.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:20179f38afb0158859a4141d29ac650d3fdbd0cf801a74ce25565c934f03776c", size = 26045779, upload-time = "2026-05-14T03:43:31.999Z" }, + { url = "https://files.pythonhosted.org/packages/10/0a/d3f159a201530b84b72ca5f98c68d1f351c2d9a1864558ed76c811407fae/wandb-0.27.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:626497d7975fa898d0a4a239da7a510483495ca3514510dbe75004a25963af4d", size = 25480764, upload-time = "2026-05-14T03:43:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6a/8721fcdf71d42639191040a77a585d2982402b1754700cb2ecfc2ca1470a/wandb-0.27.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f772da7005cc26a2a32b729a16982a583dc68b3d493df6a09d0aa5c5ca5a2060", size = 27256204, upload-time = "2026-05-14T03:43:39.765Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/279d167ba79fb7a8a43401c9f25efd0f6663ee9bd1eaf5a8578530198888/wandb-0.27.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:63acfc5b994e4a90e4a2fbdee6d45e664da3dd865bb1419942c8995c06c41cf1", size = 25647469, upload-time = "2026-05-14T03:43:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/a69ac59300e3c813939d0764348959ed2a21e14c668cb1cebcb04010da6a/wandb-0.27.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:17aae6e4a88cd05c00ea8f546220918e3ebb6f8c1c36b70ef04a5ac75f0d7160", size = 27599005, upload-time = "2026-05-14T03:43:50.926Z" }, + { url = "https://files.pythonhosted.org/packages/5f/40/bf510c8758727df020f83b717ebc1fcc1739ed7f6ae1796ebef60bf6f592/wandb-0.27.0-py3-none-win32.whl", hash = "sha256:0bd5659417e386bf6538b5e2ffe6885774c6197f0e4853bfed517d5b0db457f1", size = 25036164, upload-time = "2026-05-14T03:43:54.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/ff/69f88e7d90c22b79bcb911143c13e59742ee192080b21015ff83a5a1f60a/wandb-0.27.0-py3-none-win_amd64.whl", hash = "sha256:89d584b73166eecee96fb446f18d0e45b1aa45aba6a3696296f3f06d7454516b", size = 25036170, upload-time = "2026-05-14T03:43:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/38/f7efd7a87297a55c7e9a331a1dbb5b19e54aeacc11fe6f43f8636a73987c/wandb-0.27.0-py3-none-win_arm64.whl", hash = "sha256:a6c129c311edf210a2b4f2f4acc557eff522628125f5f28ed27df19c16c07079", size = 22972710, upload-time = "2026-05-14T03:44:03.275Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + +[[package]] +name = "wrapt" +version = "1.14.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/49/925324e2eaa0c83c45b7a1eb31004af4ad7b37fdc1d9e897ea7d551215da/wrapt-1.14.2.tar.gz", hash = "sha256:19d33781d891c99b5a35f810656d2fe01765b35ccfb6d395007d2edb1058d98f", size = 50701, upload-time = "2025-08-12T06:59:02.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/12f70730cf9237e07aa5887bdb3e3695fd68e7e0d6dd66b5dd7a8794724a/wrapt-1.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:734de7e0182d15f03eb4f4fce0ff7f07a072119a19cdcc97c40aee96d10fe851", size = 35836, upload-time = "2025-08-12T06:58:20.535Z" }, + { url = "https://files.pythonhosted.org/packages/f1/95/5e261ed720bdddf9bbfea2b9bbd30bd03e298b02267333a231592a0cef7b/wrapt-1.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:851ded3662733bab093297a0adb844c71e7754b8c144f76bb569ac6544a7c27a", size = 76907, upload-time = "2025-08-12T06:58:39.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/fcf75886924473ebc11ded6f589f9f715bd605a787201e8b9b1f34b958a7/wrapt-1.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f7b9a0364edef5a3033307cde69f7c3775a74eb83caeff8955e72ec3e0963fe", size = 77665, upload-time = "2025-08-12T06:58:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/16/de/e9205650d0c8401c4e2cc2a01edc89ce3fabda219e2efabcc71d92d02a70/wrapt-1.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3066203bf6e35a040c848143636e6fc2aa4ad6fb2b027a956f462083a3b635ea", size = 76401, upload-time = "2025-08-12T06:58:30.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e6/e3b79ba90e7b3a7c1915dbecb84aa491dc0a16238216dae5137bb0d85689/wrapt-1.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:072684f6264ca83695d1c7ad601e61b17953351b0950790cd50c327402006492", size = 76360, upload-time = "2025-08-12T06:58:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/35/07/fe53594e194b133b29b0808c40f1ad92773186c314dbb5b1bf5bb9a4e34a/wrapt-1.14.2-cp310-cp310-win32.whl", hash = "sha256:26c36d922c70de7ae7e933d0f9a10b4017d007d505f60fe02efa8e260a6c7af4", size = 33681, upload-time = "2025-08-12T06:58:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/12/68/134031baba6e1ba7dbdf2a217e99feb7177a2ad860b19f3ba319d40e0baf/wrapt-1.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:ead0c5373df2c551347c7f71903a6403efb391506e1e349704ec21fe797e59bf", size = 35811, upload-time = "2025-08-12T06:58:51.696Z" }, + { url = "https://files.pythonhosted.org/packages/16/ea/7ab441022d98da4cd4ff6d2ec6cbd8e4bf69085f261bdf72971a43bd8a45/wrapt-1.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3913516545546d3ff82ffe5bb4a7eab29dc7f1c71cf7329a1494659dc2a60b7b", size = 35832, upload-time = "2025-08-12T06:58:22.674Z" }, + { url = "https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:122015da6e0a1d09e1f9fbb8c756b9a8802e6cb01138f5256c121602bc7c7399", size = 77384, upload-time = "2025-08-12T06:58:41.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/4057df32059ea4782cf34b7a9dc08c6b29c4e29ecccfa58b32a12eb77582/wrapt-1.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:257a53f69737f340d0ba6e4d5554b56a579ca884de6eada15a7126baae8dacc0", size = 78135, upload-time = "2025-08-12T06:58:31.259Z" }, + { url = "https://files.pythonhosted.org/packages/37/80/d688dfde8c9d75b41c6f01a71e6f64a77861b6ad67e04d622d78060d638c/wrapt-1.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e60ca4aa80a85a0bc24cdf0f971d736f6d0957bdbc93874339027cfb0f286c99", size = 76933, upload-time = "2025-08-12T06:58:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/2b5f8b11c463f75acb6ca710a2ac2e40549f6b5d68bc85c135fc49a7ef19/wrapt-1.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f23bfecdc0e139f15b195ca3601e71a1388dc058bdbbd160ba9d0f4a17b4a12d", size = 76873, upload-time = "2025-08-12T06:58:42.64Z" }, + { url = "https://files.pythonhosted.org/packages/28/d4/003f22d726ca8a0bc2170cad7651dad732f3d3a6e4f69d1f13d8244a93e9/wrapt-1.14.2-cp311-cp311-win32.whl", hash = "sha256:cb5aee915d67441bd07acbce9e4cab1d01a110b4e5f02932e80e5ef4c9411ae2", size = 33675, upload-time = "2025-08-12T06:58:54.885Z" }, + { url = "https://files.pythonhosted.org/packages/8e/61/8eb200c6cf899fedc677a7a01b2f189ad58f1d154d0ef84dab30800b9f01/wrapt-1.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e9773691347205f7d11e7c4395561633285c8d1d691ebfc73ffd64f9072aa22e", size = 35811, upload-time = "2025-08-12T06:58:53.921Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c7/7376998449689cf2adbdbeacad47084410d00f3ae04cf73e6127cf52b950/wrapt-1.14.2-py3-none-any.whl", hash = "sha256:82eea3b559f51f22aefc530b747b87406811ef00cb0c4734f48cb139c748db63", size = 21577, upload-time = "2025-08-12T06:59:01.408Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +]