diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..57d083009957dd371fb67cee54b80d356b10fe7e --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 100 +ignore = E203 E741 W503 E731 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..9141c54ac6d024b319eb9c68a3983a497dc46bf1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c6ea656430ce2fbfd061bb6bbb361b7766bd181 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,81 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 + +name: Bug Report +description: Report a bug in awesome-depth-anything-3 +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting! Please fill out the form below. + + **Note**: For issues with the model itself (accuracy, artifacts), please report to the [upstream repository](https://github.com/ByteDance-Seed/Depth-Anything-3/issues). + + - type: textarea + id: description + attributes: + label: Bug Description + description: What happened? What did you expect to happen? + placeholder: Describe the bug... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Minimal code or steps to reproduce the issue + placeholder: | + ```python + from depth_anything_3.api import DepthAnything3 + model = DepthAnything3.from_pretrained("depth-anything/DA3-LARGE") + # ... + ``` + validations: + required: true + + - type: textarea + id: traceback + attributes: + label: Error Traceback + description: Full error message and traceback + render: shell + placeholder: Paste the full traceback here... + + - type: input + id: version + attributes: + label: Package Version + placeholder: "0.1.0" + validations: + required: true + + - type: dropdown + id: device + attributes: + label: Device + options: + - CUDA (NVIDIA GPU) + - MPS (Apple Silicon) + - CPU + validations: + required: true + + - type: input + id: pytorch + attributes: + label: PyTorch Version + placeholder: "2.9.0" + + - type: input + id: python + attributes: + label: Python Version + placeholder: "3.11" + + - type: input + id: os + attributes: + label: Operating System + placeholder: "macOS 14.0 / Ubuntu 22.04 / Windows 11" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..5172f095d218588d40274d6f22d07e98bf73fbd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Upstream Repository + url: https://github.com/ByteDance-Seed/Depth-Anything-3/issues + about: For issues with the model architecture, accuracy, or training + - name: Discussions + url: https://github.com/Aedelon/awesome-depth-anything-3/discussions + about: Ask questions or share ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000000000000000000000000000000000..0a01f833b87037c2b06561ab4270af51e4c26ec3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,61 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 + +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for your suggestion! + + **Note**: For model/architecture changes, please suggest to the [upstream repository](https://github.com/ByteDance-Seed/Depth-Anything-3/issues). + This fork focuses on optimization, deployment, and developer experience. + + - type: textarea + id: problem + attributes: + label: Problem / Use Case + description: What problem does this solve? What are you trying to do? + placeholder: I'm trying to... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How would you like it to work? + placeholder: It would be great if... + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any other approaches you've considered? + placeholder: I also thought about... + + - type: dropdown + id: category + attributes: + label: Category + options: + - Performance optimization + - CLI improvement + - API enhancement + - Documentation + - Testing + - CI/CD + - Other + validations: + required: true + + - type: checkboxes + id: contribution + attributes: + label: Contribution + options: + - label: I would be willing to submit a PR for this feature diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..5ca6b850026110a7bfc5d56b2495af6ab604f45d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 + +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.11 + + - name: Install dependencies + run: uv sync --extra dev + + - name: Lint with ruff + run: uv run ruff check src/ + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --extra dev + + - name: Run tests + run: uv run pytest tests/ -v --tb=short -x + env: + PYTORCH_ENABLE_MPS_FALLBACK: "1" + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for hatch-vcs + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.11 + + - name: Build package + run: uv build + + - name: Check package + run: uvx twine check dist/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000000000000000000000000000000000..671c6a69ff13feec23776d104f37197553870155 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,34 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 + +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # Required for trusted publishing + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for hatch-vcs version + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.11 + + - name: Build package + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # Uses trusted publishing (OIDC) - no API token needed + # Configure at: https://pypi.org/manage/project/awesome-depth-anything-3/settings/publishing/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42557f1d41d01ef128e4a03e9d2353a3555289d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Distribution / packaging +workspace/ +build/ +dist/ +*.egg-info/ +.eggs/ +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ + +# Test/coverage +.coverage +.pytest_cache/ +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover + +# Jupyter notebooks +.ipynb_checkpoints/ + +# IDE +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Project-specific +gallery*/ +debug*/ +DA3HF*/ +gradio_workspace/ +eval_workspace/ +FILTER*/ +input_images*/ +*.gradio/ +.gradio/ +src/debug_main.py +temp*.png +/outputs + +# Model weights and large files +*.pt +*.pth +*.ckpt +*.safetensors +!assets/**/*.pt + +# Logs +*.log +logs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3d9935935aee777bc71316f2e999759f8e612df2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +repos: + - repo: 'https://github.com/pre-commit/pre-commit-hooks' + rev: v4.5.0 + hooks: + - id: check-added-large-files + args: + - '--maxkb=125' + - id: check-ast + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: no-commit-to-branch + args: + - '--branch' + - 'master' + - id: pretty-format-json + exclude: '.*\.ipynb$' + args: + - '--autofix' + - '--indent' + - '4' + - id: trailing-whitespace + args: + - '--markdown-linebreak-ext=md' + - repo: 'https://github.com/pycqa/isort' + rev: 5.13.2 + hooks: + - id: isort + args: + - '--settings-file' + - 'pyproject.toml' + - '--filter-files' + - repo: 'https://github.com/asottile/pyupgrade' + rev: v3.15.2 + hooks: + - id: pyupgrade + args: [--py38-plus, --keep-runtime-typing] + - repo: 'https://github.com/psf/black.git' + rev: 24.3.0 + hooks: + - id: black + args: + - '--config=pyproject.toml' + - repo: 'https://github.com/PyCQA/flake8' + rev: 7.0.0 + hooks: + - id: flake8 + args: + - '--config=.flake8' + - repo: 'https://github.com/myint/autoflake' + rev: v1.4 + hooks: + - id: autoflake + args: [ '--remove-all-unused-imports', '--recursive', '--remove-unused-variables', '--in-place'] diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 0000000000000000000000000000000000000000..e48b06c6d33c40e68d214fbb53cf1ae0ba453394 --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,217 @@ +# Benchmark Results + +Performance benchmarks comparing **awesome-depth-anything-3** (optimized fork) against the vanilla upstream implementation. + +> **Test Environment**: Apple Silicon (M-series), PyTorch 2.9.0 +> **Models**: da3-small, da3-base, da3-large, da3-giant + +--- + +## Quick Summary + +| Feature | Improvement | +|---------|-------------| +| Model Loading (cached) | **200x faster** (0.8s → 0.005s) | +| Inference (MPS, batch 4) | **1.14x faster** | +| Cold Load Time | **1.7x faster** | +| Memory Efficiency | Adaptive batching prevents OOM | + +--- + +## 1. Awesome vs Upstream Comparison + +Direct comparison between this optimized fork and the original upstream repository. + +### MPS (Apple Silicon GPU) + +| Batch Size | Upstream | Awesome | Speedup | Notes | +|------------|----------|---------|---------|-------| +| 1 | 3.47 img/s | 3.50 img/s | 1.01x | Minimal overhead | +| 2 | 3.64 img/s | 3.83 img/s | 1.05x | Batching benefits | +| **4** | 3.32 img/s | 3.78 img/s | **1.14x** | Best improvement | + +#### Model Loading Performance + +| Metric | Upstream | Awesome | Speedup | +|--------|----------|---------|---------| +| Cold Load | 1.28s | 0.77s | **1.7x** | +| Cached Load | N/A | 0.005s | **~200x** | + +The model caching system is the standout feature - after the first load, subsequent loads are essentially instant. + +### CPU + +| Batch Size | Upstream | Awesome | Speedup | +|------------|----------|---------|---------| +| 1 | 0.27 img/s | 0.31 img/s | 1.13x | +| 2 | 0.24 img/s | 0.24 img/s | 1.00x | +| 4 | 0.17 img/s | 0.16 img/s | 0.95x | + +> **Note**: CPU performance is similar between versions since GPU-specific optimizations don't apply. The slight regression at batch 4 is within measurement noise. + +--- + +## 2. Model Performance by Size + +Throughput benchmarks on MPS (Apple Silicon) with 1280x720 input images. + +| Model | Parameters | Batch 1 | Batch 4 | Best Config | +|-------|------------|---------|---------|-------------| +| **da3-small** | ~25M | 22.2 img/s | 27.2 img/s | B=4 SDPA | +| **da3-base** | ~100M | 10.7 img/s | 11.6 img/s | B=4 SDPA | +| **da3-large** | ~335M | 3.8 img/s | 3.8 img/s | B=1-2 | +| **da3-giant** | ~1.1B | 1.6 img/s | 1.2 img/s | B=1 | + +### Latency (single image) + +| Model | MPS | CPU | MPS Speedup | +|-------|-----|-----|-------------| +| da3-small | 45 ms | ~3,500 ms | ~78x | +| da3-base | 94 ms | ~7,000 ms | ~74x | +| da3-large | 265 ms | ~3,900 ms | ~15x | +| da3-giant | 618 ms | N/A | - | + +--- + +## 3. Preprocessing Pipeline + +### Strategy: Hybrid CPU/GPU + +On Apple Silicon, **CPU preprocessing is faster** than GPU (Kornia) due to optimized OpenCV/Accelerate routines. The overhead of MPS kernel launches exceeds the benefit for image transforms. + +| Resolution | CPU Time | GPU Time | Winner | +|------------|----------|----------|--------| +| 640x480 | 6.0 ms | N/A | CPU | +| 1920x1080 | 18.7 ms | N/A | CPU | +| 3840x2160 | 57.0 ms | N/A | CPU | + +> **Design Decision**: GPU preprocessing is automatically disabled on MPS. The GPU is reserved for model inference where it provides 15-78x speedup. + +### CUDA (NVIDIA) + +On CUDA, GPU preprocessing with NVJPEG provides significant benefits for JPEG decoding directly to GPU memory, eliminating CPU→GPU transfer overhead. + +--- + +## 4. Attention Mechanisms + +Comparison between SDPA (Scaled Dot-Product Attention / Flash Attention) and manual attention implementation. + +### Per-Layer Performance + +| Config | SDPA | Manual | Speedup | +|--------|------|--------|---------| +| ViT-L 518px (MPS) | 2.21 ms | 1.86 ms | 0.8x | +| ViT-L 1024px (MPS) | 9.91 ms | 5.87 ms | 0.6x | +| ViT-L 518px (CPU) | 3.75 ms | 4.96 ms | 1.3x | +| ViT-L 1024px (CPU) | 11.73 ms | 16.85 ms | 1.4x | + +> **Insight**: On MPS, manual attention is faster for ViT due to MPS's SDPA implementation overhead. On CPU, SDPA benefits from optimized BLAS operations. + +### End-to-End Impact + +| Model | SDPA | Manual | Best | +|-------|------|--------|------| +| da3-small | 21.8 img/s | 22.2 img/s | Manual | +| da3-base | 9.8 img/s | 10.7 img/s | Manual | +| da3-large | 3.8 img/s | 3.7 img/s | SDPA | +| da3-giant | 1.6 img/s | 1.6 img/s | Tie | + +--- + +## 5. Adaptive Batching + +The adaptive batching system dynamically adjusts batch size based on available GPU memory. + +### Test: 20 images with da3-large on MPS + +| Strategy | Total Time | Throughput | Batches Used | +|----------|------------|------------|--------------| +| Fixed B=1 | 5,612 ms | 3.6 img/s | [1,1,1...] | +| Fixed B=2 | 5,514 ms | **3.6 img/s** | [2,2,2...] | +| Fixed B=4 | 8,305 ms | 2.4 img/s | [4,4,4,4,4] | +| Adaptive 85% | 5,637 ms | 3.5 img/s | [4,4,4...] | + +> **Recommendation**: For MPS with da3-large, fixed batch size of 2 provides optimal throughput. Adaptive batching is more valuable for: +> - Variable input sizes +> - Unknown GPU memory constraints +> - Preventing OOM errors on smaller GPUs + +--- + +## 6. Cross-Device Comparison + +### Inference Throughput (da3-large, batch=1) + +``` +MPS (Apple Silicon) ████████████████████████████████████████ 3.7 img/s +CPU ███ 0.3 img/s +``` + +**MPS provides ~12x speedup over CPU** for da3-large inference. + +### Attention Layer (ViT-L 518px, SDPA) + +``` +MPS ████████████████████████ 2.40 ms +CPU ███████████████████████████████████████ 3.75 ms +``` + +--- + +## 7. Optimization Recommendations + +### For Apple Silicon (MPS) + +1. **Use model caching** - 200x faster subsequent loads +2. **Batch size 2-4** for da3-small/base, **batch 1-2** for da3-large/giant +3. **Let CPU handle preprocessing** - it's faster than MPS for image transforms +4. **SDPA vs Manual**: Both are similar; SDPA slightly better for larger models + +### For NVIDIA CUDA + +1. **Enable GPU preprocessing** with NVJPEG for JPEG inputs +2. **Use SDPA** (Flash Attention) - significant speedup +3. **Larger batch sizes** benefit more from GPU parallelism +4. **Adaptive batching** to maximize VRAM utilization + +### For CPU-only + +1. **Use smallest viable model** (da3-small: 22x faster than da3-giant) +2. **Batch size 1** is optimal (memory bandwidth limited) +3. **SDPA provides 1.3-1.4x speedup** on CPU + +--- + +## Running Benchmarks + +```bash +# Quick benchmark (fewer iterations) +uv run python benchmarks/full_benchmark.py --quick + +# Full benchmark on specific device +uv run python benchmarks/full_benchmark.py --device mps +uv run python benchmarks/full_benchmark.py --device cuda +uv run python benchmarks/full_benchmark.py --device cpu + +# Compare against upstream (requires upstream repo) +uv run python benchmarks/comparative_benchmark.py --device all + +# Skip specific tests +uv run python benchmarks/full_benchmark.py --skip-batching +``` + +--- + +## Methodology + +- **Warmup**: 2 inference passes before timing +- **Runs**: 3-5 iterations per configuration +- **Synchronization**: `torch.mps.synchronize()` / `torch.cuda.synchronize()` for accurate GPU timing +- **Memory cleanup**: `gc.collect()` + cache clearing between tests +- **Input**: Synthetic 1280x720 RGB images (consistent across tests) + +--- + +*Benchmarks last updated: December 2024* +*Hardware: Apple Silicon (M-series) | Software: PyTorch 2.9.0* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..7e6f39f27d7e68ae66d5cb73f4967cd90938155c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2024-12-03 + +### Added + +- **Model Caching**: ~200x faster model loading after first use via `ModelCache` singleton +- **Adaptive Batching**: Automatic batch size optimization based on available GPU memory + - `batch_inference()` method with `batch_size="auto"` option + - `get_optimal_batch_size()` for memory-aware batch sizing +- **CLI Batching Options**: `--batch-size`, `--max-batch-size`, `--target-memory-utilization` +- **Apple Silicon Optimizations**: Smart CPU/GPU preprocessing selection for MPS +- **GPU Preprocessing**: Kornia-based GPU preprocessing with NVJPEG support on CUDA +- **Comprehensive Benchmarks**: Performance comparison scripts and documentation +- **PyPI Package**: Published as `awesome-depth-anything-3` +- **CI/CD**: GitHub Actions for testing, linting, and PyPI publishing +- **HF Spaces Demo**: Interactive Gradio demo on Hugging Face +- **Colab Tutorial**: Interactive notebook with examples + +### Changed + +- Package renamed from `depth-anything-3` to `awesome-depth-anything-3` +- Improved error handling in CLI commands +- Better logging with configurable levels + +### Credits + +This package is an optimized fork of [Depth Anything 3](https://github.com/ByteDance-Seed/Depth-Anything-3) +by ByteDance. All model architecture and weights are their work. See README for full attribution. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..78eec8dd04e8b61bfc8f707633808268f4f93412 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to awesome-depth-anything-3 + +Thank you for your interest in contributing! This document provides guidelines for contributing to this project. + +## Important Note + +This is an **optimized fork** of [Depth Anything 3](https://github.com/ByteDance-Seed/Depth-Anything-3) by ByteDance. + +- **Model/architecture changes** should be proposed to the [upstream repository](https://github.com/ByteDance-Seed/Depth-Anything-3) +- **Optimization/deployment improvements** are welcome here + +## Development Setup + +```bash +# Clone the repository +git clone https://github.com/Aedelon/awesome-depth-anything-3.git +cd awesome-depth-anything-3 + +# Install with development dependencies (using uv) +uv sync --extra dev + +# Or with pip +pip install -e ".[dev]" +``` + +## Running Tests + +```bash +# Run all tests +uv run pytest tests/ -v + +# Run specific test file +uv run pytest tests/test_adaptive_batching.py -v + +# Run with coverage +uv run pytest tests/ --cov=src/depth_anything_3 +``` + +## Code Style + +We use `ruff` for linting and formatting: + +```bash +# Check for issues +uv run ruff check src/ + +# Auto-fix issues +uv run ruff check src/ --fix + +# Format code +uv run ruff format src/ +``` + +## Pre-commit Hooks + +We recommend using pre-commit hooks: + +```bash +uv run pre-commit install +uv run pre-commit run --all-files +``` + +## Pull Request Process + +1. **Fork** the repository +2. **Create a branch** for your feature (`git checkout -b feature/amazing-feature`) +3. **Make your changes** with clear, descriptive commits +4. **Run tests** and linting +5. **Update documentation** if needed +6. **Push** to your fork and **open a Pull Request** + +### PR Guidelines + +- Keep PRs focused on a single change +- Include tests for new functionality +- Update CHANGELOG.md for user-facing changes +- Ensure CI passes before requesting review + +## Types of Contributions Welcome + +### Highly Welcome + +- Performance optimizations +- Bug fixes +- Documentation improvements +- Test coverage improvements +- CI/CD improvements +- Device compatibility (CUDA, MPS, CPU) + +### Discuss First + +- New CLI commands +- API changes +- New dependencies + +### Redirect to Upstream + +- Model architecture changes +- Training code changes +- New model variants + +## Reporting Issues + +When reporting bugs, please include: + +- Python version +- PyTorch version +- Device type (CUDA/MPS/CPU) +- Minimal reproduction code +- Full error traceback + +## License + +By contributing, you agree that your contributions will be licensed under the Apache License 2.0. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..a2fa36578f5094f56f4b0ec57e70eb30aced4519 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on + the same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 The Depth Anything 3 Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ac1a6caeeb7e923e4c754435ea49a984bf60843e --- /dev/null +++ b/README.md @@ -0,0 +1,418 @@ +--- +title: Awesome Depth Anything 3 +emoji: 🌊 +colorFrom: blue +colorTo: purple +sdk: gradio +sdk_version: 5.50.0 +app_file: app.py +pinned: false +license: apache-2.0 +short_description: Metric 3D reconstruction from images/video +--- + +
+ +# Awesome Depth Anything 3 + +**Optimized fork of Depth Anything 3 with production-ready features** + +[![PyPI](https://img.shields.io/pypi/v/awesome-depth-anything-3)](https://pypi.org/project/awesome-depth-anything-3/) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE) +[![Tests](https://github.com/Aedelon/awesome-depth-anything-3/actions/workflows/ci.yml/badge.svg)](https://github.com/Aedelon/awesome-depth-anything-3/actions) +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Aedelon/awesome-depth-anything-3/blob/main/notebooks/da3_tutorial.ipynb) +[![HF Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/Aedelon/awesome-depth-anything-3) + +[Demo](https://huggingface.co/spaces/Aedelon/awesome-depth-anything-3) · [Tutorial](notebooks/da3_tutorial.ipynb) · [Benchmarks](BENCHMARKS.md) · [Original Paper](https://arxiv.org/abs/2511.10647) + +
+ +--- + +> **This is an optimized fork** of [Depth Anything 3](https://github.com/ByteDance-Seed/Depth-Anything-3) by ByteDance. +> All credit for the model architecture, training, and research goes to the original authors (see [Credits](#-credits) below). +> This fork focuses on **production optimization, developer experience, and ease of deployment**. + +## 🚀 What's New in This Fork + +| Feature | Description | +|---------|-------------| +| **Model Caching** | ~200x faster model loading after first use | +| **Adaptive Batching** | Automatic batch size optimization based on GPU memory | +| **PyPI Package** | `pip install awesome-depth-anything-3` | +| **CLI Improvements** | Batch processing options, better error handling | +| **Apple Silicon Optimized** | Smart CPU/GPU preprocessing for best MPS performance | +| **Comprehensive Benchmarks** | Detailed performance analysis across devices | + +### Performance Improvements + +| Metric | Upstream | This Fork | Improvement | +|--------|----------|-----------|-------------| +| Cached model load | ~1s | ~5ms | **200x faster** | +| Batch 4 inference (MPS) | 3.32 img/s | 3.78 img/s | **1.14x faster** | +| Cold model load | 1.28s | 0.77s | **1.7x faster** | + +--- + +
+ +## Original Depth Anything 3 + +

Recovering the Visual Space from Any Views

+ +[**Haotong Lin**](https://haotongl.github.io/)* · [**Sili Chen**](https://github.com/SiliChen321)* · [**Jun Hao Liew**](https://liewjunhao.github.io/)* · [**Donny Y. Chen**](https://donydchen.github.io)* · [**Zhenyu Li**](https://zhyever.github.io/) · [**Guang Shi**](https://scholar.google.com/citations?user=MjXxWbUAAAAJ&hl=en) · [**Jiashi Feng**](https://scholar.google.com.sg/citations?user=Q8iay0gAAAAJ&hl=en) +
+[**Bingyi Kang**](https://bingykang.github.io/)*† + +†project lead *Equal Contribution + +Paper PDF +Project Page + + +
+ +This work presents **Depth Anything 3 (DA3)**, a model that predicts spatially consistent geometry from +arbitrary visual inputs, with or without known camera poses. +In pursuit of minimal modeling, DA3 yields two key insights: +- 💎 A **single plain transformer** (e.g., vanilla DINO encoder) is sufficient as a backbone without architectural specialization, +- ✨ A singular **depth-ray representation** obviates the need for complex multi-task learning. + +🏆 DA3 significantly outperforms +[DA2](https://github.com/DepthAnything/Depth-Anything-V2) for monocular depth estimation, +and [VGGT](https://github.com/facebookresearch/vggt) for multi-view depth estimation and pose estimation. +All models are trained exclusively on **public academic datasets**. + + +

+ Depth Anything 3 - Left +

+

+ Depth Anything 3 +

+ + +## 📰 News +- **30-11-2025:** Add [`use_ray_pose`](#use-ray-pose) and [`ref_view_strategy`](docs/funcs/ref_view_strategy.md) (reference view selection for multi-view inputs). +- **25-11-2025:** Add [Awesome DA3 Projects](#-awesome-da3-projects), a community-driven section featuring DA3-based applications. +- **14-11-2025:** Paper, project page, code and models are all released. + +## ✨ Highlights + +### 🏆 Model Zoo +We release three series of models, each tailored for specific use cases in visual geometry. + +- 🌟 **DA3 Main Series** (`DA3-Giant`, `DA3-Large`, `DA3-Base`, `DA3-Small`) These are our flagship foundation models, trained with a unified depth-ray representation. By varying the input configuration, a single model can perform a wide range of tasks: + + 🌊 **Monocular Depth Estimation**: Predicts a depth map from a single RGB image. + + 🌊 **Multi-View Depth Estimation**: Generates consistent depth maps from multiple images for high-quality fusion. + + 🎯 **Pose-Conditioned Depth Estimation**: Achieves superior depth consistency when camera poses are provided as input. + + 📷 **Camera Pose Estimation**: Estimates camera extrinsics and intrinsics from one or more images. + + 🟡 **3D Gaussian Estimation**: Directly predicts 3D Gaussians, enabling high-fidelity novel view synthesis. + +- 📐 **DA3 Metric Series** (`DA3Metric-Large`) A specialized model fine-tuned for metric depth estimation in monocular settings, ideal for applications requiring real-world scale. + +- 🔍 **DA3 Monocular Series** (`DA3Mono-Large`). A dedicated model for high-quality relative monocular depth estimation. Unlike disparity-based models (e.g., [Depth Anything 2](https://github.com/DepthAnything/Depth-Anything-V2)), it directly predicts depth, resulting in superior geometric accuracy. + +🔗 Leveraging these available models, we developed a **nested series** (`DA3Nested-Giant-Large`). This series combines a any-view giant model with a metric model to reconstruct visual geometry at a real-world metric scale. + +### 🛠️ Codebase Features +Our repository is designed to be a powerful and user-friendly toolkit for both practical application and future research. +- 🎨 **Interactive Web UI & Gallery**: Visualize model outputs and compare results with an easy-to-use Gradio-based web interface. +- ⚡ **Flexible Command-Line Interface (CLI)**: Powerful and scriptable CLI for batch processing and integration into custom workflows. +- 💾 **Multiple Export Formats**: Save your results in various formats, including `glb`, `npz`, depth images, `ply`, 3DGS videos, etc, to seamlessly connect with other tools. +- 🔧 **Extensible and Modular Design**: The codebase is structured to facilitate future research and the integration of new models or functionalities. + + + + + +## 🚀 Quick Start + +### 📦 Installation + +```bash +# From PyPI (recommended) +pip install awesome-depth-anything-3 + +# With Gradio web UI +pip install awesome-depth-anything-3[app] + +# With CUDA optimizations (xformers + gsplat) +pip install awesome-depth-anything-3[cuda] + +# Everything +pip install awesome-depth-anything-3[all] +``` + +
+Development installation + +```bash +git clone https://github.com/Aedelon/awesome-depth-anything-3.git +cd awesome-depth-anything-3 +pip install -e ".[dev]" + +# Optional: 3D Gaussian Splatting head +pip install --no-build-isolation git+https://github.com/nerfstudio-project/gsplat.git@0b4dddf +``` +
+ +For detailed model information, please refer to the [Model Cards](#-model-cards) section below. + +### 💻 Basic Usage + +```python +import glob, os, torch +from depth_anything_3.api import DepthAnything3 +device = torch.device("cuda") +model = DepthAnything3.from_pretrained("depth-anything/DA3NESTED-GIANT-LARGE") +model = model.to(device=device) +example_path = "assets/examples/SOH" +images = sorted(glob.glob(os.path.join(example_path, "*.png"))) +prediction = model.inference( + images, +) +# prediction.processed_images : [N, H, W, 3] uint8 array +print(prediction.processed_images.shape) +# prediction.depth : [N, H, W] float32 array +print(prediction.depth.shape) +# prediction.conf : [N, H, W] float32 array +print(prediction.conf.shape) +# prediction.extrinsics : [N, 3, 4] float32 array # opencv w2c or colmap format +print(prediction.extrinsics.shape) +# prediction.intrinsics : [N, 3, 3] float32 array +print(prediction.intrinsics.shape) +``` + +```bash + +export MODEL_DIR=depth-anything/DA3NESTED-GIANT-LARGE +# This can be a Hugging Face repository or a local directory +# If you encounter network issues, consider using the following mirror: export HF_ENDPOINT=https://hf-mirror.com +# Alternatively, you can download the model directly from Hugging Face +export GALLERY_DIR=workspace/gallery +mkdir -p $GALLERY_DIR + +# CLI auto mode with backend reuse +da3 backend --model-dir ${MODEL_DIR} --gallery-dir ${GALLERY_DIR} # Cache model to gpu +da3 auto assets/examples/SOH \ + --export-format glb \ + --export-dir ${GALLERY_DIR}/TEST_BACKEND/SOH \ + --use-backend + +# CLI video processing with feature visualization +da3 video assets/examples/robot_unitree.mp4 \ + --fps 15 \ + --use-backend \ + --export-dir ${GALLERY_DIR}/TEST_BACKEND/robo \ + --export-format glb-feat_vis \ + --feat-vis-fps 15 \ + --process-res-method lower_bound_resize \ + --export-feat "11,21,31" + +# CLI auto mode without backend reuse +da3 auto assets/examples/SOH \ + --export-format glb \ + --export-dir ${GALLERY_DIR}/TEST_CLI/SOH \ + --model-dir ${MODEL_DIR} + +``` + +The model architecture is defined in [`DepthAnything3Net`](src/depth_anything_3/model/da3.py), and specified with a Yaml config file located at [`src/depth_anything_3/configs`](src/depth_anything_3/configs). The input and output processing are handled by [`DepthAnything3`](src/depth_anything_3/api.py). To customize the model architecture, simply create a new config file (*e.g.*, `path/to/new/config`) as: + +```yaml +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitb + out_layers: [5, 7, 9, 11] + alt_start: 4 + qknorm_start: 4 + rope_start: 4 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 1536 + output_dim: 2 + features: &head_features 128 + out_channels: &head_out_channels [96, 192, 384, 768] +``` + +Then, the model can be created with the following code snippet. +```python +from depth_anything_3.cfg import create_object, load_config + +Model = create_object(load_config("path/to/new/config")) +``` + + + +## 📚 Useful Documentation + +- 🖥️ [Command Line Interface](docs/CLI.md) +- 📑 [Python API](docs/API.md) + + +## 🗂️ Model Cards + +Generally, you should observe that DA3-LARGE achieves comparable results to VGGT. + +The Nested series uses an Any-view model to estimate pose and depth, and a monocular metric depth estimator for scaling. + +| 🗃️ Model Name | 📏 Params | 📊 Rel. Depth | 📷 Pose Est. | 🧭 Pose Cond. | 🎨 GS | 📐 Met. Depth | ☁️ Sky Seg | 📄 License | +|-------------------------------|-----------|---------------|--------------|---------------|-------|---------------|-----------|----------------| +| **Nested** | | | | | | | | | +| [DA3NESTED-GIANT-LARGE](https://huggingface.co/depth-anything/DA3NESTED-GIANT-LARGE) | 1.40B | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | CC BY-NC 4.0 | +| **Any-view Model** | | | | | | | | | +| [DA3-GIANT](https://huggingface.co/depth-anything/DA3-GIANT) | 1.15B | ✅ | ✅ | ✅ | ✅ | | | CC BY-NC 4.0 | +| [DA3-LARGE](https://huggingface.co/depth-anything/DA3-LARGE) | 0.35B | ✅ | ✅ | ✅ | | | | CC BY-NC 4.0 | +| [DA3-BASE](https://huggingface.co/depth-anything/DA3-BASE) | 0.12B | ✅ | ✅ | ✅ | | | | Apache 2.0 | +| [DA3-SMALL](https://huggingface.co/depth-anything/DA3-SMALL) | 0.08B | ✅ | ✅ | ✅ | | | | Apache 2.0 | +| | | | | | | | | | +| **Monocular Metric Depth** | | | | | | | | | +| [DA3METRIC-LARGE](https://huggingface.co/depth-anything/DA3METRIC-LARGE) | 0.35B | ✅ | | | | ✅ | ✅ | Apache 2.0 | +| | | | | | | | | | +| **Monocular Depth** | | | | | | | | | +| [DA3MONO-LARGE](https://huggingface.co/depth-anything/DA3MONO-LARGE) | 0.35B | ✅ | | | | | ✅ | Apache 2.0 | + + +## ⚡ Performance Benchmarks + +Inference throughput measured on Apple Silicon (MPS) with PyTorch 2.9.0. For detailed benchmarks, see [BENCHMARKS.md](BENCHMARKS.md). + +### Apple Silicon (MPS) - Batch Size 1 + +| Model | Latency | Throughput | +|-------|---------|------------| +| DA3-Small | 46 ms | **22 img/s** | +| DA3-Base | 93 ms | **11 img/s** | +| DA3-Large | 265 ms | **3.8 img/s** | +| DA3-Giant | 618 ms | **1.6 img/s** | + +### Cross-Device Comparison (DA3-Large) + +| Device | Throughput | vs CPU | +|--------|------------|--------| +| CPU | 0.3 img/s | 1.0x | +| Apple Silicon (MPS) | 3.8 img/s | **13x** | +| NVIDIA L4 (CUDA) | 10.3 img/s | **34x** | + +### Batch Processing + +```python +from depth_anything_3.api import DepthAnything3 + +model = DepthAnything3.from_pretrained("depth-anything/DA3-LARGE") + +# Adaptive batching (recommended for large image sets) +results = model.batch_inference( + images=image_paths, + batch_size="auto", # Automatically selects optimal batch size + target_memory_utilization=0.85, +) + +# Fixed batch size +results = model.batch_inference( + images=image_paths, + batch_size=4, +) +``` + +> See [BENCHMARKS.md](BENCHMARKS.md) for comprehensive benchmarks including preprocessing, attention mechanisms, and adaptive batching strategies. + + +## ❓ FAQ + +- **Monocular Metric Depth**: To obtain metric depth in meters from `DA3METRIC-LARGE`, use `metric_depth = focal * net_output / 300.`, where `focal` is the focal length in pixels (typically the average of fx and fy from the camera intrinsic matrix K). Note that the output from `DA3NESTED-GIANT-LARGE` is already in meters. + +- **Ray Head (`use_ray_pose`)**: Our API and CLI support `use_ray_pose` arg, which means that the model will derive camera pose from ray head, which is generally slightly slower, but more accurate. Note that the default is `False` for faster inference speed. +
+ AUC3 Results for DA3NESTED-GIANT-LARGE + + | Model | HiRoom | ETH3D | DTU | 7Scenes | ScanNet++ | + |-------|------|-------|-----|---------|-----------| + | `ray_head` | 84.4 | 52.6 | 93.9 | 29.5 | 89.4 | + | `cam_head` | 80.3 | 48.4 | 94.1 | 28.5 | 85.0 | + +
+ + + + +- **Older GPUs without XFormers support**: See [Issue #11](https://github.com/ByteDance-Seed/Depth-Anything-3/issues/11). Thanks to [@S-Mahoney](https://github.com/S-Mahoney) for the solution! + + +## 🏢 Awesome DA3 Projects + +A community-curated list of Depth Anything 3 integrations across 3D tools, creative pipelines, robotics, and web/VR viewers, including but not limited to these. You are welcome to submit your DA3-based project via PR, and we will review and feature it if applicable. + +- [DA3-blender](https://github.com/xy-gao/DA3-blender): Blender addon for DA3-based 3D reconstruction from a set of images. + +- [ComfyUI-DepthAnythingV3](https://github.com/PozzettiAndrea/ComfyUI-DepthAnythingV3): ComfyUI nodes for Depth Anything 3, supporting single/multi-view and video-consistent depth with optional point‑cloud export. + +- [DA3-ROS2-Wrapper](https://github.com/GerdsenAI/GerdsenAI-Depth-Anything-3-ROS2-Wrapper): Real-time DA3 depth in ROS2 with multi-camera support. + +- [VideoDepthViewer3D](https://github.com/amariichi/VideoDepthViewer3D): Streaming videos with DA3 metric depth to a Three.js/WebXR 3D viewer for VR/stereo playback. + + +## 📝 Credits + +### Original Authors + +This package is built on top of **Depth Anything 3**, created by the ByteDance Seed team: + +- [Haotong Lin](https://haotongl.github.io/), [Sili Chen](https://github.com/SiliChen321), [Jun Hao Liew](https://liewjunhao.github.io/), [Donny Y. Chen](https://donydchen.github.io), [Zhenyu Li](https://zhyever.github.io/), [Guang Shi](https://scholar.google.com/citations?user=MjXxWbUAAAAJ), [Jiashi Feng](https://scholar.google.com.sg/citations?user=Q8iay0gAAAAJ), [Bingyi Kang](https://bingykang.github.io/) + +All model weights, architecture, and core algorithms are their work. This fork only adds production optimizations and deployment tooling. + +### Fork Maintainer + +This optimized fork is maintained by [Delanoe Pirard (Aedelon)](https://github.com/Aedelon). + +Contributions: +- Model caching system +- Adaptive batching +- Apple Silicon (MPS) optimizations +- PyPI packaging and CI/CD +- Comprehensive benchmarking + +### Citation + +If you use Depth Anything 3 in your research, please cite the original paper: + +```bibtex +@article{depthanything3, + title={Depth Anything 3: Recovering the visual space from any views}, + author={Haotong Lin and Sili Chen and Jun Hao Liew and Donny Y. Chen and Zhenyu Li and Guang Shi and Jiashi Feng and Bingyi Kang}, + journal={arXiv preprint arXiv:2511.10647}, + year={2025} +} +``` + +If you specifically use features from this fork (caching, batching, MPS optimizations), you may additionally reference: + +``` +awesome-depth-anything-3: https://github.com/Aedelon/awesome-depth-anything-3 +``` diff --git a/README.md.original b/README.md.original new file mode 100644 index 0000000000000000000000000000000000000000..bb10cca51618474a701ea402f9f074038e0cda78 --- /dev/null +++ b/README.md.original @@ -0,0 +1,405 @@ +
+ +# Awesome Depth Anything 3 + +**Optimized fork of Depth Anything 3 with production-ready features** + +[![PyPI](https://img.shields.io/pypi/v/awesome-depth-anything-3)](https://pypi.org/project/awesome-depth-anything-3/) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE) +[![Tests](https://github.com/Aedelon/awesome-depth-anything-3/actions/workflows/ci.yml/badge.svg)](https://github.com/Aedelon/awesome-depth-anything-3/actions) +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Aedelon/awesome-depth-anything-3/blob/main/notebooks/da3_tutorial.ipynb) +[![HF Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/Aedelon/awesome-depth-anything-3) + +[Demo](https://huggingface.co/spaces/Aedelon/awesome-depth-anything-3) · [Tutorial](notebooks/da3_tutorial.ipynb) · [Benchmarks](BENCHMARKS.md) · [Original Paper](https://arxiv.org/abs/2511.10647) + +
+ +--- + +> **This is an optimized fork** of [Depth Anything 3](https://github.com/ByteDance-Seed/Depth-Anything-3) by ByteDance. +> All credit for the model architecture, training, and research goes to the original authors (see [Credits](#-credits) below). +> This fork focuses on **production optimization, developer experience, and ease of deployment**. + +## 🚀 What's New in This Fork + +| Feature | Description | +|---------|-------------| +| **Model Caching** | ~200x faster model loading after first use | +| **Adaptive Batching** | Automatic batch size optimization based on GPU memory | +| **PyPI Package** | `pip install awesome-depth-anything-3` | +| **CLI Improvements** | Batch processing options, better error handling | +| **Apple Silicon Optimized** | Smart CPU/GPU preprocessing for best MPS performance | +| **Comprehensive Benchmarks** | Detailed performance analysis across devices | + +### Performance Improvements + +| Metric | Upstream | This Fork | Improvement | +|--------|----------|-----------|-------------| +| Cached model load | ~1s | ~5ms | **200x faster** | +| Batch 4 inference (MPS) | 3.32 img/s | 3.78 img/s | **1.14x faster** | +| Cold model load | 1.28s | 0.77s | **1.7x faster** | + +--- + +
+ +## Original Depth Anything 3 + +

Recovering the Visual Space from Any Views

+ +[**Haotong Lin**](https://haotongl.github.io/)* · [**Sili Chen**](https://github.com/SiliChen321)* · [**Jun Hao Liew**](https://liewjunhao.github.io/)* · [**Donny Y. Chen**](https://donydchen.github.io)* · [**Zhenyu Li**](https://zhyever.github.io/) · [**Guang Shi**](https://scholar.google.com/citations?user=MjXxWbUAAAAJ&hl=en) · [**Jiashi Feng**](https://scholar.google.com.sg/citations?user=Q8iay0gAAAAJ&hl=en) +
+[**Bingyi Kang**](https://bingykang.github.io/)*† + +†project lead *Equal Contribution + +Paper PDF +Project Page + + +
+ +This work presents **Depth Anything 3 (DA3)**, a model that predicts spatially consistent geometry from +arbitrary visual inputs, with or without known camera poses. +In pursuit of minimal modeling, DA3 yields two key insights: +- 💎 A **single plain transformer** (e.g., vanilla DINO encoder) is sufficient as a backbone without architectural specialization, +- ✨ A singular **depth-ray representation** obviates the need for complex multi-task learning. + +🏆 DA3 significantly outperforms +[DA2](https://github.com/DepthAnything/Depth-Anything-V2) for monocular depth estimation, +and [VGGT](https://github.com/facebookresearch/vggt) for multi-view depth estimation and pose estimation. +All models are trained exclusively on **public academic datasets**. + + +

+ Depth Anything 3 - Left +

+

+ Depth Anything 3 +

+ + +## 📰 News +- **30-11-2025:** Add [`use_ray_pose`](#use-ray-pose) and [`ref_view_strategy`](docs/funcs/ref_view_strategy.md) (reference view selection for multi-view inputs). +- **25-11-2025:** Add [Awesome DA3 Projects](#-awesome-da3-projects), a community-driven section featuring DA3-based applications. +- **14-11-2025:** Paper, project page, code and models are all released. + +## ✨ Highlights + +### 🏆 Model Zoo +We release three series of models, each tailored for specific use cases in visual geometry. + +- 🌟 **DA3 Main Series** (`DA3-Giant`, `DA3-Large`, `DA3-Base`, `DA3-Small`) These are our flagship foundation models, trained with a unified depth-ray representation. By varying the input configuration, a single model can perform a wide range of tasks: + + 🌊 **Monocular Depth Estimation**: Predicts a depth map from a single RGB image. + + 🌊 **Multi-View Depth Estimation**: Generates consistent depth maps from multiple images for high-quality fusion. + + 🎯 **Pose-Conditioned Depth Estimation**: Achieves superior depth consistency when camera poses are provided as input. + + 📷 **Camera Pose Estimation**: Estimates camera extrinsics and intrinsics from one or more images. + + 🟡 **3D Gaussian Estimation**: Directly predicts 3D Gaussians, enabling high-fidelity novel view synthesis. + +- 📐 **DA3 Metric Series** (`DA3Metric-Large`) A specialized model fine-tuned for metric depth estimation in monocular settings, ideal for applications requiring real-world scale. + +- 🔍 **DA3 Monocular Series** (`DA3Mono-Large`). A dedicated model for high-quality relative monocular depth estimation. Unlike disparity-based models (e.g., [Depth Anything 2](https://github.com/DepthAnything/Depth-Anything-V2)), it directly predicts depth, resulting in superior geometric accuracy. + +🔗 Leveraging these available models, we developed a **nested series** (`DA3Nested-Giant-Large`). This series combines a any-view giant model with a metric model to reconstruct visual geometry at a real-world metric scale. + +### 🛠️ Codebase Features +Our repository is designed to be a powerful and user-friendly toolkit for both practical application and future research. +- 🎨 **Interactive Web UI & Gallery**: Visualize model outputs and compare results with an easy-to-use Gradio-based web interface. +- ⚡ **Flexible Command-Line Interface (CLI)**: Powerful and scriptable CLI for batch processing and integration into custom workflows. +- 💾 **Multiple Export Formats**: Save your results in various formats, including `glb`, `npz`, depth images, `ply`, 3DGS videos, etc, to seamlessly connect with other tools. +- 🔧 **Extensible and Modular Design**: The codebase is structured to facilitate future research and the integration of new models or functionalities. + + + + + +## 🚀 Quick Start + +### 📦 Installation + +```bash +# From PyPI (recommended) +pip install awesome-depth-anything-3 + +# With Gradio web UI +pip install awesome-depth-anything-3[app] + +# With CUDA optimizations (xformers + gsplat) +pip install awesome-depth-anything-3[cuda] + +# Everything +pip install awesome-depth-anything-3[all] +``` + +
+Development installation + +```bash +git clone https://github.com/Aedelon/awesome-depth-anything-3.git +cd awesome-depth-anything-3 +pip install -e ".[dev]" + +# Optional: 3D Gaussian Splatting head +pip install --no-build-isolation git+https://github.com/nerfstudio-project/gsplat.git@0b4dddf +``` +
+ +For detailed model information, please refer to the [Model Cards](#-model-cards) section below. + +### 💻 Basic Usage + +```python +import glob, os, torch +from depth_anything_3.api import DepthAnything3 +device = torch.device("cuda") +model = DepthAnything3.from_pretrained("depth-anything/DA3NESTED-GIANT-LARGE") +model = model.to(device=device) +example_path = "assets/examples/SOH" +images = sorted(glob.glob(os.path.join(example_path, "*.png"))) +prediction = model.inference( + images, +) +# prediction.processed_images : [N, H, W, 3] uint8 array +print(prediction.processed_images.shape) +# prediction.depth : [N, H, W] float32 array +print(prediction.depth.shape) +# prediction.conf : [N, H, W] float32 array +print(prediction.conf.shape) +# prediction.extrinsics : [N, 3, 4] float32 array # opencv w2c or colmap format +print(prediction.extrinsics.shape) +# prediction.intrinsics : [N, 3, 3] float32 array +print(prediction.intrinsics.shape) +``` + +```bash + +export MODEL_DIR=depth-anything/DA3NESTED-GIANT-LARGE +# This can be a Hugging Face repository or a local directory +# If you encounter network issues, consider using the following mirror: export HF_ENDPOINT=https://hf-mirror.com +# Alternatively, you can download the model directly from Hugging Face +export GALLERY_DIR=workspace/gallery +mkdir -p $GALLERY_DIR + +# CLI auto mode with backend reuse +da3 backend --model-dir ${MODEL_DIR} --gallery-dir ${GALLERY_DIR} # Cache model to gpu +da3 auto assets/examples/SOH \ + --export-format glb \ + --export-dir ${GALLERY_DIR}/TEST_BACKEND/SOH \ + --use-backend + +# CLI video processing with feature visualization +da3 video assets/examples/robot_unitree.mp4 \ + --fps 15 \ + --use-backend \ + --export-dir ${GALLERY_DIR}/TEST_BACKEND/robo \ + --export-format glb-feat_vis \ + --feat-vis-fps 15 \ + --process-res-method lower_bound_resize \ + --export-feat "11,21,31" + +# CLI auto mode without backend reuse +da3 auto assets/examples/SOH \ + --export-format glb \ + --export-dir ${GALLERY_DIR}/TEST_CLI/SOH \ + --model-dir ${MODEL_DIR} + +``` + +The model architecture is defined in [`DepthAnything3Net`](src/depth_anything_3/model/da3.py), and specified with a Yaml config file located at [`src/depth_anything_3/configs`](src/depth_anything_3/configs). The input and output processing are handled by [`DepthAnything3`](src/depth_anything_3/api.py). To customize the model architecture, simply create a new config file (*e.g.*, `path/to/new/config`) as: + +```yaml +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitb + out_layers: [5, 7, 9, 11] + alt_start: 4 + qknorm_start: 4 + rope_start: 4 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 1536 + output_dim: 2 + features: &head_features 128 + out_channels: &head_out_channels [96, 192, 384, 768] +``` + +Then, the model can be created with the following code snippet. +```python +from depth_anything_3.cfg import create_object, load_config + +Model = create_object(load_config("path/to/new/config")) +``` + + + +## 📚 Useful Documentation + +- 🖥️ [Command Line Interface](docs/CLI.md) +- 📑 [Python API](docs/API.md) + + +## 🗂️ Model Cards + +Generally, you should observe that DA3-LARGE achieves comparable results to VGGT. + +The Nested series uses an Any-view model to estimate pose and depth, and a monocular metric depth estimator for scaling. + +| 🗃️ Model Name | 📏 Params | 📊 Rel. Depth | 📷 Pose Est. | 🧭 Pose Cond. | 🎨 GS | 📐 Met. Depth | ☁️ Sky Seg | 📄 License | +|-------------------------------|-----------|---------------|--------------|---------------|-------|---------------|-----------|----------------| +| **Nested** | | | | | | | | | +| [DA3NESTED-GIANT-LARGE](https://huggingface.co/depth-anything/DA3NESTED-GIANT-LARGE) | 1.40B | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | CC BY-NC 4.0 | +| **Any-view Model** | | | | | | | | | +| [DA3-GIANT](https://huggingface.co/depth-anything/DA3-GIANT) | 1.15B | ✅ | ✅ | ✅ | ✅ | | | CC BY-NC 4.0 | +| [DA3-LARGE](https://huggingface.co/depth-anything/DA3-LARGE) | 0.35B | ✅ | ✅ | ✅ | | | | CC BY-NC 4.0 | +| [DA3-BASE](https://huggingface.co/depth-anything/DA3-BASE) | 0.12B | ✅ | ✅ | ✅ | | | | Apache 2.0 | +| [DA3-SMALL](https://huggingface.co/depth-anything/DA3-SMALL) | 0.08B | ✅ | ✅ | ✅ | | | | Apache 2.0 | +| | | | | | | | | | +| **Monocular Metric Depth** | | | | | | | | | +| [DA3METRIC-LARGE](https://huggingface.co/depth-anything/DA3METRIC-LARGE) | 0.35B | ✅ | | | | ✅ | ✅ | Apache 2.0 | +| | | | | | | | | | +| **Monocular Depth** | | | | | | | | | +| [DA3MONO-LARGE](https://huggingface.co/depth-anything/DA3MONO-LARGE) | 0.35B | ✅ | | | | | ✅ | Apache 2.0 | + + +## ⚡ Performance Benchmarks + +Inference throughput measured on Apple Silicon (MPS) with PyTorch 2.9.0. For detailed benchmarks, see [BENCHMARKS.md](BENCHMARKS.md). + +### Apple Silicon (MPS) - Batch Size 1 + +| Model | Latency | Throughput | +|-------|---------|------------| +| DA3-Small | 46 ms | **22 img/s** | +| DA3-Base | 93 ms | **11 img/s** | +| DA3-Large | 265 ms | **3.8 img/s** | +| DA3-Giant | 618 ms | **1.6 img/s** | + +### Cross-Device Comparison (DA3-Large) + +| Device | Throughput | vs CPU | +|--------|------------|--------| +| CPU | 0.3 img/s | 1.0x | +| Apple Silicon (MPS) | 3.8 img/s | **13x** | +| NVIDIA L4 (CUDA) | 10.3 img/s | **34x** | + +### Batch Processing + +```python +from depth_anything_3.api import DepthAnything3 + +model = DepthAnything3.from_pretrained("depth-anything/DA3-LARGE") + +# Adaptive batching (recommended for large image sets) +results = model.batch_inference( + images=image_paths, + batch_size="auto", # Automatically selects optimal batch size + target_memory_utilization=0.85, +) + +# Fixed batch size +results = model.batch_inference( + images=image_paths, + batch_size=4, +) +``` + +> See [BENCHMARKS.md](BENCHMARKS.md) for comprehensive benchmarks including preprocessing, attention mechanisms, and adaptive batching strategies. + + +## ❓ FAQ + +- **Monocular Metric Depth**: To obtain metric depth in meters from `DA3METRIC-LARGE`, use `metric_depth = focal * net_output / 300.`, where `focal` is the focal length in pixels (typically the average of fx and fy from the camera intrinsic matrix K). Note that the output from `DA3NESTED-GIANT-LARGE` is already in meters. + +- **Ray Head (`use_ray_pose`)**: Our API and CLI support `use_ray_pose` arg, which means that the model will derive camera pose from ray head, which is generally slightly slower, but more accurate. Note that the default is `False` for faster inference speed. +
+ AUC3 Results for DA3NESTED-GIANT-LARGE + + | Model | HiRoom | ETH3D | DTU | 7Scenes | ScanNet++ | + |-------|------|-------|-----|---------|-----------| + | `ray_head` | 84.4 | 52.6 | 93.9 | 29.5 | 89.4 | + | `cam_head` | 80.3 | 48.4 | 94.1 | 28.5 | 85.0 | + +
+ + + + +- **Older GPUs without XFormers support**: See [Issue #11](https://github.com/ByteDance-Seed/Depth-Anything-3/issues/11). Thanks to [@S-Mahoney](https://github.com/S-Mahoney) for the solution! + + +## 🏢 Awesome DA3 Projects + +A community-curated list of Depth Anything 3 integrations across 3D tools, creative pipelines, robotics, and web/VR viewers, including but not limited to these. You are welcome to submit your DA3-based project via PR, and we will review and feature it if applicable. + +- [DA3-blender](https://github.com/xy-gao/DA3-blender): Blender addon for DA3-based 3D reconstruction from a set of images. + +- [ComfyUI-DepthAnythingV3](https://github.com/PozzettiAndrea/ComfyUI-DepthAnythingV3): ComfyUI nodes for Depth Anything 3, supporting single/multi-view and video-consistent depth with optional point‑cloud export. + +- [DA3-ROS2-Wrapper](https://github.com/GerdsenAI/GerdsenAI-Depth-Anything-3-ROS2-Wrapper): Real-time DA3 depth in ROS2 with multi-camera support. + +- [VideoDepthViewer3D](https://github.com/amariichi/VideoDepthViewer3D): Streaming videos with DA3 metric depth to a Three.js/WebXR 3D viewer for VR/stereo playback. + + +## 📝 Credits + +### Original Authors + +This package is built on top of **Depth Anything 3**, created by the ByteDance Seed team: + +- [Haotong Lin](https://haotongl.github.io/), [Sili Chen](https://github.com/SiliChen321), [Jun Hao Liew](https://liewjunhao.github.io/), [Donny Y. Chen](https://donydchen.github.io), [Zhenyu Li](https://zhyever.github.io/), [Guang Shi](https://scholar.google.com/citations?user=MjXxWbUAAAAJ), [Jiashi Feng](https://scholar.google.com.sg/citations?user=Q8iay0gAAAAJ), [Bingyi Kang](https://bingykang.github.io/) + +All model weights, architecture, and core algorithms are their work. This fork only adds production optimizations and deployment tooling. + +### Fork Maintainer + +This optimized fork is maintained by [Delanoe Pirard (Aedelon)](https://github.com/Aedelon). + +Contributions: +- Model caching system +- Adaptive batching +- Apple Silicon (MPS) optimizations +- PyPI packaging and CI/CD +- Comprehensive benchmarking + +### Citation + +If you use Depth Anything 3 in your research, please cite the original paper: + +```bibtex +@article{depthanything3, + title={Depth Anything 3: Recovering the visual space from any views}, + author={Haotong Lin and Sili Chen and Jun Hao Liew and Donny Y. Chen and Zhenyu Li and Guang Shi and Jiashi Feng and Bingyi Kang}, + journal={arXiv preprint arXiv:2511.10647}, + year={2025} +} +``` + +If you specifically use features from this fork (caching, batching, MPS optimizations), you may additionally reference: + +``` +awesome-depth-anything-3: https://github.com/Aedelon/awesome-depth-anything-3 +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..737a2393eb961043b207a12b67874062682a9fea --- /dev/null +++ b/app.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Hugging Face Spaces entry point for awesome-depth-anything-3. + +This file is the main entry point for the HF Spaces deployment. +It launches the Gradio web interface with optimized settings for cloud deployment. +""" + +import os +import tempfile + +# Disable analytics and configure for HF Spaces +os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" +os.environ["DA3_LOG_LEVEL"] = "WARNING" + +from depth_anything_3.app.gradio_app import DepthAnything3App + + +def main(): + """Launch the Gradio app for HF Spaces.""" + # Use DA3-LARGE for good balance of quality and speed + workspace_dir = "/tmp/workspace" + gallery_dir = "/tmp/gallery" + + # Create directories + os.makedirs(workspace_dir, exist_ok=True) + os.makedirs(gallery_dir, exist_ok=True) + + app = DepthAnything3App( + model_dir="depth-anything/DA3-LARGE", + workspace_dir=workspace_dir, + gallery_dir=gallery_dir, + ) + + demo = app.create_app() + + # Build allowed paths for Gradio file access + allowed_paths = [ + os.getcwd(), + tempfile.gettempdir(), + workspace_dir, + gallery_dir, + "/tmp", + ] + + # Launch for HF Spaces (theme/css already set in create_app via gr.Blocks()) + demo.queue(max_size=10).launch( + server_name="0.0.0.0", + server_port=7860, + share=True, + show_error=True, + allowed_paths=allowed_paths, + ) + + +if __name__ == "__main__": + main() diff --git a/assets/examples/SOH/000.png b/assets/examples/SOH/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a93e0d4c9bf6b01e5e3f6906b98e2ddca3b95e --- /dev/null +++ b/assets/examples/SOH/000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea78c3b872b1e8b27de48cadf1d4a692cd42ddf5f72fcab78e2be2937935fb79 +size 1091690 diff --git a/assets/examples/SOH/010.png b/assets/examples/SOH/010.png new file mode 100644 index 0000000000000000000000000000000000000000..c7352f9d9b6410b3050f3d665788e74194f643d6 --- /dev/null +++ b/assets/examples/SOH/010.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c91a69d4e050e75e3760fbacda18a452b9abcf3065e6d6bd940b4a99d48f7982 +size 1134666 diff --git a/assets/examples/robot_unitree.mp4 b/assets/examples/robot_unitree.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..6dbbfd885b4ef3ff5580578ec38fd6deab6a27c7 --- /dev/null +++ b/assets/examples/robot_unitree.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99bc274f7613a665c6135085fe01691ebfaa9033101319071f37c550ab21d1ea +size 1964268 diff --git a/assets/images/da3_radar.png b/assets/images/da3_radar.png new file mode 100644 index 0000000000000000000000000000000000000000..3231985835e11a51e8b59c48ee84740fe4b7e06a --- /dev/null +++ b/assets/images/da3_radar.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d85aa2a89c1959e31ea627474360720b94a393126e8102399ce4dbc355d81d47 +size 214877 diff --git a/assets/images/demo320-2.gif b/assets/images/demo320-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..e9cff45d4fc754fb44da25f5f7572861ffb64038 --- /dev/null +++ b/assets/images/demo320-2.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc02d85064d875f7679c70b7156e7b5c7eef872895dbe45a241400c6f99e22f9 +size 18815873 diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b6b9bf54dd4699eb2299fd91b02c8a15f03307fb --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +"""Benchmark scripts for Depth Anything 3.""" diff --git a/benchmarks/comparative_benchmark.py b/benchmarks/comparative_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..d038fcafa2e2894d40468a064fe5ef31c76051e9 --- /dev/null +++ b/benchmarks/comparative_benchmark.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Comparative Benchmark: awesome-depth-anything-3 vs upstream (vanilla) + +Compares performance between the optimized fork and the original upstream. + +Usage: + python benchmarks/comparative_benchmark.py --device mps + python benchmarks/comparative_benchmark.py --device cuda + python benchmarks/comparative_benchmark.py --device all + python benchmarks/comparative_benchmark.py --quick +""" + +import argparse +import contextlib +import gc +import io +import logging +import os +import shutil +import sys +import time +import warnings + +# Suppress ALL logging before any imports +logging.disable(logging.CRITICAL) +os.environ["DA3_LOG_LEVEL"] = "CRITICAL" +os.environ["PYTHONWARNINGS"] = "ignore" +warnings.filterwarnings("ignore") + +import numpy as np +import torch +from PIL import Image + +# Suppress all loggers +logging.getLogger("depth_anything_3").disabled = True +logging.getLogger("dinov2").disabled = True +logging.getLogger().setLevel(logging.CRITICAL) + + +@contextlib.contextmanager +def suppress_output(): + """Context manager to suppress stdout and stderr.""" + with contextlib.redirect_stdout(io.StringIO()), \ + contextlib.redirect_stderr(io.StringIO()): + # Also suppress all loggers again + logging.disable(logging.CRITICAL) + yield + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +AWESOME_REPO = "/Users/aedelon/Workspace/awesome-depth-anything-3" +UPSTREAM_REPO = "/Users/aedelon/Workspace/depth-anything-3-upstream" +MODEL_NAME = "da3-large" + + +# ============================================================================ +# UTILITIES +# ============================================================================ + +def cleanup(): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + if torch.backends.mps.is_available(): + torch.mps.empty_cache() + + +def sync_device(device): + if device.type == "cuda": + torch.cuda.synchronize() + elif device.type == "mps": + torch.mps.synchronize() + + +def clear_modules(): + """Clear depth_anything_3 from sys.modules.""" + to_remove = [k for k in sys.modules.keys() if "depth_anything_3" in k] + for k in to_remove: + del sys.modules[k] + + +def suppress_logging(): + """Suppress all logging after module import.""" + logging.disable(logging.CRITICAL) + try: + from depth_anything_3.utils.logger import logger + logger.level = 100 + except: + pass + + +def get_available_devices(): + """Get available devices.""" + devices = [torch.device("cpu")] + if torch.backends.mps.is_available(): + devices.append(torch.device("mps")) + if torch.cuda.is_available(): + devices.append(torch.device("cuda")) + return devices + + +def get_device_name(device): + if device.type == "cuda": + return torch.cuda.get_device_name(device) + elif device.type == "mps": + return "Apple Silicon (MPS)" + return "CPU" + + +# ============================================================================ +# BENCHMARK: UPSTREAM (VANILLA) +# ============================================================================ + +def benchmark_upstream(device, pil_images, process_res=504, runs=3): + """Benchmark upstream/vanilla depth-anything-3.""" + + # Setup path + clear_modules() + upstream_src = os.path.join(UPSTREAM_REPO, "src") + if upstream_src in sys.path: + sys.path.remove(upstream_src) + sys.path.insert(0, upstream_src) + + with suppress_output(): + from depth_anything_3.api import DepthAnything3 + suppress_logging() + + cleanup() + + # Cold load + start = time.perf_counter() + model = DepthAnything3(model_name=MODEL_NAME) + model = model.to(device) + model.eval() + cold_load_time = time.perf_counter() - start + + # Warmup + for _ in range(2): + model.inference(pil_images[:1], process_res=process_res) + sync_device(device) + cleanup() + + # Benchmark inference + times = [] + for _ in range(runs): + cleanup() + sync_device(device) + start = time.perf_counter() + model.inference(pil_images, process_res=process_res) + sync_device(device) + times.append(time.perf_counter() - start) + + avg_time = np.mean(times) + std_time = np.std(times) + throughput = len(pil_images) / avg_time + + del model + cleanup() + + # Cleanup path + sys.path.remove(upstream_src) + clear_modules() + + return { + "cold_load": cold_load_time, + "inference_time": avg_time, + "inference_std": std_time, + "throughput": throughput, + } + + +# ============================================================================ +# BENCHMARK: AWESOME (OPTIMIZED) +# ============================================================================ + +def benchmark_awesome(device, pil_images, process_res=504, runs=3, use_cache=True): + """Benchmark awesome (optimized) depth-anything-3.""" + + # Setup path + clear_modules() + awesome_src = os.path.join(AWESOME_REPO, "src") + if awesome_src in sys.path: + sys.path.remove(awesome_src) + sys.path.insert(0, awesome_src) + + with suppress_output(): + from depth_anything_3.api import DepthAnything3 + from depth_anything_3.cache import get_model_cache + suppress_logging() + + # Clear cache if testing cold load + if not use_cache: + cache = get_model_cache() + cache.clear() + + cleanup() + + # Cold/warm load + start = time.perf_counter() + model = DepthAnything3(model_name=MODEL_NAME, device=device, use_cache=use_cache) + load_time = time.perf_counter() - start + + # For cache test, do a second load + cached_load_time = None + if use_cache: + del model + cleanup() + start = time.perf_counter() + model = DepthAnything3(model_name=MODEL_NAME, device=device, use_cache=True) + cached_load_time = time.perf_counter() - start + + # Warmup + for _ in range(2): + model.inference(pil_images[:1], process_res=process_res) + sync_device(device) + cleanup() + + # Benchmark inference + times = [] + for _ in range(runs): + cleanup() + sync_device(device) + start = time.perf_counter() + model.inference(pil_images, process_res=process_res) + sync_device(device) + times.append(time.perf_counter() - start) + + avg_time = np.mean(times) + std_time = np.std(times) + throughput = len(pil_images) / avg_time + + del model + cleanup() + + # Cleanup path + sys.path.remove(awesome_src) + clear_modules() + + return { + "cold_load": load_time, + "cached_load": cached_load_time, + "inference_time": avg_time, + "inference_std": std_time, + "throughput": throughput, + } + + +# ============================================================================ +# MAIN +# ============================================================================ + +def run_comparison(device, batch_sizes, process_res=504, runs=3): + """Run comparison for a specific device.""" + + results = {} + temp_dir = "temp_compare" + os.makedirs(temp_dir, exist_ok=True) + + try: + # Create test images + max_batch = max(batch_sizes) + pil_images = [] + for i in range(max_batch): + img = Image.new("RGB", (1280, 720), color=(100 + i*10, 150, 200)) + pil_images.append(img) + + for batch_size in batch_sizes: + test_images = pil_images[:batch_size] + results[batch_size] = {} + + print(f"\n Batch size: {batch_size}") + print(f" {'-'*50}") + + # Upstream + print(f" Testing UPSTREAM (vanilla)...", end=" ", flush=True) + try: + upstream = benchmark_upstream(device, test_images, process_res, runs) + results[batch_size]["upstream"] = upstream + print(f"{upstream['throughput']:.2f} img/s") + except Exception as e: + print(f"ERROR: {e}") + results[batch_size]["upstream"] = None + + # Awesome (no cache - fair comparison) + print(f" Testing AWESOME (no cache)...", end=" ", flush=True) + try: + awesome_nc = benchmark_awesome(device, test_images, process_res, runs, use_cache=False) + results[batch_size]["awesome_nocache"] = awesome_nc + print(f"{awesome_nc['throughput']:.2f} img/s") + except Exception as e: + print(f"ERROR: {e}") + results[batch_size]["awesome_nocache"] = None + + # Awesome (with cache) + print(f" Testing AWESOME (cached)...", end=" ", flush=True) + try: + awesome_c = benchmark_awesome(device, test_images, process_res, runs, use_cache=True) + results[batch_size]["awesome_cached"] = awesome_c + print(f"{awesome_c['throughput']:.2f} img/s") + except Exception as e: + print(f"ERROR: {e}") + results[batch_size]["awesome_cached"] = None + + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + return results + + +def print_results_table(results, device): + """Print formatted results table.""" + + print(f"\n{'='*70}") + print(f" RESULTS: {device.type.upper()}") + print(f"{'='*70}") + + # Header + print(f"\n{'Batch':<8} {'Metric':<18} {'Upstream':<12} {'Awesome':<12} {'Speedup':<10}") + print("-" * 60) + + for batch_size, data in sorted(results.items()): + upstream = data.get("upstream") + awesome = data.get("awesome_nocache") or data.get("awesome_cached") + + if not upstream or not awesome: + continue + + # Inference throughput + u_thr = upstream["throughput"] + a_thr = awesome["throughput"] + speedup = a_thr / u_thr if u_thr > 0 else 0 + print(f"{batch_size:<8} {'Throughput (img/s)':<18} {u_thr:<12.2f} {a_thr:<12.2f} {speedup:<10.2f}x") + + # Inference time + u_time = upstream["inference_time"] * 1000 + a_time = awesome["inference_time"] * 1000 + speedup = u_time / a_time if a_time > 0 else 0 + print(f"{'':<8} {'Latency (ms)':<18} {u_time:<12.1f} {a_time:<12.1f} {speedup:<10.2f}x") + + # Cold load time + u_load = upstream["cold_load"] + a_load = awesome["cold_load"] + speedup = u_load / a_load if a_load > 0 else 0 + print(f"{'':<8} {'Cold load (s)':<18} {u_load:<12.2f} {a_load:<12.2f} {speedup:<10.2f}x") + + # Cached load (awesome only) + cached = data.get("awesome_cached") + if cached and cached.get("cached_load"): + c_load = cached["cached_load"] + speedup = u_load / c_load if c_load > 0 else 0 + print(f"{'':<8} {'Cached load (s)':<18} {'-':<12} {c_load:<12.3f} {speedup:<10.1f}x") + + print() + + +def main(): + parser = argparse.ArgumentParser(description="Comparative Benchmark: Awesome vs Upstream") + parser.add_argument("--device", "-d", type=str, default="auto", + choices=["auto", "cpu", "mps", "cuda", "all"], + help="Device to benchmark") + parser.add_argument("--batch-sizes", type=int, nargs="+", default=[1, 2, 4], + help="Batch sizes to test") + parser.add_argument("--runs", type=int, default=3, help="Number of runs per test") + parser.add_argument("--quick", action="store_true", help="Quick mode (fewer runs)") + args = parser.parse_args() + + if args.quick: + args.batch_sizes = [1, 2] + args.runs = 2 + + # Determine devices + available = get_available_devices() + if args.device == "auto": + devices = [available[-1]] + elif args.device == "all": + devices = available + else: + requested = torch.device(args.device) + if requested in available: + devices = [requested] + else: + print(f"Device '{args.device}' not available. Available: {[d.type for d in available]}") + return + + # Header + print("\n" + "=" * 70) + print(" COMPARATIVE BENCHMARK: AWESOME vs UPSTREAM (VANILLA)") + print("=" * 70) + print(f" Model: {MODEL_NAME}") + print(f" PyTorch: {torch.__version__}") + print(f" Batch sizes: {args.batch_sizes}") + print(f" Runs per test: {args.runs}") + print(f" Devices: {[d.type.upper() for d in devices]}") + for d in available: + status = "✓" if d in devices else "○" + print(f" {status} {d.type.upper()}: {get_device_name(d)}") + print("=" * 70) + + all_results = {} + + for device in devices: + print(f"\n{'#'*70}") + print(f" DEVICE: {device.type.upper()} ({get_device_name(device)})") + print(f"{'#'*70}") + + results = run_comparison(device, args.batch_sizes, runs=args.runs) + all_results[device.type] = results + print_results_table(results, device) + + # Final summary + print("\n" + "=" * 70) + print(" SUMMARY") + print("=" * 70) + + for device_type, results in all_results.items(): + print(f"\n {device_type.upper()}:") + + for batch_size, data in sorted(results.items()): + upstream = data.get("upstream") + awesome = data.get("awesome_nocache") + + if upstream and awesome: + speedup = awesome["throughput"] / upstream["throughput"] + print(f" Batch {batch_size}: {speedup:.2f}x faster inference") + + print("\n" + "=" * 70 + "\n") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/flash_attention_benchmark.py b/benchmarks/flash_attention_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..f59d5c20a1e995f0a088f1c1c67de93e5104832f --- /dev/null +++ b/benchmarks/flash_attention_benchmark.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +# Copyright (c) Delanoe Pirard / Aedelon - Apache 2.0 +""" +Flash Attention Benchmark for Depth Anything 3. + +Provides clear performance comparison with tables and analysis. + +Usage: + python benchmarks/flash_attention_benchmark.py + python benchmarks/flash_attention_benchmark.py --detailed +""" + +import argparse +import gc +import os +import sys +import time +from dataclasses import dataclass + +import torch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from depth_anything_3.model.dinov2.layers import ( + FLASH_ATTN_AVAILABLE, + FLASH_ATTN_VERSION, + Attention, +) + + +@dataclass +class BenchmarkConfig: + """Configuration for a benchmark test case.""" + + name: str + seq_len: int + batch_size: int + embed_dim: int + num_heads: int + image_size: str # Description of corresponding image size + + @property + def description(self): + return f"{self.name} ({self.image_size})" + + +# Depth Anything 3 model configurations +DA3_CONFIGS = { + "vitb": {"embed_dim": 768, "num_heads": 12, "depth": 12}, + "vitl": {"embed_dim": 1024, "num_heads": 16, "depth": 24}, + "vitg": {"embed_dim": 1536, "num_heads": 24, "depth": 40}, +} + + +def get_device_info(): + """Get device information.""" + if torch.cuda.is_available(): + device = torch.device("cuda") + device_name = torch.cuda.get_device_name() + memory_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3 + compute_cap = torch.cuda.get_device_capability() + return { + "type": "cuda", + "device": device, + "name": device_name, + "memory_gb": memory_gb, + "compute_capability": f"{compute_cap[0]}.{compute_cap[1]}", + } + elif torch.backends.mps.is_available(): + return { + "type": "mps", + "device": torch.device("mps"), + "name": "Apple Silicon", + "memory_gb": None, + "compute_capability": None, + } + else: + return { + "type": "cpu", + "device": torch.device("cpu"), + "name": "CPU", + "memory_gb": None, + "compute_capability": None, + } + + +def benchmark_attention(attn_module, x, warmup=5, runs=20): + """Run benchmark for a single attention module.""" + device = x.device + + # Warmup + with torch.no_grad(): + for _ in range(warmup): + _ = attn_module(x) + if device.type == "cuda": + torch.cuda.synchronize() + + # Reset memory tracking + if device.type == "cuda": + torch.cuda.reset_peak_memory_stats() + + # Benchmark + times = [] + with torch.no_grad(): + for _ in range(runs): + if device.type == "cuda": + torch.cuda.synchronize() + start = time.perf_counter() + _ = attn_module(x) + if device.type == "cuda": + torch.cuda.synchronize() + times.append((time.perf_counter() - start) * 1000) + + # Memory + peak_mem_mb = 0 + if device.type == "cuda": + peak_mem_mb = torch.cuda.max_memory_allocated() / 1024 / 1024 + + times_tensor = torch.tensor(times) + return { + "mean_ms": times_tensor.mean().item(), + "std_ms": times_tensor.std().item(), + "min_ms": times_tensor.min().item(), + "peak_mem_mb": peak_mem_mb, + } + + +def print_header(): + """Print benchmark header.""" + print("\n" + "=" * 80) + print(" " * 20 + "FLASH ATTENTION BENCHMARK - DEPTH ANYTHING 3") + print("=" * 80 + "\n") + + +def get_sdpa_backend_info(): + """Get info about which SDPA backend is being used.""" + info = {} + if torch.cuda.is_available(): + from torch.backends.cuda import ( + flash_sdp_enabled, + mem_efficient_sdp_enabled, + math_sdp_enabled, + ) + info["flash_sdp"] = flash_sdp_enabled() + info["mem_efficient_sdp"] = mem_efficient_sdp_enabled() + info["math_sdp"] = math_sdp_enabled() + return info + + +def print_device_info(device_info): + """Print device information.""" + print("📊 HARDWARE CONFIGURATION") + print("─" * 80) + print(f" Device Type : {device_info['type'].upper()}") + print(f" Device Name : {device_info['name']}") + if device_info["memory_gb"]: + print(f" Memory : {device_info['memory_gb']:.1f} GB") + if device_info["compute_capability"]: + print(f" Compute Cap. : {device_info['compute_capability']}") + cc = float(device_info["compute_capability"]) + if cc >= 7.5: + print(f" ✅ Flash Attention supported (≥7.5)") + else: + print(f" ❌ Flash Attention requires ≥7.5") + + # SDPA backend info + sdpa_info = get_sdpa_backend_info() + if sdpa_info: + print(f"\n PyTorch SDPA Backends:") + print(f" Flash SDP : {'✅ Enabled' if sdpa_info.get('flash_sdp') else '❌ Disabled'}") + print(f" MemEfficient : {'✅ Enabled' if sdpa_info.get('mem_efficient_sdp') else '❌ Disabled'}") + print(f" Math SDP : {'✅ Enabled' if sdpa_info.get('math_sdp') else '❌ Disabled'}") + + if sdpa_info.get('flash_sdp'): + print(f"\n ⚡ PyTorch SDPA uses Flash Attention internally!") + print(f" (No need for flash-attn package with PyTorch >= 2.2)") + + print(f"\n flash-attn pkg : {'✅ Installed v' + FLASH_ATTN_VERSION if FLASH_ATTN_AVAILABLE else '❌ Not installed (optional)'}") + print() + + +def print_table_header(): + """Print benchmark table header.""" + print( + "┌──────────────────────────┬──────────────┬──────────────┬──────────────┬────────────┐" + ) + print( + "│ Configuration │ flash_attn │ sdpa │ manual │ Speedup │" + ) + print( + "├──────────────────────────┼──────────────┼──────────────┼──────────────┼────────────┤" + ) + + +def print_table_row(config_desc, results, baseline="sdpa"): + """Print a benchmark result row.""" + backends = ["flash_attn", "sdpa", "manual"] + + # Format times + time_strs = [] + for backend in backends: + if backend in results and results[backend]: + time_ms = results[backend]["mean_ms"] + time_strs.append(f"{time_ms:6.2f} ms") + else: + time_strs.append(" N/A") + + # Calculate speedup + speedup_str = " -" + if "flash_attn" in results and results["flash_attn"] and baseline in results: + if results[baseline]: + speedup = results[baseline]["mean_ms"] / results["flash_attn"]["mean_ms"] + speedup_str = f" {speedup:.2f}x ⚡" if speedup > 1.1 else f" {speedup:.2f}x" + + print( + f"│ {config_desc:24s} │ {time_strs[0]:12s} │ {time_strs[1]:12s} │ {time_strs[2]:12s} │ {speedup_str:10s} │" + ) + + +def print_table_footer(): + """Print benchmark table footer.""" + print( + "└──────────────────────────┴──────────────┴──────────────┴──────────────┴────────────┘" + ) + + +def print_model_analysis(model_name, config, results, num_layers): + """Print detailed analysis for a specific model.""" + if "flash_attn" not in results or not results["flash_attn"]: + return + + flash_time = results["flash_attn"]["mean_ms"] + sdpa_time = results["sdpa"]["mean_ms"] if "sdpa" in results else flash_time + + speedup = sdpa_time / flash_time + time_saved_per_layer = (sdpa_time - flash_time) / num_layers + total_time_saved = time_saved_per_layer * num_layers + + print(f"\n 📈 {model_name} Analysis:") + print(f" • Attention time per layer: {flash_time:.2f} ms (flash) vs {sdpa_time:.2f} ms (sdpa)") + print(f" • Time saved per layer: {time_saved_per_layer:.2f} ms") + print(f" • Total time saved ({num_layers} layers): {total_time_saved:.1f} ms") + print(f" • Speedup: {speedup:.2f}x on attention") + + # Estimate full inference impact + # Attention is ~15-20% of total inference time + attn_fraction = 0.175 + overall_speedup = 1 / (1 - attn_fraction + attn_fraction / speedup) + overall_improvement = (1 - 1 / overall_speedup) * 100 + + print( + f" • Estimated full inference speedup: {overall_speedup:.2f}x (~{overall_improvement:.1f}% faster)" + ) + + +def run_benchmark(test_configs, backends, warmup=5, runs=20, detailed=False): + """Run complete benchmark suite.""" + device_info = get_device_info() + device = device_info["device"] + dtype = torch.float16 if device.type == "cuda" else torch.float32 + + print_header() + print_device_info(device_info) + + # Filter backends based on availability + available_backends = [] + if FLASH_ATTN_AVAILABLE and device.type == "cuda": + available_backends.append("flash_attn") + available_backends.append("sdpa") + if detailed: + available_backends.append("manual") + + all_results = {} + + # Run benchmarks by model + for model_name, model_config in DA3_CONFIGS.items(): + print(f"\n🔬 MODEL: {model_name.upper()} (dim={model_config['embed_dim']}, heads={model_config['num_heads']}, depth={model_config['depth']})") + print("─" * 80) + print_table_header() + + model_results = {} + + for test_config in test_configs: + # Adjust config for this model + config = BenchmarkConfig( + name=test_config.name, + seq_len=test_config.seq_len, + batch_size=test_config.batch_size, + embed_dim=model_config["embed_dim"], + num_heads=model_config["num_heads"], + image_size=test_config.image_size, + ) + + x = torch.randn( + config.batch_size, config.seq_len, config.embed_dim, device=device, dtype=dtype + ) + + results = {} + for backend in available_backends: + gc.collect() + if device.type == "cuda": + torch.cuda.empty_cache() + + try: + attn = Attention( + dim=config.embed_dim, + num_heads=config.num_heads, + attn_backend=backend, + ).to(device, dtype) + attn.eval() + + result = benchmark_attention(attn, x, warmup=warmup, runs=runs) + results[backend] = result + + del attn + except Exception as e: + results[backend] = None + if detailed: + print(f" {backend} failed: {e}") + + model_results[config.name] = results + print_table_row(config.description, results) + + print_table_footer() + + # Analysis for this model + if detailed and model_results: + # Use medium config for analysis + medium_key = next( + (k for k in model_results.keys() if "1024" in k.lower() or "medium" in k.lower()), + list(model_results.keys())[0], + ) + print_model_analysis( + model_name.upper(), + test_configs[0], + model_results[medium_key], + model_config["depth"], + ) + + all_results[model_name] = model_results + + # Final summary + print("\n" + "=" * 80) + print("📋 SUMMARY & RECOMMENDATIONS") + print("=" * 80) + + sdpa_info = get_sdpa_backend_info() + + if device.type == "cuda": + # Check if PyTorch SDPA has Flash enabled + if sdpa_info.get('flash_sdp'): + print("\n✅ Flash Attention is ACTIVE via PyTorch SDPA!") + print("\n Your setup:") + print(f" • PyTorch {torch.__version__} with native Flash Attention") + print(" • SDPA backend: Flash SDP ⚡") + print(" • No additional packages needed!") + print("\n Benefits you're already getting:") + print(" • 2-4x faster attention vs manual implementation") + print(" • Memory-efficient attention computation") + print(" • Automatic kernel selection per input size") + + if FLASH_ATTN_AVAILABLE: + print(f"\n ℹ️ flash-attn v{FLASH_ATTN_VERSION} also installed") + print(" (May provide slight additional optimization in some cases)") + else: + print("\n ℹ️ flash-attn package: Not needed!") + print(" PyTorch >= 2.2 includes Flash Attention natively.") + + elif FLASH_ATTN_AVAILABLE: + print("\n✅ Flash Attention is ACTIVE via flash-attn package") + print(f"\n Using flash-attn v{FLASH_ATTN_VERSION}") + print("\n Benefits:") + print(" • 2-3x faster attention computation") + print(" • ~15-25% overall inference speedup") + print(" • Lower memory usage") + + else: + print("\n⚠️ Flash Attention not available") + print("\n Options to enable:") + print(" 1. Upgrade PyTorch to >= 2.2 (recommended)") + print(" 2. Install flash-attn: pip install flash-attn --no-build-isolation") + + elif device.type == "mps": + print("\n📱 Apple Silicon (MPS) detected") + print("\n • Flash Attention not available for MPS") + print(" • PyTorch SDPA uses optimized Metal kernels") + print(" • Already running at optimal speed for your hardware") + + else: + print("\n💻 CPU detected") + print("\n • Consider using GPU for faster inference") + print(" • Flash Attention is CUDA-only") + + # Print SDPA vs Manual speedup summary + print("\n" + "─" * 80) + print("⚡ PERFORMANCE COMPARISON") + print("─" * 80) + print("\n SDPA vs Manual attention speedup (per layer):") + + for model_name, model_results in all_results.items(): + if model_results: + # Get XLarge config results for most impact + xlarge_key = next((k for k in model_results.keys() if "xlarge" in k.lower()), list(model_results.keys())[-1]) + if xlarge_key in model_results: + res = model_results[xlarge_key] + if res.get("sdpa") and res.get("manual"): + speedup = res["manual"]["mean_ms"] / res["sdpa"]["mean_ms"] + print(f" • {model_name.upper():6s}: {speedup:.1f}x faster (sdpa: {res['sdpa']['mean_ms']:.2f}ms vs manual: {res['manual']['mean_ms']:.2f}ms)") + + print("\n" + "=" * 80) + print() + + return all_results + + +def main(): + parser = argparse.ArgumentParser(description="Flash Attention benchmark for DA3") + parser.add_argument( + "--detailed", + action="store_true", + help="Show detailed analysis and include manual backend", + ) + parser.add_argument( + "--warmup", + type=int, + default=5, + help="Warmup iterations (default: 5)", + ) + parser.add_argument( + "--runs", + type=int, + default=20, + help="Benchmark runs (default: 20)", + ) + + args = parser.parse_args() + + # Test configurations based on common image sizes + test_configs = [ + BenchmarkConfig( + name="Small", + seq_len=256, + batch_size=1, + embed_dim=768, # Will be overridden per model + num_heads=12, # Will be overridden per model + image_size="392px image", + ), + BenchmarkConfig( + name="Medium", + seq_len=529, + batch_size=1, + embed_dim=768, + num_heads=12, + image_size="518px image", + ), + BenchmarkConfig( + name="Large", + seq_len=1024, + batch_size=1, + embed_dim=768, + num_heads=12, + image_size="742px image", + ), + BenchmarkConfig( + name="XLarge", + seq_len=1369, + batch_size=1, + embed_dim=768, + num_heads=12, + image_size="1024px image", + ), + ] + + backends = ["flash_attn", "sdpa"] + if args.detailed: + backends.append("manual") + + run_benchmark( + test_configs=test_configs, + backends=backends, + warmup=args.warmup, + runs=args.runs, + detailed=args.detailed, + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/benchmarks/full_benchmark.py b/benchmarks/full_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..56811bd2424d144bc109b88e94352a4ade647bc8 --- /dev/null +++ b/benchmarks/full_benchmark.py @@ -0,0 +1,696 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Delanoe Pirard / Aedelon - Apache 2.0 +""" +Full Benchmark Suite for Depth Anything 3 + +Tests ALL optimization combinations for each device (CPU, MPS, CUDA). + +Optimizations tested: +- Preprocessing: CPU (PIL) vs GPU (NVJPEG on CUDA) +- Attention: SDPA (Flash Attention) vs Manual + +Usage: + python benchmarks/full_benchmark.py # Best device only + python benchmarks/full_benchmark.py -d all # All devices + python benchmarks/full_benchmark.py -d cuda # CUDA only + python benchmarks/full_benchmark.py --quick # Quick mode +""" + +import argparse +import gc +import logging +import os +import shutil +import sys +import time +import warnings +from dataclasses import dataclass +from typing import Dict, List, Optional + +# Suppress ALL logging before any imports +logging.disable(logging.CRITICAL) +os.environ["DA3_LOG_LEVEL"] = "ERROR" +warnings.filterwarnings("ignore") + +import numpy as np +import torch +from PIL import Image + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +# Suppress depth_anything_3 logger specifically +logging.getLogger("depth_anything_3").disabled = True +logging.getLogger("dinov2").disabled = True + + +# ============================================================================ +# STYLES +# ============================================================================ + +class Style: + CYAN = "\033[96m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + BOLD = "\033[1m" + DIM = "\033[2m" + RESET = "\033[0m" + + +def colored(text, color, bold=False): + prefix = Style.BOLD if bold else "" + return f"{prefix}{color}{text}{Style.RESET}" + + +# ============================================================================ +# UTILITIES +# ============================================================================ + +def cleanup(): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + if torch.backends.mps.is_available(): + torch.mps.empty_cache() + + +def sync_device(device): + if device.type == "cuda": + torch.cuda.synchronize() + elif device.type == "mps": + torch.mps.synchronize() + + +def get_available_devices() -> List[torch.device]: + """Get all available devices for benchmarking.""" + devices = [torch.device("cpu")] + if torch.backends.mps.is_available(): + devices.append(torch.device("mps")) + if torch.cuda.is_available(): + devices.append(torch.device("cuda")) + return devices + + +def get_device_name(device: torch.device) -> str: + """Get human-readable device name.""" + if device.type == "cuda": + return torch.cuda.get_device_name(device) + elif device.type == "mps": + return "Apple Silicon (MPS)" + else: + import platform + return f"CPU ({platform.processor() or 'Unknown'})" + + +# ============================================================================ +# DATA CLASSES +# ============================================================================ + +@dataclass +class BenchmarkResult: + """Single benchmark result.""" + mean_ms: float + std_ms: float + fps: float + + @classmethod + def from_times(cls, times: List[float], batch_size: int = 1): + mean_ms = np.mean(times) + std_ms = np.std(times) + fps = 1000 / mean_ms * batch_size + return cls(mean_ms=mean_ms, std_ms=std_ms, fps=fps) + + +@dataclass +class OptimizationConfig: + """Configuration for a specific optimization combination.""" + name: str + preprocessing: str # "cpu" or "gpu" + attention: str # "sdpa" or "manual" + description: str + + @property + def short_name(self) -> str: + prep = "GPU" if self.preprocessing == "gpu" else "CPU" + attn = "SDPA" if self.attention == "sdpa" else "Manual" + return f"{prep}+{attn}" + + +# ============================================================================ +# BENCHMARK FUNCTIONS +# ============================================================================ + +def get_optimization_configs(device: torch.device) -> List[OptimizationConfig]: + """Get all valid optimization configurations for a device.""" + configs = [] + + if device.type == "cuda": + # CUDA: All 4 combinations + configs = [ + OptimizationConfig("gpu_sdpa", "gpu", "sdpa", "GPU Decode (NVJPEG) + SDPA (Flash)"), + OptimizationConfig("gpu_manual", "gpu", "manual", "GPU Decode (NVJPEG) + Manual Attn"), + OptimizationConfig("cpu_sdpa", "cpu", "sdpa", "CPU Decode (PIL) + SDPA (Flash)"), + OptimizationConfig("cpu_manual", "cpu", "manual", "CPU Decode (PIL) + Manual Attn"), + ] + elif device.type == "mps": + # MPS: CPU preprocessing is better, 2 combinations + configs = [ + OptimizationConfig("cpu_sdpa", "cpu", "sdpa", "CPU Decode (PIL) + SDPA"), + OptimizationConfig("cpu_manual", "cpu", "manual", "CPU Decode (PIL) + Manual Attn"), + ] + else: + # CPU: 2 combinations + configs = [ + OptimizationConfig("cpu_sdpa", "cpu", "sdpa", "SDPA Attention"), + OptimizationConfig("cpu_manual", "cpu", "manual", "Manual Attention"), + ] + + return configs + + +def benchmark_preprocessing_detailed(device: torch.device, runs: int = 5) -> Dict: + """Benchmark preprocessing in detail.""" + from depth_anything_3.utils.io.input_processor import InputProcessor + from depth_anything_3.utils.io.gpu_input_processor import GPUInputProcessor + + results = {} + temp_dir = "temp_bench_preproc" + + sizes = [ + ("720p", 1280, 720), + ("1080p", 1920, 1080), + ("4K", 3840, 2160), + ] + + os.makedirs(temp_dir, exist_ok=True) + + try: + cpu_proc = InputProcessor() + gpu_proc = None + if device.type == "cuda": + gpu_proc = GPUInputProcessor(device=device) + + for name, w, h in sizes: + results[name] = {} + + # Create test files + files = [] + pil_imgs = [] + for i in range(4): + img = Image.new("RGB", (w, h), color=(100 + i*10, 150, 200)) + fpath = f"{temp_dir}/{name}_{i}.jpg" + img.save(fpath, quality=95) + files.append(fpath) + pil_imgs.append(img.copy()) + + # CPU benchmark + cleanup() + for _ in range(2): + cpu_proc(image=pil_imgs, process_res=518, num_workers=8) + + times = [] + for _ in range(runs): + start = time.perf_counter() + cpu_proc(image=pil_imgs, process_res=518, num_workers=8) + times.append((time.perf_counter() - start) * 1000) + results[name]["cpu"] = BenchmarkResult.from_times(times, batch_size=4) + + # GPU benchmark (NVJPEG for CUDA) + if gpu_proc and gpu_proc.use_gpu: + cleanup() + for _ in range(2): + gpu_proc(image=files, process_res=518, num_workers=1) + sync_device(device) + + times = [] + for _ in range(runs): + sync_device(device) + start = time.perf_counter() + gpu_proc(image=files, process_res=518, num_workers=1) + sync_device(device) + times.append((time.perf_counter() - start) * 1000) + results[name]["gpu"] = BenchmarkResult.from_times(times, batch_size=4) + + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + return results + + +def benchmark_attention_detailed(device: torch.device, runs: int = 10) -> Dict: + """Benchmark attention backends in detail.""" + from depth_anything_3.model.dinov2.layers import Attention + + results = {} + dtype = torch.float16 if device.type == "cuda" else torch.float32 + + configs = [ + ("ViT-S (518px)", 384, 6, 529), + ("ViT-L (518px)", 1024, 16, 529), + ("ViT-L (770px)", 1024, 16, 1156), + ] + + for name, dim, heads, seq_len in configs: + results[name] = {} + x = torch.randn(1, seq_len, dim, device=device, dtype=dtype) + + for backend in ["sdpa", "manual"]: + cleanup() + attn = Attention(dim=dim, num_heads=heads, attn_backend=backend).to(device, dtype) + attn.eval() + + # Warmup + with torch.no_grad(): + for _ in range(3): + attn(x) + sync_device(device) + + # Benchmark + times = [] + with torch.no_grad(): + for _ in range(runs): + sync_device(device) + start = time.perf_counter() + attn(x) + sync_device(device) + times.append((time.perf_counter() - start) * 1000) + + results[name][backend] = BenchmarkResult.from_times(times) + del attn + + return results + + +def benchmark_inference_matrix( + device: torch.device, + models: List[str], + runs: int = 3, +) -> Dict: + """Benchmark all optimization combinations for inference.""" + from depth_anything_3.api import DepthAnything3 + + results = {} + temp_dir = "temp_bench_infer" + configs = get_optimization_configs(device) + + os.makedirs(temp_dir, exist_ok=True) + + # Create test images (720p) + img_paths = [] + pil_imgs = [] + for i in range(4): + img = Image.new("RGB", (1280, 720), color=(100 + i*20, 150, 200)) + path = f"{temp_dir}/test_{i}.jpg" + img.save(path, quality=95) + img_paths.append(path) + pil_imgs.append(img.copy()) + + try: + for model_name in models: + results[model_name] = {} + + for config in configs: + cleanup() + + # Set attention backend + os.environ["DA3_ATTENTION_BACKEND"] = config.attention + + # Load model fresh (to apply attention backend) + model = DepthAnything3( + model_name=model_name, + device=device, + use_cache=False, + ) + + # Choose input based on preprocessing + if config.preprocessing == "gpu" and device.type == "cuda": + test_input = img_paths[:1] # File paths for NVJPEG + else: + test_input = pil_imgs[:1] # PIL for CPU preprocessing + + # Warmup + for _ in range(3): + model.inference(test_input, process_res=518) + sync_device(device) + + # Benchmark + times = [] + for _ in range(runs): + sync_device(device) + start = time.perf_counter() + model.inference(test_input, process_res=518) + sync_device(device) + times.append((time.perf_counter() - start) * 1000) + + results[model_name][config.name] = { + "result": BenchmarkResult.from_times(times, batch_size=1), + "config": config, + } + + del model + cleanup() + + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + return results + + +# ============================================================================ +# DISPLAY FUNCTIONS +# ============================================================================ + +def print_header(title: str): + """Print section header.""" + print() + print(colored("═" * 70, Style.CYAN)) + print(colored("║", Style.CYAN) + colored(f" {title}", Style.BOLD).center(77) + colored("║", Style.CYAN)) + print(colored("═" * 70, Style.CYAN)) + + +def print_subheader(title: str): + """Print subsection header.""" + print() + print(colored(f"▶ {title}", Style.YELLOW, bold=True)) + print(colored("─" * 70, Style.DIM)) + + +def format_speedup(speedup: float) -> str: + """Format speedup with color.""" + if speedup >= 1.5: + return colored(f"{speedup:.2f}x", Style.GREEN, bold=True) + elif speedup >= 1.1: + return colored(f"{speedup:.2f}x", Style.GREEN) + elif speedup >= 0.95: + return f"{speedup:.2f}x" + else: + return colored(f"{speedup:.2f}x", Style.RED) + + +def print_preprocessing_results(results: Dict, device: torch.device): + """Print preprocessing benchmark results.""" + print_subheader("PREPROCESSING (4 images batch)") + + has_gpu = any("gpu" in r for r in results.values()) + + if has_gpu: + print(f" {'Resolution':<12} {'CPU (PIL)':<14} {'GPU (NVJPEG)':<14} {'Speedup':<10}") + print(f" {'-'*50}") + + for name, data in results.items(): + cpu_ms = data["cpu"].mean_ms + if "gpu" in data: + gpu_ms = data["gpu"].mean_ms + speedup = cpu_ms / gpu_ms + print(f" {name:<12} {cpu_ms:>8.1f} ms {gpu_ms:>8.1f} ms {format_speedup(speedup)}") + else: + print(f" {name:<12} {cpu_ms:>8.1f} ms {'N/A':<14}") + else: + print(f" {'Resolution':<12} {'CPU (PIL)':<14}") + print(f" {'-'*30}") + for name, data in results.items(): + cpu_ms = data["cpu"].mean_ms + print(f" {name:<12} {cpu_ms:>8.1f} ms") + + # Summary + if has_gpu: + speedups = [] + for data in results.values(): + if "gpu" in data: + speedups.append(data["cpu"].mean_ms / data["gpu"].mean_ms) + if speedups: + avg = np.mean(speedups) + print() + print(f" {colored('→', Style.GREEN)} GPU preprocessing avg {colored(f'{avg:.1f}x', Style.GREEN, bold=True)} faster") + + +def print_attention_results(results: Dict, device: torch.device): + """Print attention benchmark results.""" + print_subheader("ATTENTION (per layer forward pass)") + + print(f" {'Config':<18} {'SDPA':<12} {'Manual':<12} {'Speedup':<10}") + print(f" {'-'*52}") + + for name, data in results.items(): + sdpa_ms = data["sdpa"].mean_ms + manual_ms = data["manual"].mean_ms + speedup = manual_ms / sdpa_ms + print(f" {name:<18} {sdpa_ms:>6.3f} ms {manual_ms:>6.3f} ms {format_speedup(speedup)}") + + # Summary + speedups = [d["manual"].mean_ms / d["sdpa"].mean_ms for d in results.values()] + avg = np.mean(speedups) + print() + print(f" {colored('→', Style.GREEN)} SDPA avg {colored(f'{avg:.1f}x', Style.GREEN, bold=True)} faster than manual") + + # Check Flash SDP + if device.type == "cuda": + from torch.backends.cuda import flash_sdp_enabled + if flash_sdp_enabled(): + print(f" {colored('→', Style.GREEN)} Flash Attention: {colored('ENABLED', Style.GREEN, bold=True)} (PyTorch native)") + + +def print_inference_matrix(results: Dict, device: torch.device): + """Print inference benchmark matrix.""" + print_subheader("END-TO-END INFERENCE (720p input, batch=1)") + + configs = get_optimization_configs(device) + + # Header + header = f" {'Model':<12}" + for cfg in configs: + header += f" {cfg.short_name:<14}" + header += " Best" + print(header) + print(f" {'-'*(14 + 15*len(configs) + 6)}") + + # Results per model + for model_name, model_results in results.items(): + row = f" {model_name:<12}" + + best_fps = 0 + best_config = None + worst_fps = float('inf') + + for cfg in configs: + if cfg.name in model_results: + result = model_results[cfg.name]["result"] + fps = result.fps + row += f" {fps:>6.1f} img/s " + + if fps > best_fps: + best_fps = fps + best_config = cfg + if fps < worst_fps: + worst_fps = fps + else: + row += f" {'N/A':<14}" + + # Best indicator + if best_config: + row += f" {colored(best_config.short_name, Style.GREEN, bold=True)}" + + print(row) + + # Summary + print() + print(f" {Style.DIM}Legend: GPU=NVJPEG decode, CPU=PIL decode, SDPA=Flash Attention{Style.RESET}") + + +def print_device_summary( + device: torch.device, + preproc_results: Dict, + attn_results: Dict, + infer_results: Dict, +): + """Print summary for a device.""" + print() + print(colored("─" * 70, Style.CYAN)) + print(colored(f" {device.type.upper()} - OPTIMIZATION SUMMARY", Style.BOLD)) + print(colored("─" * 70, Style.CYAN)) + + # Best configuration + if infer_results: + print() + print(f" {colored('Best configuration per model:', Style.CYAN)}") + + for model_name, model_results in infer_results.items(): + if not model_results: + continue + + best_name = max(model_results.keys(), key=lambda k: model_results[k]["result"].fps) + best = model_results[best_name] + worst_name = min(model_results.keys(), key=lambda k: model_results[k]["result"].fps) + worst = model_results[worst_name] + + speedup = best["result"].fps / worst["result"].fps if worst["result"].fps > 0 else 1 + + print(f" {model_name:<12} {colored(best['config'].description, Style.GREEN)}") + print(f" {'':<12} {best['result'].fps:.1f} img/s ({speedup:.1f}x vs worst)") + + # Recommendations + print() + print(f" {colored('Recommendations:', Style.CYAN)}") + + if device.type == "cuda": + print(f" ✓ Use {colored('GPU preprocessing (NVJPEG)', Style.GREEN)} for file inputs") + print(f" ✓ {colored('SDPA (Flash Attention)', Style.GREEN)} is enabled by default") + print(f" ✓ Pass file paths (not PIL images) to leverage NVJPEG") + elif device.type == "mps": + print(f" ✓ Use {colored('CPU preprocessing', Style.GREEN)} (faster than GPU on MPS)") + print(f" ✓ {colored('SDPA', Style.GREEN)} provides moderate speedup") + else: + print(f" ✓ {colored('SDPA', Style.GREEN)} provides speedup over manual attention") + print(f" ○ Consider using GPU (CUDA/MPS) for better performance") + + +# ============================================================================ +# MAIN +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + description="DA3 Full Benchmark - Test all optimization combinations", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python benchmarks/full_benchmark.py # Best device only + python benchmarks/full_benchmark.py -d all # All devices + python benchmarks/full_benchmark.py -d cuda # CUDA only + python benchmarks/full_benchmark.py --quick # Quick mode (fewer runs) + python benchmarks/full_benchmark.py --models da3-small da3-large + """ + ) + parser.add_argument("--quick", action="store_true", help="Quick mode (fewer runs)") + parser.add_argument("--skip-preprocessing", action="store_true", help="Skip preprocessing benchmark") + parser.add_argument("--skip-attention", action="store_true", help="Skip attention benchmark") + parser.add_argument("--skip-inference", action="store_true", help="Skip inference benchmark") + parser.add_argument("-d", "--device", type=str, default="auto", + choices=["auto", "cpu", "mps", "cuda", "all"], + help="Device to benchmark (default: auto)") + parser.add_argument("--models", nargs="+", default=None, + help="Models to benchmark (default: all)") + args = parser.parse_args() + + # Configure runs + runs_preproc = 3 if args.quick else 5 + runs_attn = 5 if args.quick else 10 + runs_infer = 2 if args.quick else 4 + + # Determine models + if args.models: + models = args.models + elif args.quick: + models = ["da3-small", "da3-large"] + else: + models = ["da3-small", "da3-base", "da3-large"] + + # Determine devices + available_devices = get_available_devices() + if args.device == "auto": + devices_to_test = [available_devices[-1]] # Best available + elif args.device == "all": + devices_to_test = available_devices + else: + requested = torch.device(args.device) + if requested in available_devices: + devices_to_test = [requested] + else: + print(f"Error: Device '{args.device}' not available.") + print(f"Available: {[d.type for d in available_devices]}") + return + + # Main header + print() + print(colored("╔" + "═" * 68 + "╗", Style.CYAN)) + print(colored("║", Style.CYAN) + colored(" DEPTH ANYTHING 3 - FULL BENCHMARK", Style.BOLD).center(77) + colored("║", Style.CYAN)) + print(colored("║", Style.CYAN) + colored(" All Optimization Combinations", Style.DIM).center(77) + colored("║", Style.CYAN)) + print(colored("╚" + "═" * 68 + "╝", Style.CYAN)) + + print(f"\n {Style.DIM}PyTorch{Style.RESET} : {colored(torch.__version__, Style.CYAN)}") + print(f" {Style.DIM}Models{Style.RESET} : {colored(', '.join(models), Style.CYAN)}") + print(f" {Style.DIM}Mode{Style.RESET} : {colored('Quick' if args.quick else 'Full', Style.CYAN)}") + + print(f"\n {Style.DIM}Available devices:{Style.RESET}") + for d in available_devices: + status = colored("●", Style.GREEN) if d in devices_to_test else colored("○", Style.DIM) + print(f" {status} {d.type.upper():<6} {get_device_name(d)}") + + all_results = {} + + # Run benchmarks for each device + for device in devices_to_test: + device_name = get_device_name(device) + all_results[device.type] = {} + + print_header(f"{device.type.upper()} - {device_name}") + + # 1. Preprocessing + preproc_results = {} + if not args.skip_preprocessing and device.type != "cpu": + preproc_results = benchmark_preprocessing_detailed(device, runs=runs_preproc) + all_results[device.type]["preprocessing"] = preproc_results + print_preprocessing_results(preproc_results, device) + elif device.type == "cpu": + print_subheader("PREPROCESSING") + print(f" {Style.DIM}Skipped (CPU only - no GPU comparison){Style.RESET}") + + # 2. Attention + attn_results = {} + if not args.skip_attention: + attn_results = benchmark_attention_detailed(device, runs=runs_attn) + all_results[device.type]["attention"] = attn_results + print_attention_results(attn_results, device) + + # 3. Inference Matrix + infer_results = {} + if not args.skip_inference: + infer_results = benchmark_inference_matrix(device, models, runs=runs_infer) + all_results[device.type]["inference"] = infer_results + print_inference_matrix(infer_results, device) + + # Device Summary + print_device_summary(device, preproc_results, attn_results, infer_results) + + cleanup() + + # Cross-device comparison + if len(devices_to_test) > 1 and not args.skip_inference: + print_header("CROSS-DEVICE COMPARISON") + + # Find common model + common_model = models[-1] # Usually largest tested + + print() + print(f" {colored(f'{common_model} (best config per device):', Style.CYAN)}") + print(f" {'Device':<10} {'Config':<30} {'Performance':<15}") + print(f" {'-'*55}") + + base_fps = None + for device in devices_to_test: + if device.type in all_results and "inference" in all_results[device.type]: + infer = all_results[device.type]["inference"].get(common_model, {}) + if infer: + best_name = max(infer.keys(), key=lambda k: infer[k]["result"].fps) + best = infer[best_name] + fps = best["result"].fps + + if base_fps is None: + base_fps = fps + + speedup = fps / base_fps if base_fps else 1 + speedup_str = f"({speedup:.1f}x)" if device != devices_to_test[0] else "(baseline)" + + print(f" {device.type.upper():<10} {best['config'].description:<30} {fps:>5.1f} img/s {speedup_str}") + + # Final summary + print() + print(colored("═" * 70, Style.CYAN)) + print(colored("║", Style.CYAN) + colored(" BENCHMARK COMPLETE", Style.BOLD).center(77) + colored("║", Style.CYAN)) + print(colored("═" * 70, Style.CYAN)) + print() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/gpu_preprocessing_benchmark.py b/benchmarks/gpu_preprocessing_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..cf513b5b58e7b8ab2d4e9bc1520dba08a19de4d7 --- /dev/null +++ b/benchmarks/gpu_preprocessing_benchmark.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Delanoe Pirard +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +GPU Preprocessing Benchmark + +Compares CPU vs GPU preprocessing performance across different image sizes. +Measures: +- Preprocessing time only +- Total inference time (preprocessing + model forward) +- Memory usage +- Speedup percentages +""" + +import time +from typing import List, Tuple + +import numpy as np +import torch +from PIL import Image + +from depth_anything_3.utils.io.input_processor import InputProcessor +from depth_anything_3.utils.io.gpu_input_processor import GPUInputProcessor + + +import os +import shutil + +def create_test_files(sizes: List[Tuple[int, int]], count: int = 4, temp_dir: str = "temp_bench_imgs") -> Tuple[List[List[str]], str]: + """Create test image files on disk. + + Args: + sizes: List of (width, height) tuples + count: Number of images per size + temp_dir: Directory to save images + + Returns: + List of image path batches, one per size + Path to temp directory + """ + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + os.makedirs(temp_dir) + + batches = [] + for w, h, _ in sizes: + batch = [] + for i in range(count): + img = Image.new("RGB", (w, h), color=(i * 50, 100, 150)) + fname = f"{temp_dir}/{w}x{h}_{i}.jpg" + img.save(fname, quality=95, subsampling=0) + batch.append(fname) + batches.append(batch) + return batches, temp_dir + +def benchmark_gpu_decode_files( + processor, + image_paths: List[str], + process_res: int = 504, + warmup_runs: int = 2, + benchmark_runs: int = 10, + num_workers: int = 8, +) -> float: + """Benchmark GPU decoding (from file path).""" + # Warmup + for _ in range(warmup_runs): + processor( + image=image_paths, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + ) + + # Benchmark + times = [] + for _ in range(benchmark_runs): + if hasattr(processor, 'device') and processor.device.type == "cuda": + torch.cuda.synchronize() + + start = time.perf_counter() + # Pass file paths directly to GPUInputProcessor + tensor, _, _ = processor( + image=image_paths, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + ) + + if hasattr(processor, 'device') and processor.device.type == "cuda": + torch.cuda.synchronize() + + elapsed = time.perf_counter() - start + times.append(elapsed) + + return np.mean(times) + +def create_test_images(sizes: List[Tuple[int, int]], count: int = 4) -> List[List[Image.Image]]: + """Create test images for each size. + + Args: + sizes: List of (width, height) tuples + count: Number of images per size + + Returns: + List of image batches, one per size + """ + batches = [] + for w, h in sizes: + batch = [Image.new("RGB", (w, h), color=(i * 50, 100, 150)) for i in range(count)] + batches.append(batch) + return batches + + +def benchmark_hybrid( + processor, + images: List[Image.Image], + process_res: int = 504, + warmup_runs: int = 2, + benchmark_runs: int = 10, + num_workers: int = 8, + device=torch.device("cuda") +) -> float: + """Benchmark hybrid preprocessing (CPU resize -> GPU normalize).""" + + # Warmup + for _ in range(warmup_runs): + imgs_cpu, _, _ = processor( + image=images, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + perform_normalization=False + ) + imgs_gpu = imgs_cpu.to(device, non_blocking=True).float() / 255.0 + _ = InputProcessor.normalize_tensor(imgs_gpu, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + + # Benchmark + times = [] + for _ in range(benchmark_runs): + if device.type == "cuda": + torch.cuda.synchronize() + + start = time.perf_counter() + + # 1. CPU Preprocessing (uint8) + imgs_cpu, _, _ = processor( + image=images, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + perform_normalization=False + ) + + # 2. Transfer + Normalize + imgs_gpu = imgs_cpu.to(device, non_blocking=True).float() / 255.0 + _ = InputProcessor.normalize_tensor(imgs_gpu, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + + if device.type == "cuda": + torch.cuda.synchronize() + + elapsed = time.perf_counter() - start + times.append(elapsed) + + return np.mean(times) + +def benchmark_preprocessing( + processor, + images: List[Image.Image], + process_res: int = 504, + warmup_runs: int = 2, + benchmark_runs: int = 10, + num_workers: int = 8, +) -> float: + """Benchmark preprocessing performance. + + Args: + processor: InputProcessor or GPUInputProcessor instance + images: List of test images + process_res: Processing resolution + warmup_runs: Number of warmup runs to discard + benchmark_runs: Number of benchmark runs to average + num_workers: Number of parallel workers (for CPU processor) + + Returns: + Average preprocessing time in seconds + """ + # Warmup + for _ in range(warmup_runs): + processor( + image=images, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + ) + + # Benchmark + times = [] + for _ in range(benchmark_runs): + if hasattr(processor, 'device') and processor.device.type == "cuda": + torch.cuda.synchronize() + + start = time.perf_counter() + tensor, _, _ = processor( + image=images, + process_res=process_res, + process_res_method="upper_bound_resize", + num_workers=num_workers, + ) + + if hasattr(processor, 'device') and processor.device.type == "cuda": + torch.cuda.synchronize() + + elapsed = time.perf_counter() - start + times.append(elapsed) + + return np.mean(times) + + +def print_results_table(results: List[dict]): + """Pretty print benchmark results as table.""" + print("\n" + "=" * 140) + print("GPU PREPROCESSING BENCHMARK RESULTS") + print("=" * 140) + print(f"{'Image Size':<15} {'CPU Time':<12} {'GPU Time':<12} {'Hybrid Time':<12} {'GPU Decode':<12} {'Best Method':<15}") + print("-" * 140) + + for result in results: + size_str = f"{result['width']}x{result['height']}" + cpu_time = f"{result['cpu_time']*1000:.2f} ms" + gpu_time = f"{result['gpu_time']*1000:.2f} ms" + hybrid_time = f"{result['hybrid_time']*1000:.2f} ms" + gpu_decode_time = f"{result['gpu_decode_time']*1000:.2f} ms" + + times = [result['cpu_time'], result['gpu_time'], result['hybrid_time'], result['gpu_decode_time']] + labels = ["CPU", "GPU", "Hybrid", "GPU Decode"] + best_idx = np.argmin(times) + best = labels[best_idx] + + print(f"{size_str:<15} {cpu_time:<12} {gpu_time:<12} {hybrid_time:<12} {gpu_decode_time:<12} {best:<15}") + + print("=" * 140 + "\n") + + +def main(): + """Run comprehensive benchmark.""" + print("\n" + "=" * 100) + print("INITIALIZING GPU PREPROCESSING BENCHMARK") + print("=" * 100) + + # Check GPU availability + if torch.cuda.is_available(): + device_name = "cuda" + device_info = torch.cuda.get_device_name(0) + print(f"✓ GPU Device: {device_info}") + print("✓ GPU preprocessing: ENABLED (NVJPEG + Kornia)") + elif torch.backends.mps.is_available(): + device_name = "mps" + device_info = "Apple MPS" + print(f"✓ GPU Device: {device_info}") + print("ℹ GPU preprocessing: DISABLED on MPS (CPU is faster on Apple Silicon)") + print(" → GPUInputProcessor will use CPU path automatically") + print(" → GPU reserved for model inference (5-10x speedup there)") + else: + print("✗ No GPU available - benchmark will show CPU vs CPU (no speedup expected)") + device_name = "cpu" + device_info = "CPU only" + + device = torch.device(device_name) + + # Create processors + cpu_proc = InputProcessor() + gpu_proc = GPUInputProcessor(device=device_name) + print(f"✓ Processors initialized: CPU vs {device_name.upper()}") + + # Test configurations + # Format: (width, height, description) + test_sizes = [ + (640, 480, "Small (VGA)"), + (1280, 720, "Medium (HD)"), + (1920, 1080, "Large (Full HD)"), + (3840, 2160, "XLarge (4K)"), + ] + + process_res = 504 + num_images = 4 + num_workers = 8 + + print(f"✓ Test config: {num_images} images per batch, process_res={process_res}, num_workers={num_workers}") + print(f"✓ Testing {len(test_sizes)} image sizes: {', '.join([desc for _, _, desc in test_sizes])}") + + # Create test images + print("\nGenerating test images (PIL & Files)...") + image_batches_pil = create_test_images([(w, h) for w, h, _ in test_sizes], count=num_images) + image_batches_files, temp_dir = create_test_files(test_sizes, count=num_images) + print("✓ Test images generated") + + # Run benchmarks + print("\nRunning benchmarks (this may take a minute)...\n") + results = [] + + try: + for (w, h, desc), imgs_pil, imgs_files in zip(test_sizes, image_batches_pil, image_batches_files): + print(f"Benchmarking {desc} ({w}x{h})...", end=" ", flush=True) + + cpu_time = benchmark_preprocessing(cpu_proc, imgs_pil, process_res, num_workers=num_workers) + gpu_time = benchmark_preprocessing(gpu_proc, imgs_pil, process_res, num_workers=num_workers) + hybrid_time = benchmark_hybrid(cpu_proc, imgs_pil, process_res, num_workers=num_workers, device=device) + + # GPU Decode uses file paths + gpu_decode_time = benchmark_gpu_decode_files(gpu_proc, imgs_files, process_res, num_workers=num_workers) + + results.append({ + 'width': w, + 'height': h, + 'description': desc, + 'cpu_time': cpu_time, + 'gpu_time': gpu_time, + 'hybrid_time': hybrid_time, + 'gpu_decode_time': gpu_decode_time + }) + + best_time = min(cpu_time, gpu_time, hybrid_time, gpu_decode_time) + if best_time == gpu_decode_time: + win = "GPU Decode" + elif best_time == hybrid_time: + win = "Hybrid" + elif best_time == gpu_time: + win = "GPU" + else: + win = "CPU" + + print(f"✓ Best: {win}") + + # Print results table + print_results_table(results) + + # Memory info (CUDA only) + if device_name == "cuda": + print("\nGPU Memory Usage:") + print(f" Allocated: {torch.cuda.memory_allocated(0) / 1024**2:.1f} MB") + print(f" Cached: {torch.cuda.memory_reserved(0) / 1024**2:.1f} MB") + + finally: + # Cleanup + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + print(f"\n✓ Cleaned up temp directory: {temp_dir}") + +if __name__ == "__main__": + main() + diff --git a/benchmarks/results/temp_images/test_image_0000.jpg b/benchmarks/results/temp_images/test_image_0000.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c94f7a8d014884544d958dd0abc0621ec2199fbd --- /dev/null +++ b/benchmarks/results/temp_images/test_image_0000.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b66fe582ae8d3bb62f6dcff97784125ae59ea7e62bdd49e89699fb7664c41a08 +size 360371 diff --git a/benchmarks/results/temp_images/test_image_0001.jpg b/benchmarks/results/temp_images/test_image_0001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45689743b02d613569bd055731e2b4f78bf86e37 --- /dev/null +++ b/benchmarks/results/temp_images/test_image_0001.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24b4ecf7b65cc1f6c85453a03417da5a48d23295824d275e0cc9361a4feb36c5 +size 360564 diff --git a/benchmarks/results/temp_images/test_image_0002.jpg b/benchmarks/results/temp_images/test_image_0002.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6beaa8d0a80eaa64610897d54a6e48d05407387 --- /dev/null +++ b/benchmarks/results/temp_images/test_image_0002.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dc218e581801d2332d2b7fe4eb35a997dc8d564d643dadeb2ee7539c7bf76f9 +size 360218 diff --git a/benchmarks/results/temp_images/test_image_0003.jpg b/benchmarks/results/temp_images/test_image_0003.jpg new file mode 100644 index 0000000000000000000000000000000000000000..544e3d9f4cc32086cc70e36d8a3be2f45d8579e3 --- /dev/null +++ b/benchmarks/results/temp_images/test_image_0003.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4749f606ec40b7385eda5edecf5be20d02caed202fca5d39e198765dc93557f7 +size 360306 diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000000000000000000000000000000000000..89a9de362ce260b37cc86c6345974ef372ad9db9 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,465 @@ +# 📚 DepthAnything3 API Documentation + +## 📑 Table of Contents + +1. [📖 Overview](#overview) +2. [💡 Usage Examples](#usage-examples) +3. [🔧 Core API](#core-api) + - [DepthAnything3 Class](#depthanything3-class) + - [inference() Method](#inference-method) +4. [⚙️ Parameters](#parameters) + - [Input Parameters](#input-parameters) + - [Pose Alignment Parameters](#pose-alignment-parameters) + - [Feature Export Parameters](#feature-export-parameters) + - [Rendering Parameters](#rendering-parameters) + - [Processing Parameters](#processing-parameters) + - [Export Parameters](#export-parameters) +5. [📤 Export Formats](#export-formats) +6. [↩️ Return Value](#return-value) + +## 📖 Overview + +This documentation provides comprehensive API reference for DepthAnything3, including usage examples, parameter specifications, export formats, and advanced features. It covers both basic pose and depth estimation workflows and advanced pose-conditioned processing with multiple export capabilities. + +## 💡 Usage Examples + +Here are quick examples to get you started: + +### 🚀 Basic Depth Estimation +```python +from depth_anything_3.api import DepthAnything3 + +# Initialize and run inference +model = DepthAnything3.from_pretrained("depth-anything/DA3NESTED-GIANT-LARGE").to("cuda") +prediction = model.inference(["image1.jpg", "image2.jpg"]) +``` + +### 📷 Pose-Conditioned Depth Estimation +```python +import numpy as np + +# With camera parameters for better consistency +prediction = model.inference( + image=["image1.jpg", "image2.jpg"], + extrinsics=extrinsics_array, # (N, 4, 4) + intrinsics=intrinsics_array # (N, 3, 3) +) +``` + +### 📤 Export Results +```python +# Export depth data and 3D visualization +prediction = model.inference( + image=image_paths, + export_dir="./output", + export_format="mini_npz-glb" +) +``` + +### 🔍 Feature Extraction +```python +# Export intermediate features from specific layers +prediction = model.inference( + image=image_paths, + export_dir="./output", + export_format="feat_vis", + export_feat_layers=[0, 1, 2] # Export features from layers 0, 1, 2 +) +``` + +### ✨ Advanced Export with Gaussian Splatting +```python +# Export multiple formats including Gaussian Splatting +# Note: infer_gs=True requires da3-giant or da3nested-giant-large model +model = DepthAnything3(model_name="da3-giant").to("cuda") + +prediction = model.inference( + image=image_paths, + extrinsics=extrinsics_array, + intrinsics=intrinsics_array, + export_dir="./output", + export_format="npz-glb-gs_ply-gs_video", + align_to_input_ext_scale=True, + infer_gs=True, # Required for gs_ply and gs_video exports +) +``` + +### 🎨 Advanced Export with Feature Visualization +```python +# Export with intermediate feature visualization +prediction = model.inference( + image=image_paths, + export_dir="./output", + export_format="mini_npz-glb-depth_vis-feat_vis", + export_feat_layers=[0, 5, 10, 15, 20], + feat_vis_fps=30, +) +``` + +### 📐 Using Ray-Based Pose Estimation +```python +# Use ray-based pose estimation instead of camera decoder +prediction = model.inference( + image=image_paths, + export_dir="./output", + export_format="glb", + use_ray_pose=True, # Enable ray-based pose estimation +) +``` + +### 🎯 Reference View Selection +```python +# For multi-view inputs, automatically select the best reference view +prediction = model.inference( + image=image_paths, + ref_view_strategy="saddle_balanced", # Default: balanced selection +) + +# For video sequences, use middle frame as reference +prediction = model.inference( + image=video_frames, + ref_view_strategy="middle", # Good for temporally ordered inputs +) +``` + +## 🔧 Core API + +### 🔨 DepthAnything3 Class + +The main API class that provides depth estimation capabilities with optional pose conditioning. + +#### 🎯 Initialization + +```python +from depth_anything_3 import DepthAnything3 + +# Initialize the model with a model name +model = DepthAnything3(model_name="da3-large") +model = model.to("cuda") # Move to GPU +``` + +**Parameters:** +- `model_name` (str, default: "da3-large"): The name of the model preset to use. + - **Available models:** + - 🦾 `"da3-giant"` - 1.15B params, any-view model with GS support + - ⭐ `"da3-large"` - 0.35B params, any-view model (recommended for most use cases) + - 📦 `"da3-base"` - 0.12B params, any-view model + - 🪶 `"da3-small"` - 0.08B params, any-view model + - 👁️ `"da3mono-large"` - 0.35B params, monocular depth only + - 📏 `"da3metric-large"` - 0.35B params, metric depth with sky segmentation + - 🎯 `"da3nested-giant-large"` - 1.40B params, nested model with all features + +### 🚀 inference() Method + +The primary inference method that processes images and returns depth predictions. + +```python +prediction = model.inference( + image=image_list, + extrinsics=extrinsics_array, # Optional + intrinsics=intrinsics_array, # Optional + align_to_input_ext_scale=True, # Whether to align predicted poses to input scale + infer_gs=True, # Enable Gaussian branch for gs exports + use_ray_pose=False, # Use ray-based pose estimation instead of camera decoder + ref_view_strategy="saddle_balanced", # Reference view selection strategy + render_exts=render_extrinsics, # Optional renders for gs_video + render_ixts=render_intrinsics, # Optional renders for gs_video + render_hw=(height, width), # Optional renders for gs_video + process_res=504, + process_res_method="upper_bound_resize", + export_dir="output_directory", # Optional + export_format="mini_npz", + export_feat_layers=[], # List of layer indices to export features from + conf_thresh_percentile=40.0, # Confidence threshold percentile for depth map in GLB export + num_max_points=1_000_000, # Maximum number of points to export in GLB export + show_cameras=True, # Whether to show cameras in GLB export + feat_vis_fps=15, # Frames per second for feature visualization in feat_vis export + export_kwargs={} # Optional, additional arguments to export functions. export_format:key:val, see 'Parameters/Export Parameters' for details +) +``` + +## ⚙️ Parameters + +### 📸 Input Parameters + +#### `image` (required) +- **Type**: `List[Union[np.ndarray, Image.Image, str]]` +- **Description**: List of input images. Can be numpy arrays, PIL Images, or file paths. +- **Example**: + ```python + # From file paths + image = ["image1.jpg", "image2.jpg", "image3.jpg"] + + # From numpy arrays + image = [np.array(img1), np.array(img2)] + + # From PIL Images + image = [Image.open("image1.jpg"), Image.open("image2.jpg")] + ``` + +#### `extrinsics` (optional) +- **Type**: `Optional[np.ndarray]` +- **Shape**: `(N, 4, 4)` where N is the number of input images +- **Description**: Camera extrinsic matrices (world-to-camera transformation). When provided, enables pose-conditioned depth estimation mode. +- **Note**: If not provided, the model operates in standard depth estimation mode. + +#### `intrinsics` (optional) +- **Type**: `Optional[np.ndarray]` +- **Shape**: `(N, 3, 3)` where N is the number of input images +- **Description**: Camera intrinsic matrices containing focal length and principal point information. When provided, enables pose-conditioned depth estimation mode. + +### 🎯 Pose Alignment Parameters + +#### `align_to_input_ext_scale` (default: True) +- **Type**: `bool` +- **Description**: When True the predicted extrinsics are replaced with the input + ones and the depth maps are rescaled to match their metric scale. When False the + function returns the internally aligned poses computed via Umeyama alignment. + +#### `infer_gs` (default: False) +- **Type**: `bool` +- **Description**: Enable Gaussian Splatting branch for gaussian splatting exports. Required when using `gs_ply` or `gs_video` export formats. + +#### `use_ray_pose` (default: False) +- **Type**: `bool` +- **Description**: Use ray-based pose estimation instead of camera decoder for pose prediction. When True, the model uses ray prediction heads to estimate camera poses; when False, it uses the camera decoder approach. + +#### `ref_view_strategy` (default: "saddle_balanced") +- **Type**: `str` +- **Description**: Strategy for selecting the reference view from multiple input views. Options: `"first"`, `"middle"`, `"saddle_balanced"`, `"saddle_sim_range"`. Only applied when number of views ≥ 3. See [detailed documentation](funcs/ref_view_strategy.md) for strategy comparisons. +- **Available strategies**: + - `"saddle_balanced"`: Selects view with balanced features across multiple metrics (recommended default) + - `"saddle_sim_range"`: Selects view with largest similarity range + - `"first"`: Always uses first view (not recommended, equivalent to no reordering for views < 3) + - `"middle"`: Uses middle view (recommended for video sequences) + +### 🔍 Feature Export Parameters + +#### `export_feat_layers` (default: []) +- **Type**: `List[int]` +- **Description**: List of layer indices to export intermediate features from. Features are stored in the `aux` dictionary of the Prediction object with keys like `feat_layer_0`, `feat_layer_1`, etc. + +### 🎥 Rendering Parameters + +These arguments are only used when exporting Gaussian-splatting videos (include +`"gs_video"` in `export_format`). They describe an auxiliary camera trajectory +with ``M`` views. + +#### `render_exts` (optional) +- **Type**: `Optional[np.ndarray]` +- **Shape**: `(M, 4, 4)` +- **Description**: Camera extrinsics for the synthesized trajectory. If omitted, + the exporter falls back to the predicted poses. + +#### `render_ixts` (optional) +- **Type**: `Optional[np.ndarray]` +- **Shape**: `(M, 3, 3)` +- **Description**: Camera intrinsics for each rendered frame. Leave `None` to + reuse the input intrinsics. + +#### `render_hw` (optional) +- **Type**: `Optional[Tuple[int, int]]` +- **Description**: Explicit output resolution `(height, width)` for the rendered + frames. Defaults to the input resolution when not provided. + +### ⚡ Processing Parameters + +#### `process_res` (default: 504) +- **Type**: `int` +- **Description**: Base resolution for processing. The model will resize images to this resolution for inference. + +#### `process_res_method` (default: "upper_bound_resize") +- **Type**: `str` +- **Description**: Method for resizing images to the target resolution. +- **Options**: + - `"upper_bound_resize"`: Resize so that the specified dimension (504) becomes the longer side + - `"lower_bound_resize"`: Resize so that the specified dimension (504) becomes the shorter side +- **Example**: + - Input: 1200×1600 → Output: 378×504 (with `process_res=504`, `process_res_method="upper_bound_resize"`) + - Input: 504×672 → Output: 504×672 (no change needed) + +### 📦 Export Parameters + +#### `export_dir` (optional) +- **Type**: `Optional[str]` +- **Description**: Directory path where exported files will be saved. If not provided, no files will be exported. + +#### `export_format` (default: "mini_npz") +- **Type**: `str` +- **Description**: Format for exporting results. Supports multiple formats separated by `-`. +- **Example**: `"mini_npz-glb"` exports both mini_npz and glb formats. + +#### 🌐 GLB Export Parameters + +These parameters are passed directly to the `inference()` method and only apply when `export_format` includes `"glb"`. + +##### `conf_thresh_percentile` (default: 40.0) +- **Type**: `float` +- **Description**: Lower percentile for adaptive confidence threshold. Points below this confidence percentile will be filtered out from the point cloud. + +##### `num_max_points` (default: 1,000,000) +- **Type**: `int` +- **Description**: Maximum number of points in the exported point cloud. If the point cloud exceeds this limit, it will be downsampled. + +##### `show_cameras` (default: True) +- **Type**: `bool` +- **Description**: Whether to include camera wireframes in the exported GLB file for visualization. + +#### 🎨 Feature Visualization Parameters + +These parameters are passed directly to the `inference()` method and only apply when `export_format` includes `"feat_vis"`. + +##### `feat_vis_fps` (default: 15) +- **Type**: `int` +- **Description**: Frame rate for the output video when visualizing features across multiple images. + +#### ✨🎥 3DGS and 3DGS Video Parameters + +These parameters are passed directly to the `inference()` method and only apply when `export_format` includes `"gs_ply"` or `"gs_video"`. + +##### `export_kwargs` (default: `{}`) +- Type: `dict[str, dict[str, Any]]` +- Description: Per-format extra arguments passed to export functions, mainly for `"gs_ply"` and `"gs_video"`. + - Access pattern: `export_kwargs[export_format][key] = value` + - Example: + ```python + { + "gs_ply": { + "gs_views_interval": 1, + }, + "gs_video": { + "trj_mode": "interpolate_smooth", + "chunk_size": 1, + "vis_depth": None, + }, + } + ``` + +## 📤 Export Formats + +The API supports multiple export formats for different use cases: + +### 📊 `mini_npz` +- **Description**: Minimal NPZ format containing essential data +- **Contents**: `depth`, `conf`, `exts`, `ixts` +- **Use case**: Lightweight storage for depth data with camera parameters + +### 📦 `npz` +- **Description**: Full NPZ format with comprehensive data +- **Contents**: `depth`, `conf`, `exts`, `ixts`, `image`, etc. +- **Use case**: Complete data export for advanced processing + +### 🌐 `glb` +- **Description**: 3D visualization format with point cloud and camera poses +- **Contents**: + - Point cloud with colors from original images + - Camera wireframes for visualization + - Confidence-based filtering and downsampling +- **Use case**: 3D visualization, inspection, and analysis +- **Features**: + - Automatic sky depth handling + - Confidence threshold filtering + - Background filtering (black/white) + - Scene scale normalization +- **Parameters** (passed via `inference()` method directly): + - `conf_thresh_percentile` (float, default: 40.0): Lower percentile for adaptive confidence threshold. Points below this confidence percentile will be filtered out. + - `num_max_points` (int, default: 1,000,000): Maximum number of points in the exported point cloud. If exceeded, points will be downsampled. + - `show_cameras` (bool, default: True): Whether to include camera wireframes in the exported GLB file for visualization. + +### ✨ `gs_ply` +- **Description**: Gaussian Splatting point cloud format +- **Contents**: 3DGS data in PLY format. Compatible with standard 3DGS viewers such as [SuperSplat](https://superspl.at/editor) (recommended), [SPARK](https://sparkjs.dev/viewer/). +- **Use case**: Gaussian Splatting reconstruction +- **Requirements**: Must set `infer_gs=True` when calling `inference()`. Only supported by `da3-giant` and `da3nested-giant-large` models. +- **Additional configs**, provided via `export_kwargs` (see [Export Parameters](#export-parameters)): + - `gs_views_interval`: Export to 3DGS every N views, default: `1`. + +### 🎥 `gs_video` +- **Description**: Rasterized 3DGS to obtain videos +- **Contents**: A video of 3DGS-rasterized views using either provided viewpoints or a predefined camera trajectory. +- **Use case**: Video rendering for Gaussian Splatting +- **Requirements**: Must set `infer_gs=True` when calling `inference()`. Only supported by `da3-giant` and `da3nested-giant-large` models. +- **Note**: Can optionally use `render_exts`, `render_ixts`, and `render_hw` parameters in `inference()` method to specify novel viewpoints. +- **Additional configs**, provided via `export_kwargs` (see [Export Parameters](#export-parameters)): + - `extrinsics`: Optional world-to-camera poses for novel views. Falls back to the predicted poses of input views if not provided. (Alternatively, use `render_exts` parameter in `inference()`) + - `intrinsics`: Optional camera intrinsics for novel views. Falls back to the predicted intrinsics of input views if not provided. (Alternatively, use `render_ixts` parameter in `inference()`) + - `out_image_hw`: Optional output resolution `H x W`. Falls back to input resolution if not provided. (Alternatively, use `render_hw` parameter in `inference()`) + - `chunk_size`: Number of views rasterized per batch. Default: `8`. + - `trj_mode`: Predefined camera trajectory for novel-view rendering. + - `color_mode`: Same as `render_mode` in [gsplat](https://docs.gsplat.studio/main/apis/rasterization.html#gsplat.rasterization). + - `vis_depth`: How depth is combined with RGB. Default: `hcat` (horizontal concatenation). + - `enable_tqdm`: Whether to display a tqdm progress bar during rendering. + - `output_name`: File name of the rendered video. + - `video_quality`: Video quality to save. Default: `high`. + - `high`: High quality video (default) + - `medium`: Medium quality video (balance of storage space and quality) + - `low`: Low quality video (fewer storage space) + +### 🔍 `feat_vis` +- **Description**: Feature visualization format +- **Contents**: PCA-visualized intermediate features from specified layers +- **Use case**: Model interpretability and feature analysis +- **Note**: Requires `export_feat_layers` to be specified +- **Parameters** (passed via `inference()` method directly): + - `feat_vis_fps` (int, default: 15): Frame rate for the output video when visualizing features across multiple images. + +### 🎨 `depth_vis` +- **Description**: Depth visualization format +- **Contents**: Color-coded depth maps alongside original images +- **Use case**: Visual inspection of depth estimation quality + +### 🔗 Multiple Format Export +You can export multiple formats simultaneously by separating them with `-`: + +```python +# Export both mini_npz and glb formats +export_format = "mini_npz-glb" + +# Export multiple formats +export_format = "npz-glb-gs_ply" +``` + +## ↩️ Return Value + +The `inference()` method returns a `Prediction` object with the following attributes: + +### 📊 Core Outputs + +- **depth**: `np.ndarray` - Estimated depth maps with shape `(N, H, W)` where N is the number of images, H is height, and W is width. +- **conf**: `np.ndarray` - Confidence maps with shape `(N, H, W)` indicating prediction reliability (optional, depends on model). + +### 📷 Camera Parameters + +- **extrinsics**: `np.ndarray` - Camera extrinsic matrices with shape `(N, 3, 4)` representing world-to-camera transformations. Only present if camera poses were estimated or provided as input. +- **intrinsics**: `np.ndarray` - Camera intrinsic matrices with shape `(N, 3, 3)` containing focal length and principal point information. Only present if poses were estimated or provided as input. + +### 🎁 Additional Outputs + +- **processed_images**: `np.ndarray` - Preprocessed input images with shape `(N, H, W, 3)` in RGB format (0-255 uint8). +- **aux**: `dict` - Auxiliary outputs including: + - `feat_layer_X`: Intermediate features from layer X (if `export_feat_layers` was specified) + - `gaussians`: 3D Gaussian Splats data (if `infer_gs=True`) + +### 💻 Usage Example + +```python +prediction = model.inference(image=["img1.jpg", "img2.jpg"]) + +# Access depth maps +depth_maps = prediction.depth # shape: (2, H, W) + +# Access confidence +if hasattr(prediction, 'conf'): + confidence = prediction.conf + +# Access camera parameters (if available) +if hasattr(prediction, 'extrinsics'): + camera_poses = prediction.extrinsics # shape: (2, 4, 4) + +if hasattr(prediction, 'intrinsics'): + camera_intrinsics = prediction.intrinsics # shape: (2, 3, 3) + +# Access intermediate features (if export_feat_layers was set) +if hasattr(prediction, 'aux') and 'feat_layer_0' in prediction.aux: + features = prediction.aux['feat_layer_0'] +``` diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 0000000000000000000000000000000000000000..c99ba0bb7540028bae3e0fbe93c0945fe5140c1e --- /dev/null +++ b/docs/CLI.md @@ -0,0 +1,654 @@ +# 🚀 Depth Anything 3 Command Line Interface + +## 📋 Table of Contents + +- [📖 Overview](#overview) +- [⚡ Quick Start](#quick-start) +- [📚 Command Reference](#command-reference) + - [🤖 auto - Auto Mode](#auto---auto-mode) + - [🖼️ image - Single Image Processing](#image---single-image-processing) + - [🗂️ images - Image Directory Processing](#images---image-directory-processing) + - [🎬 video - Video Processing](#video---video-processing) + - [📐 colmap - COLMAP Dataset Processing](#colmap---colmap-dataset-processing) + - [🔧 backend - Backend Service](#backend---backend-service) + - [🎨 gradio - Gradio Application](#gradio---gradio-application) + - [🖼️ gallery - Gallery Server](#gallery---gallery-server) +- [⚙️ Parameter Details](#parameter-details) +- [💡 Usage Examples](#usage-examples) + +## 📖 Overview + +The Depth Anything 3 CLI provides a comprehensive command-line toolkit supporting image depth estimation, video processing, COLMAP dataset handling, and web applications. + +The backend service enables cache model to GPU so that we do not need to reload model for each command. + +## ⚡ Quick Start + +The CLI can run fully offline or connect to the backend for cached weights and task scheduling: + +```bash +# 🔧 Start backend service (optional, keeps model resident in GPU memory) +da3 backend --model-dir depth-anything/DA3NESTED-GIANT-LARGE + +# 🚀 Use auto mode to process input +da3 auto path/to/input --export-dir ./workspace/scene001 + +# ♻️ Reuse backend for next job +da3 auto path/to/video.mp4 \ + --export-dir ./workspace/scene002 \ + --use-backend \ + --backend-url http://localhost:8008 +``` + +Each export directory contains `scene.glb`, `scene.jpg`, and optional extras such as `depth_vis/` or `gs_video/` depending on the requested format. + +## 📚 Command Reference + +### 🤖 auto - Auto Mode + +Automatically detect input type and dispatch to the appropriate handler. + +**Usage:** + +```bash +da3 auto INPUT_PATH [OPTIONS] +``` + +**Input Type Detection:** +- 🖼️ Single image file (.jpg, .png, .jpeg, .webp, .bmp, .tiff, .tif) +- 📁 Image directory +- 🎬 Video file (.mp4, .avi, .mov, .mkv, .flv, .wmv, .webm, .m4v) +- 📐 COLMAP directory (containing `images/` and `sparse/` subdirectories) + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `INPUT_PATH` | str | Required | Input path (image, directory, video, or COLMAP) | +| `--model-dir` | str | Default model | Model directory path | +| `--export-dir` | str | `debug` | Export directory | +| `--export-format` | str | `glb` | Export format (supports `mini_npz`, `glb`, `feat_vis`, etc., can be combined with hyphens) | +| `--device` | str | `cuda` | Device to use | +| `--use-backend` | bool | `False` | Use backend service for inference | +| `--backend-url` | str | `http://localhost:8008` | Backend service URL | +| `--process-res` | int | `504` | Processing resolution | +| `--process-res-method` | str | `upper_bound_resize` | Processing resolution method | +| `--export-feat` | str | `""` | Export features from specified layers, comma-separated (e.g., `"0,1,2"`) | +| `--auto-cleanup` | bool | `False` | Automatically clean export directory without confirmation | +| `--fps` | float | `1.0` | [Video] Frame sampling FPS | +| `--sparse-subdir` | str | `""` | [COLMAP] Sparse reconstruction subdirectory (e.g., `"0"` for `sparse/0/`) | +| `--align-to-input-ext-scale` | bool | `True` | [COLMAP] Align prediction to input extrinsics scale | +| `--use-ray-pose` | bool | `False` | Use ray-based pose estimation instead of camera decoder | +| `--ref-view-strategy` | str | `saddle_balanced` | Reference view selection strategy: `first`, `middle`, `saddle_balanced`, `saddle_sim_range`. See [docs](funcs/ref_view_strategy.md) | +| `--conf-thresh-percentile` | float | `40.0` | [GLB] Lower percentile for adaptive confidence threshold | +| `--num-max-points` | int | `1000000` | [GLB] Maximum number of points in the point cloud | +| `--show-cameras` | bool | `True` | [GLB] Show camera wireframes in the exported scene | +| `--feat-vis-fps` | int | `15` | [FEAT_VIS] Frame rate for output video | + +**Examples:** + +```bash +# 🖼️ Auto-process an image +da3 auto path/to/image.jpg --export-dir ./output + +# 🎬 Auto-process a video +da3 auto path/to/video.mp4 --fps 2.0 --export-dir ./output + +# 🔧 Use backend service +da3 auto path/to/input \ + --export-format mini_npz-glb \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output +``` + +--- + +### 🖼️ image - Single Image Processing + +Process a single image for camera pose and depth estimation. + +**Usage:** + +```bash +da3 image IMAGE_PATH [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `IMAGE_PATH` | str | Required | Input image file path | +| `--model-dir` | str | Default model | Model directory path | +| `--export-dir` | str | `debug` | Export directory | +| `--export-format` | str | `glb` | Export format | +| `--device` | str | `cuda` | Device to use | +| `--use-backend` | bool | `False` | Use backend service for inference | +| `--backend-url` | str | `http://localhost:8008` | Backend service URL | +| `--process-res` | int | `504` | Processing resolution | +| `--process-res-method` | str | `upper_bound_resize` | Processing resolution method | +| `--export-feat` | str | `""` | Export feature layer indices (comma-separated) | +| `--auto-cleanup` | bool | `False` | Automatically clean export directory | +| `--use-ray-pose` | bool | `False` | Use ray-based pose estimation instead of camera decoder | +| `--ref-view-strategy` | str | `saddle_balanced` | Reference view selection strategy. See [docs](funcs/ref_view_strategy.md) | +| `--conf-thresh-percentile` | float | `40.0` | [GLB] Confidence threshold percentile | +| `--num-max-points` | int | `1000000` | [GLB] Maximum number of points | +| `--show-cameras` | bool | `True` | [GLB] Show cameras | +| `--feat-vis-fps` | int | `15` | [FEAT_VIS] Video frame rate | + +**Examples:** + +```bash +# ✨ Basic usage +da3 image path/to/image.png --export-dir ./output + +# ⚡ With backend acceleration +da3 image path/to/image.png \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output + +# 🔍 Export feature visualization +da3 image image.jpg \ + --export-format feat_vis \ + --export-feat "9,19,29,39" \ + --export-dir ./results +``` + +--- + +### 🗂️ images - Image Directory Processing + +Process a directory of images for batch depth estimation. + +**Usage:** + +```bash +da3 images IMAGES_DIR [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `IMAGES_DIR` | str | Required | Directory path containing images | +| `--image-extensions` | str | `png,jpg,jpeg` | Image file extensions to process (comma-separated) | +| `--model-dir` | str | Default model | Model directory path | +| `--export-dir` | str | `debug` | Export directory | +| `--export-format` | str | `glb` | Export format | +| `--device` | str | `cuda` | Device to use | +| `--use-backend` | bool | `False` | Use backend service for inference | +| `--backend-url` | str | `http://localhost:8008` | Backend service URL | +| `--process-res` | int | `504` | Processing resolution | +| `--process-res-method` | str | `upper_bound_resize` | Processing resolution method | +| `--export-feat` | str | `""` | Export feature layer indices | +| `--auto-cleanup` | bool | `False` | Automatically clean export directory | +| `--use-ray-pose` | bool | `False` | Use ray-based pose estimation instead of camera decoder | +| `--ref-view-strategy` | str | `saddle_balanced` | Reference view selection strategy. See [docs](funcs/ref_view_strategy.md) | +| `--conf-thresh-percentile` | float | `40.0` | [GLB] Confidence threshold percentile | +| `--num-max-points` | int | `1000000` | [GLB] Maximum number of points | +| `--show-cameras` | bool | `True` | [GLB] Show cameras | +| `--feat-vis-fps` | int | `15` | [FEAT_VIS] Video frame rate | + +**Examples:** + +```bash +# 📁 Process directory (defaults to png/jpg/jpeg) +da3 images ./image_folder --export-dir ./output + +# 🎯 Custom extensions +da3 images ./dataset --image-extensions "png,jpg,webp" --export-dir ./output + +# 🔧 Use backend service +da3 images ./dataset \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output +``` + +--- + +### 🎬 video - Video Processing + +Process video by extracting frames for depth estimation. + +**Usage:** + +```bash +da3 video VIDEO_PATH [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `VIDEO_PATH` | str | Required | Input video file path | +| `--fps` | float | `1.0` | Frame extraction sampling FPS | +| `--model-dir` | str | Default model | Model directory path | +| `--export-dir` | str | `debug` | Export directory | +| `--export-format` | str | `glb` | Export format | +| `--device` | str | `cuda` | Device to use | +| `--use-backend` | bool | `False` | Use backend service for inference | +| `--backend-url` | str | `http://localhost:8008` | Backend service URL | +| `--process-res` | int | `504` | Processing resolution | +| `--process-res-method` | str | `upper_bound_resize` | Processing resolution method | +| `--export-feat` | str | `""` | Export feature layer indices | +| `--auto-cleanup` | bool | `False` | Automatically clean export directory | +| `--use-ray-pose` | bool | `False` | Use ray-based pose estimation instead of camera decoder | +| `--ref-view-strategy` | str | `saddle_balanced` | Reference view selection strategy. See [docs](funcs/ref_view_strategy.md) | +| `--conf-thresh-percentile` | float | `40.0` | [GLB] Confidence threshold percentile | +| `--num-max-points` | int | `1000000` | [GLB] Maximum number of points | +| `--show-cameras` | bool | `True` | [GLB] Show cameras | +| `--feat-vis-fps` | int | `15` | [FEAT_VIS] Video frame rate | + +**Examples:** + +```bash +# ✨ Basic video processing +da3 video path/to/video.mp4 --export-dir ./output + +# ⚙️ Control frame sampling and resolution +da3 video path/to/video.mp4 \ + --fps 2.0 \ + --process-res 1024 \ + --export-dir ./output + +# 🔧 Use backend service +da3 video path/to/video.mp4 \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output +``` + +--- + +### 📐 colmap - COLMAP Dataset Processing + +Run pose-conditioned depth estimation on COLMAP data. + +**Usage:** + +```bash +da3 colmap COLMAP_DIR [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `COLMAP_DIR` | str | Required | COLMAP directory containing `images/` and `sparse/` subdirectories | +| `--sparse-subdir` | str | `""` | Sparse reconstruction subdirectory (e.g., `"0"` for `sparse/0/`) | +| `--align-to-input-ext-scale` | bool | `True` | Align prediction to input extrinsics scale | +| `--model-dir` | str | Default model | Model directory path | +| `--export-dir` | str | `debug` | Export directory | +| `--export-format` | str | `glb` | Export format | +| `--device` | str | `cuda` | Device to use | +| `--use-backend` | bool | `False` | Use backend service for inference | +| `--backend-url` | str | `http://localhost:8008` | Backend service URL | +| `--process-res` | int | `504` | Processing resolution | +| `--process-res-method` | str | `upper_bound_resize` | Processing resolution method | +| `--export-feat` | str | `""` | Export feature layer indices | +| `--auto-cleanup` | bool | `False` | Automatically clean export directory | +| `--use-ray-pose` | bool | `False` | Use ray-based pose estimation instead of camera decoder | +| `--ref-view-strategy` | str | `saddle_balanced` | Reference view selection strategy. See [docs](funcs/ref_view_strategy.md) | +| `--conf-thresh-percentile` | float | `40.0` | [GLB] Confidence threshold percentile | +| `--num-max-points` | int | `1000000` | [GLB] Maximum number of points | +| `--show-cameras` | bool | `True` | [GLB] Show cameras | +| `--feat-vis-fps` | int | `15` | [FEAT_VIS] Video frame rate | + +**Examples:** + +```bash +# 📐 Process COLMAP dataset +da3 colmap ./colmap_dataset --export-dir ./output + +# 🎯 Use specific sparse subdirectory and align scale +da3 colmap ./colmap_dataset \ + --sparse-subdir 0 \ + --align-to-input-ext-scale \ + --export-dir ./output + +# 🔧 Use backend service +da3 colmap ./colmap_dataset \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output +``` + +--- + +### 🔧 backend - Backend Service + +Start model backend service with integrated gallery. + +**Usage:** + +```bash +da3 backend [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--model-dir` | str | Default model | Model directory path | +| `--device` | str | `cuda` | Device to use | +| `--host` | str | `127.0.0.1` | Host address to bind to | +| `--port` | int | `8008` | Port number to bind to | +| `--gallery-dir` | str | Default gallery dir | Gallery directory path (optional) | + +**Features:** +- 🎯 Keeps model resident in GPU memory +- 🔌 Provides REST inference API +- 📊 Integrated dashboard and status monitoring +- 🖼️ Optional gallery browser (if `--gallery-dir` is provided) + +**Available Endpoints:** +- 🏠 `/` - Home page +- 📊 `/dashboard` - Dashboard +- ✅ `/status` - API status +- 🖼️ `/gallery/` - Gallery browser (if enabled) + +**Examples:** + +```bash +# 🚀 Basic backend service +da3 backend --model-dir depth-anything/DA3NESTED-GIANT-LARGE + +# 🖼️ Backend with gallery +da3 backend \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --device cuda \ + --host 0.0.0.0 \ + --port 8008 \ + --gallery-dir ./workspace + +# 💻 Use CPU +da3 backend --model-dir depth-anything/DA3NESTED-GIANT-LARGE --device cpu +``` + +--- + +### 🎨 gradio - Gradio Application + +Launch Depth Anything 3 Gradio interactive web application. + +**Usage:** + +```bash +da3 gradio [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--model-dir` | str | Required | Model directory path | +| `--workspace-dir` | str | Required | Workspace directory path | +| `--gallery-dir` | str | Required | Gallery directory path | +| `--host` | str | `127.0.0.1` | Host address to bind to | +| `--port` | int | `7860` | Port number to bind to | +| `--share` | bool | `False` | Create a public link | +| `--debug` | bool | `False` | Enable debug mode | +| `--cache-examples` | bool | `False` | Pre-cache all example scenes at startup | +| `--cache-gs-tag` | str | `""` | Tag to match scene names for high-res+3DGS caching | + +**Examples:** + +```bash +# 🎨 Basic Gradio application +da3 gradio \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --workspace-dir ./workspace \ + --gallery-dir ./gallery + +# 🌐 Enable sharing and debug +da3 gradio \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --workspace-dir ./workspace \ + --gallery-dir ./gallery \ + --share \ + --debug + +# ⚡ Pre-cache examples +da3 gradio \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --workspace-dir ./workspace \ + --gallery-dir ./gallery \ + --cache-examples \ + --cache-gs-tag "dl3dv" +``` + +--- + +### 🖼️ gallery - Gallery Server + +Launch standalone Depth Anything 3 Gallery server. + +**Usage:** + +```bash +da3 gallery [OPTIONS] +``` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--gallery-dir` | str | Default gallery dir | Gallery root directory | +| `--host` | str | `127.0.0.1` | Host address to bind to | +| `--port` | int | `8007` | Port number to bind to | +| `--open-browser` | bool | `False` | Open browser after launch | + +**Note:** +The gallery expects each scene folder to contain at least `scene.glb` and `scene.jpg`, with optional subfolders such as `depth_vis/` or `gs_video/`. + +**Examples:** + +```bash +# 🖼️ Basic gallery server +da3 gallery --gallery-dir ./workspace + +# 🌐 Custom host and port +da3 gallery \ + --gallery-dir ./workspace \ + --host 0.0.0.0 \ + --port 8007 + +# 🚀 Auto-open browser +da3 gallery --gallery-dir ./workspace --open-browser +``` + +--- + +## ⚙️ Parameter Details + +### 🔧 Common Parameters + +- **`--export-dir`**: Output directory, defaults to `debug` +- **`--export-format`**: Export format, supports combining multiple formats with hyphens: + - 📦 `mini_npz`: Compressed NumPy format + - 🎨 `glb`: glTF binary format (3D scene) + - 🔍 `feat_vis`: Feature visualization + - Example: `mini_npz-glb` exports both formats + +- **`--process-res`** / **`--process-res-method`**: Control preprocessing resolution strategy + - `process-res`: Target resolution (default 504) + - `process-res-method`: Resize method (default `upper_bound_resize`) + +- **`--auto-cleanup`**: Remove existing export directory without confirmation + +- **`--use-backend`** / **`--backend-url`**: Reuse running backend service + - ⚡ Reduces model loading time + - 🌐 Supports distributed processing + +- **`--export-feat`**: Layer indices for exporting intermediate features (comma-separated) + - Example: `"9,19,29,39"` + +### 🎨 GLB Export Parameters + +- **`--conf-thresh-percentile`**: Lower percentile for adaptive confidence threshold (default 40.0) + - Used to filter low-confidence points + +- **`--num-max-points`**: Maximum number of points in point cloud (default 1,000,000) + - Controls output file size and performance + +- **`--show-cameras`**: Show camera wireframes in exported scene (default True) + +### 🔍 Feature Visualization Parameters + +- **`--feat-vis-fps`**: Frame rate for feature visualization output video (default 15) + +### 🎬 Video-Specific Parameters + +- **`--fps`**: Video frame extraction sampling rate (default 1.0 FPS) + - Higher values extract more frames + +### 📐 COLMAP-Specific Parameters + +- **`--sparse-subdir`**: Sparse reconstruction subdirectory + - Empty string uses `sparse/` directory + - `"0"` uses `sparse/0/` directory + +- **`--align-to-input-ext-scale`**: Align prediction to input extrinsics scale (default True) + - Ensures depth estimation is consistent with COLMAP scale + +--- + +## 💡 Usage Examples + +### 1️⃣ Basic Workflow + +```bash +# 🔧 Start backend service +da3 backend --model-dir depth-anything/DA3NESTED-GIANT-LARGE --host 0.0.0.0 --port 8008 + +# 🖼️ Process single image +da3 image image.jpg --export-dir ./output1 --use-backend + +# 🎬 Process video +da3 video video.mp4 --fps 2.0 --export-dir ./output2 --use-backend + +# 📐 Process COLMAP dataset +da3 colmap ./colmap_data --export-dir ./output3 --use-backend +``` + +### 2️⃣ Using Auto Mode + +```bash +# 🤖 Auto-detect and process +da3 auto ./unknown_input --export-dir ./output + +# ⚡ With backend acceleration +da3 auto ./unknown_input \ + --use-backend \ + --backend-url http://localhost:8008 \ + --export-dir ./output +``` + +### 3️⃣ Multi-Format Export + +```bash +# 📦 Export both NPZ and GLB formats +da3 auto assets/examples/SOH \ + --export-format mini_npz-glb \ + --export-dir ./workspace/soh + +# 🔍 Export feature visualization +da3 image image.jpg \ + --export-format feat_vis \ + --export-feat "9,19,29,39" \ + --export-dir ./results +``` + +### 4️⃣ Advanced Configuration + +```bash +# ⚙️ Custom resolution and point cloud density +da3 image image.jpg \ + --process-res 1024 \ + --num-max-points 2000000 \ + --conf-thresh-percentile 30.0 \ + --export-dir ./output + +# 📐 COLMAP advanced options +da3 colmap ./colmap_data \ + --sparse-subdir 0 \ + --align-to-input-ext-scale \ + --process-res 756 \ + --export-dir ./output +``` + +### 5️⃣ Batch Processing Workflow + +```bash +# 🔧 Start backend +da3 backend \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --device cuda \ + --host 0.0.0.0 \ + --port 8008 \ + --gallery-dir ./workspace + +# 🔄 Batch process multiple scenes +for scene in scene1 scene2 scene3; do + da3 auto ./data/$scene \ + --export-dir ./workspace/$scene \ + --use-backend \ + --auto-cleanup +done + +# 🖼️ Launch gallery to view results +da3 gallery --gallery-dir ./workspace --open-browser +``` + +### 6️⃣ Web Applications + +```bash +# 🎨 Launch Gradio application +da3 gradio \ + --model-dir depth-anything/DA3NESTED-GIANT-LARGE \ + --workspace-dir workspace/gradio \ + --gallery-dir ./gallery \ + --host 0.0.0.0 \ + --port 7860 \ + --share +``` + +### 7️⃣ Transformer Feature Visualization + +```bash +# 🔍 Export Transformer features +# 📦 Combined with numerical output +da3 auto video.mp4 \ + --export-format glb-feat_vis \ + --export-feat "11,21,31" \ + --export-dir ./debug \ + --use-backend +``` + +--- + +## 📝 Notes + +1. **🔧 Backend Service**: Recommended for processing multiple tasks to improve efficiency +2. **💾 GPU Memory**: Be mindful of GPU memory usage when processing high-resolution inputs +3. **📁 Export Directory**: Use `--auto-cleanup` to avoid manual confirmation for deletion +4. **🔀 Format Combination**: Multiple export formats can be combined with hyphens (e.g., `mini_npz-glb-feat_vis`) +5. **📐 COLMAP Data**: Ensure COLMAP directory structure is correct (contains `images/` and `sparse/` subdirectories) + +--- + +## ❓ Getting Help + +View detailed help for any command: + +```bash +# 📖 View main help +da3 --help + +# 🔍 View specific command help +da3 auto --help +da3 image --help +da3 backend --help +``` diff --git a/docs/funcs/ref_view_strategy.md b/docs/funcs/ref_view_strategy.md new file mode 100644 index 0000000000000000000000000000000000000000..840f27293653557e323d4b3aae29fbc88ec738de --- /dev/null +++ b/docs/funcs/ref_view_strategy.md @@ -0,0 +1,183 @@ +# 📐 Reference View Selection Strategy + +## 📖 Overview + +Reference view selection is a component in multi-view depth estimation. When processing multiple input views, the model needs to determine which view should serve as the primary reference frame for depth prediction, defining the world coordinate system. + +Different reference view will leads to different reconstruction results. This is a known consideration in multi-view geometry and was analyzed in [PI3](https://arxiv.org/abs/2507.13347). The choice of reference view can affect the quality and consistency of depth predictions across the scene. + + +## 🚀 Our Simple Solution: Automatic Reference View Selection + +DA3 provides a simple approach to address this through **automatic reference view selection** based on **class tokens**. Instead of relying on heuristics or manual selection, the model analyzes the class token features from all input views and intelligently selects the most suitable reference frame. + +--- + +## 🎨 Available Strategies + +### 1. ⚖️ `saddle_balanced` (Recommended, Default) + +**Philosophy:** +Select a view that achieves balance across multiple feature metrics. This strategy looks for a "middle ground" view that is neither too similar nor too different from other views, making it a stable reference point. + +**How it works:** +1. Extracts and normalizes class tokens from all views +2. Computes three complementary metrics for each view: + - **Similarity score**: Average cosine similarity with other views + - **Feature norm**: L2 norm of the original features + - **Feature variance**: Variance across feature dimensions +3. Normalizes each metric to [0, 1] range +4. Selects the view closest to 0.5 (median) across all three metrics + +### 2. 🎢 `saddle_sim_range` + +**Philosophy:** +Select a view with the largest similarity range to other views. This identifies "saddle point" views that are highly similar to some views but dissimilar to others, making them information-rich anchor points. + +**How it works:** +1. Computes pairwise cosine similarity between all views +2. For each view, calculates the range (max - min) of similarities to other views +3. Selects the view with the maximum similarity range + +--- + +### 3. 1️⃣ `first` (Not Recommended) + +**Philosophy:** +Always use the first view in the input sequence as the reference. + +**How it works:** +Simply returns index 0. + +**When to use:** +- ⛔ **Not recommended** in general +- 🔧 Only use when you have manually pre-sorted your views and know the first view is optimal +- 🐛 Debugging or baseline comparisons + +--- + +### 4. ⏸️ `middle` + +**Philosophy:** +Select the view in the middle of the input sequence. + +**How it works:** +Returns the view at index `S // 2` where S is the number of views. + +**When to use:** +- ⏱️ **Only recommended when input images are temporally ordered** +- 🎬 Video sequences (e.g., **DA3-LONG** setting) +- 📹 Sequential captures where the middle frame likely has the most stable viewpoint + +**Specific use case: DA3-LONG** 🎬 +In video-based depth estimation scenarios (like DA3-LONG), where inputs are consecutive frames, `middle` is often the **optimal choice** because that it has maximum overlap with all other frames. + + +## 💻 Usage + +### 🐍 Python API + +```python +from depth_anything_3 import DepthAnything3 + +model = DepthAnything3.from_pretrained("depth-anything/DA3NESTED-GIANT-LARGE") + +# Use default (saddle_balanced) +prediction = model.inference( + images, + ref_view_strategy="saddle_balanced" +) + +# For video sequences, consider using middle +prediction = model.inference( + video_frames, + ref_view_strategy="middle" # Good for temporal sequences +) + +# For complex scenes with wide baselines +prediction = model.inference( + images, + ref_view_strategy="saddle_sim_range" +) +``` + +### 🖥️ Command Line Interface + +```bash +# Default (saddle_balanced) +da3 auto input/ --export-dir output/ + +# Explicitly specify strategy +da3 auto input/ --ref-view-strategy saddle_balanced + +# For video processing +da3 video input.mp4 --ref-view-strategy middle + +# For wide-baseline multi-view +da3 images captures/ --ref-view-strategy saddle_sim_range +``` + +--- + +### 🎯 When Selection Is Applied + +Reference view selection is applied when: +- 3️⃣ Number of views S ≥ 3 + +--- + +## 💡 Recommendations + +### 📋 Quick Guide + +| Scenario | Recommended Strategy | Rationale | +|----------|---------------------|-----------| +| **Default / Unknown** | `saddle_balanced` | Robust, balanced, works well across diverse scenarios | +| **Video frames** | `middle` | Temporal coherence, stable middle frame | +| **Wide-baseline multi-view** | `saddle_sim_range` | Maximizes information coverage | +| **Pre-sorted inputs** | `first` | Use only if you've manually optimized ordering | +| **Single image** | `first` | Automatically used (no reordering needed for S ≤ 2) | + +### ✨ Best Practices + +1. 🎯 **Start with defaults**: `saddle_balanced` works well in most cases +2. 🎬 **Consider your input type**: Use `middle` for videos, `saddle_balanced` for photos +3. 🔬 **Experiment if needed**: Try different strategies if results are suboptimal +4. 📊 **Monitor performance**: Check `glb` quality and consistency across views. + +--- + +## 🔧 Technical Details + +### 🎚️ Selection Threshold + +The reference view selection is only triggered when: +```python +num_views >= 3 # At least 3 views required +``` + +For 1-2 views, no reordering is performed (equivalent to using `first`). + +### ⚙️ Implementation + +The selection happens at layer `alt_start - 1` in the vision transformer, before the first global attention layer. This ensures the selected reference view influences the entire depth prediction pipeline. + +--- + +## ❓ FAQ + +**Q: 🤔 Why is this feature provided?** +A: The model can handle any view order, but this feature provides automatic optimization for reference view selection, which can help improve depth prediction quality in multi-view scenarios. + +**Q: ⏱️ Does this add computational cost?** +A: The overhead is totally negligible. + +**Q: 🎮 Can I manually specify which view to use as reference?** +A: Not directly through this parameter. You can pre-sort your input images to place your preferred reference view first and use `ref_view_strategy="first"`. + +**Q: ⚙️ What happens if I don't specify this parameter?** +A: The default `saddle_balanced` strategy is used automatically. + +**Q: 📊 Is this feature used in the DA3 paper benchmarks?** +A: No, the paper used `first` as the default strategy for all multi-view experiments. The current default has been updated to `saddle_balanced` for better robustness. + diff --git a/notebooks/da3.ipynb b/notebooks/da3.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..9f98cdbb641292f658139d34d3286b71a3b2009c --- /dev/null +++ b/notebooks/da3.ipynb @@ -0,0 +1,150 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Depth Anything 3 (DA3) Usage Example\n", + "\n", + "This notebook demonstrates how to use Depth Anything 3 for camera poses and depth estimation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "# !pip install depth-anything-3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading weights from local directory\n", + "Model loaded on cuda\n" + ] + } + ], + "source": [ + "import os\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from PIL import Image\n", + "import torch\n", + "from depth_anything_3.api import DepthAnything3\n", + "from depth_anything_3.utils.visualize import visualize_depth\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "model = DepthAnything3.from_pretrained(\"depth-anything/DA3NESTED-GIANT-LARGE\")\n", + "model = model.to(device)\n", + "model.eval()\n", + "print(f\"Model loaded on {device}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Depth shape: (2, 280, 504)\n", + "Extrinsics: (2, 3, 4)\n", + "Intrinsics: (2, 3, 3)\n" + ] + } + ], + "source": [ + "# Load sample images and run inference\n", + "image_paths = [\n", + " \"assets/examples/SOH/000.png\",\n", + " \"assets/examples/SOH/010.png\"\n", + "]\n", + "\n", + "# Run inference\n", + "prediction = model.inference(\n", + " image=image_paths,\n", + " process_res=504,\n", + " process_res_method=\"upper_bound_resize\",\n", + " export_dir=None,\n", + " export_format=\"glb\"\n", + ")\n", + "print(f\"Depth shape: {prediction.depth.shape}\")\n", + "print(f\"Extrinsics: {prediction.extrinsics.shape if prediction.extrinsics is not None else 'None'}\")\n", + "print(f\"Intrinsics: {prediction.intrinsics.shape if prediction.intrinsics is not None else 'None'}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABBkAAAJOCAYAAADyJN+HAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs/VusbUlyFox+kTnmXGvtte916+q2++K2227j3wcwxwbJli3EkUEgZFtICAQWMtI5biE/YFl+MtCW4AFhGyMhg4Stg2xkyW/wgoRkZAPiWFwewDS4bffvrr5UV1fVrtq3dZmXkRnnISNyRObIMedce+/q6mqPqFp7zjlG3jMyIjIyIpKYmTHDDDPMMMMMM8wwwwwzzDDDDDPM8JTg3u0GzDDDDDPMMMMMM8wwwwwzzDDDDF8bMCsZZphhhhlmmGGGGWaYYYYZZphhhmcCs5JhhhlmmGGGGWaYYYYZZphhhhlmeCYwKxlmmGGGGWaYYYYZZphhhhlmmGGGZwKzkmGGGWaYYYYZZphhhhlmmGGGGWZ4JjArGWaYYYYZZphhhhlmmGGGGWaYYYZnArOSYYYZZphhhhlmmGGGGWaYYYYZZngmMCsZZphhhhlmmGGGGWaYYYYZZphhhmcCs5JhhhlmmGGGGWaYYYYZZphhhhlmeCYwKxlmmGGGGWaYYYYZZphhhhlmmGGGZwKzkmGGGZ4R/Mt/+S9BRPjv//2/v9tNAQBcXFzgk5/8JH7zN3/z4Dz/4B/8A/zFv/gX8dJLL4GI8MlPfvIda98MM8wwwwwzzPDegfe6nPPpT38aP/mTP4k/+kf/KG7cuIGXX34Zf/7P//mvmv7MMMPXEsxKhhlm+BqFi4sL/PRP//SVlAw/9VM/hf/23/4b/tgf+2PvXMNmmGGGGWaYYYYZnhKuKuf84i/+Iv7Fv/gX+BN/4k/gZ3/2Z/HjP/7j+N3f/V38yT/5J/Hrv/7r72xjZ5jhDxl073YDZphhhq8e+OxnP4sPf/jDuHfvHl544YV3uzkzzDDDDDPMMMMMzwT+yl/5K/jkJz+J69ev52c/8iM/go9//OP45Cc/iT/zZ/7Mu9i6GWb42oLZkmGGGd5B+Bt/42/g+vXrePXVV/EDP/ADuH79Ol544QX8xE/8BEIIOd0rr7wCIsLP/MzP4B//43+MD33oQzg5OcH3fu/34lOf+lRR5vd93/fh+77v+5p1ffjDH87lqZLgp3/6p0FEB7k/aP4ZZphhhhlmmGGGffBeknO+4zu+o1AwAMBzzz2H7/me78Hv/M7vPNkAzDDDDE2YLRlmmOEdhhACvv/7vx/f9V3fhZ/5mZ/Br//6r+Nnf/Zn8dGPfhSf+MQnirS//Mu/jMePH+Nv/a2/hdVqhX/yT/4J/vSf/tP4X//rf+Gll146uM4XXngB/+yf/TN84hOfwA/+4A/ih37ohwAA3/7t3/5M+zbDDDPMMMMMM/zhhve6nPPlL38Zzz///JXzzTDDDNMwKxlmmOEdhtVqhb/8l/8y/s7f+TsAgB/90R/FH//jfxy/9Eu/NGK+n/nMZ/D7v//7+MAHPgAA+LN/9s/iu77ru/AP/+E/xM/93M8dXOfp6Sn+0l/6S/jEJz6Bb//2b8df+2t/7dl1aIYZZphhhhlmmEHgvSzn/Kf/9J/wW7/1W/ipn/qpJ8o/wwwztGF2l5hhhq8A/OiP/mjx+3u+53vwB3/wB6N0P/ADP5AZLwB853d+J77ru74L//bf/tt3vI0zzDDDDDPMMMMMTwLvRTnnjTfewF/9q38VH/nIR/CTP/mTX/H6Z5jhaxlmJcMMM7zDcHx8PAqieOfOHdy/f3+U9pu+6ZtGzz72sY/hlVdeeaeaN8MMM8wwwwwzzPDE8F6Uc87Pz/EX/sJfwOPHj/Fv/s2/GcVqmGGGGZ4OZneJGWZ4h8F7/0zLIyIw8+i5DbA0wwwzzDDDDDPM8JWA95qcs9ls8EM/9EP47d/+bfy7f/fv8G3f9m3PpNwZZphhgNmSYYYZvorg93//90fPfu/3fq+49eHOnTt48ODBKN3nPve54jcRPevmzTDDDDPMMMMMMzwxvNtyTowRP/zDP4x//+//PX71V38V3/u933vlMmaYYYb9MCsZZpjhqwj+9b/+13j11Vfz7//6X/8r/st/+S/4c3/uz+VnH/3oR/HpT38ab775Zn72P//n/8R//s//uSjr2rVrANBk1DPMMMMMM8wwwwxfaXi35Zwf+7Efw6/92q/hF37hF/KNFDPMMMOzh9ldYoYZvorgG7/xG/Hd3/3d+MQnPoH1eo2f//mfx3PPPVcEJPqRH/kR/NzP/Ry+//u/H3/zb/5NvPHGG/jn//yf44/8kT+CR48e5XQnJyf41m/9Vvzar/0aPvaxj+Hu3bv4tm/7tp1mgb/yK7+Cz33uc7i4uAAA/Mf/+B/x9//+3wcA/PW//tfxoQ996B3q+QwzzDDDDDPM8LUO76ac8/M///P4hV/4BfypP/WncO3aNfyrf/Wvivc/+IM/iNPT03em4zPM8IcMZiXDDDN8FcEP//APwzmHn//5n8cbb7yB7/zO78Q//af/FC+//HJO8/GPfxy//Mu/jL/7d/8ufvzHfxzf+q3fil/5lV/Br/7qr+I3f/M3i/J+8Rd/ET/2Yz+Gv/23/zY2mw3+3t/7ezuVDL/0S7+E//Af/kP+/Ru/8Rv4jd/4DQDAd3/3d89KhhlmmGGGGWaY4Ynh3ZRz/sf/+B8AgN/6rd/Cb/3Wb43ef/azn52VDDPM8IyAuBVZZYYZZviKwiuvvIKPfOQj+Ef/6B/hJ37iJ97t5swwwwwzzDDDDDM8M5jlnBlm+MMFc0yGGWaYYYYZZphhhhlmmGGGGWaY4ZnArGSYYYYZZphhhhlmmGGGGWaYYYYZngnMSoYZZphhhhlmmGGGGWaYYYYZZpjhmcAck2GGGWaYYYYZZphhhhlmmGGGGWZ4JjBbMswwwwwzzDDDDDPMMMMMM8wwwwzPBGYlwwwzzDDDDDPMMMMMM8wwwwwzzPBMYFYyzDDDDDPMMMMMM8wwwwwzzDDDDM8EukMT/tIfABq8geyLyPj8K6/h3pdew42jJRbegZkROOLx44dYPT7HB17+ACIcoiO4RYfnXnwepzevwR85MBw8ObiOEAlIESIIRADF9JsBMNUtGgPL35TqZFcRzyowRV0OUXq4q24i5DZzTL+J0iPtSuQ0FiSVRIznQ3+zeeDkPRHgCPAMEA/vKab3zEM7WfOw+WE6qGlZ8nM0fQFA4NReMIBoGhTBzKD0FCFGREmV+kZYeA+i1HMmpEYjDYQ2h7VTYWiLti1GMxY0fBaDFIefRAA56YOMux1XovTpzBwSAOdk3nQczUQUOGAGVNOT/TM/7FB7LYfT3IOQ1gfScHJVRTVFYEkfKlyxoOUwAwFAdMN6QxqmnWpI5nLuzRAUg9WM+kKCFjLueQz3LcQD6EDZyD1lTbzn6lPByUOdb52zei3W42DRdKp90Xx3BlcAmYuJJtdDQlcdox3NUjpALPhSle3egYg+2v6paEEZ36v3una5Wgy0p43F/D3t2Alu/H8++pTlzPCuwP/3c/KFx/j3DJbVbjCEnKX+uINGNYGHonYmMQmm1tkhMlcB1Fg/O9bfFE2s+dku0LYfvG5bbfxKwUS9XL3XcXf2/RXpbD3HLbyIE21q8T4Xx+lqudOC4zKRinKO0qajk2deZMic1Kw7h6HvRR1GVlXcIpGTSOSdgoEZmZBFLmUwKCZhkOHAUZJFBhPDgcAs0qwjMAGuS8JgdEPRtl1By48Ah/Jl0Y9qzEk7anGZ2uhCje9O0juM82lZ9RxFI6jkcazqtc3Sv2jmE+ZZlg0IRV+Acd0tvLHlFGDwgbVR4yRooGczXd2GQ+BpRJxaLmzB1Lsp0dvuVez40BOaDBT4YsvW+mw6ldfl/f/7Q/vLP1jJoJWMkDUwHrz5Br74+7+DF2+f4vT4BBeXF1ivLnFxfo7Ls3M8fu0LePF9L+PajZt48/7b2Jy9H7Q8gl8s4RYOz7/wHI5Pj4Gug3MevvNwImUTAL4CV7gqT7b5rgo1AuVNom0El2mn6iQu9qOJAGNQEhQTL4tYCRYx0sZbIGDAhYJRNRrSYiiZqZm/IpsqfxigyIWQQkqY8xMnxD1xAQZDY41GxESQzWiw48SMpPNaj21ShBGMuOzDQGy4JOo84FMNTXxpEMvJfIaIU+uZq9LD4EqjWgLQaxol4NocMoReCmwR2IhBucCioLEJ8zugUFrYT4KM/x4KWdO2QmgSvOV6LJWZuaGfFM2YaR4tRz9HiGpe7VnEdm60rKnpzWntOjNty0KOeZeb25jcXQKZgjN56q4UgveOdM8cqgbXpHiy/kM4+pT0Yes6kJgX7WqUu2+cnmocLR4+KQOa4Q89jORr5fnVi1qxW+c/BJeL5dnIwBPPR5XVhV0F95Vu1uunUc5kse84AXznIZMMw+902rV78UloSk1PqzJcY/Nps9l3rmLyqngGys2llV8xPC4Ozqycy1U6C1Hq0bHRMdDDLzbvyaQhMqKOPXQSAbE4dIkAg/Mhnu7aIhgEN8yDI3veVcpLXPZDv+Q6D+AJbAYty31VeWRk4TwulOR9cJILVebP+xAjOxbzatrVEnNbOECN57VCv/m9LndizTYfU2MsKrBrJ8v7zxCetrhnTaJqGqly9JO2g2Bow9TEPwUcrGSwp7C2EQ4MHyO6fo1X/vdn4YkQYwAhwsvC2G4ucHT3Jk5Dh+fedwf377+Oo9MTXL99F5/67d/H2Qsv49qt21gen8AtHK7fuI5rN07QdQv4ZVJEBLNrI9fYtGDYaHyl+M6kXGsmfxfSF/Moq0iJea0c8DBzYJiQk3z5FM/82Y2QHa5MgIUi5bGUhJlIoRQAauUJR0aoE9gNEAOsCgeOiJwUDMSqS47DLleKjoFBFOFB8ETNfgUWRQoPRCszDuEA2QKGGRRSwy2xYttQbkyRXcBSl2qKlbDahal1ExstvlTGjKZmMOOGeWdpRaw+rbKhgIawEM0z1XyyYSpaLjtbwVBP0dApMEKipQu1dQYwFpabzMuij5ZdCzeEUnAw5WUGNkEgR69s31vAw7qybcrtKBo25GHXeK/lTY1pSxCxbVchA2YsLTJQG8eazTAnRxPNzOWP5qZuoz3uqNu0Dw4h1DR8Fsw1tse4VeSI/7YGx4z/aBp2tbPR13ftpHSGZwc0nsenndZ9y6J4X8tbFqYsNSfWRJ2nJjVcnU6x0qldtKqGQ9e8LabisTvLse2Tv3oYnnR+viLLVWWtA5IW8pd9aIo6pLrRgyn+UCfWebFyiZmrzA+oLJbNTjA/c2XxDlUeJHnDoeKNKqvIs1jRZuWFhSwlZY0UC5rHHI6p/JesOBlRBMbUNpZ/WaaNQERwALw1L+bBwjhQ2kz1xuSQtD3S0NwUHUtjEdGUP+1CNXKnrs/aktaOga2rZncja10k64xYpbNkoZa17FoEzNwdguAMwFcyo5XZbSOqNo3aIeAwyKFTh01NND9gMT3N/Yv7aNQB5K7RoOkyW+Xt62KW6xiDVY19Xy/gK8KVLBnGLQMABvdboN/Acw8XCQ4xbRQ5AjGCNj1OXY9vev8LuHnrJt46PcL55QWuL4FHp0tsLx8i9is8DBFvP3iI2889h2s3bmBxdISbd+7gg9/4QVxSuXF6ZrBvBnZK69U7S9D0sRKHqXp4SLcLWbhK3wlhdrL5cGyIEIRAKPGmYQGTA7a6mMWTIca0cW8BAeMTZQzEOqJMQEJtCkIkBDMwskVDUjRISuUGhGRmRgCBQPBYdMMJr/ahRzLfU1eAMGTHsLFNnWbRoBDImBUNHSmtMHaPeybeOpZkiKvZbKk2PTOYiqjncYXRzkt5wRSFoUhA0uYyqzYWxLeq2/ZjkhfI+DlhNoqzLeK+C6iqb2/6Oq/Nv0tgblRQmAaaAR+Z39s5n7RJG9KPBLGJ9dwU2PZBXe6OdYhKYLNJr7SxlXpa3dBxzyabjJ2KkVGTd9HLZwn7xuqAZ7BFVLS76EZdj0lg1zPwdALJDO8+WOX+PuHtEKhx6Url7EXaHZXZx43NbdG3WpHbKKay6H76sXkKOlFn2SmQv5twwEZpIulTyblTCu76K9XEq2pPzZoUrKLAGRlTea+VGfNvk8eWPaUwVyGW63bzOG0hKxm+PXJ3knwMSm66zIiREBDBkaVdQ08JDEeMwAQfGUTprR74MSUZOhKS666MTTCDZpUhbHb86bCEhsFoTbjteyW71MuHtX/mnXV91TrzYYV8Rid7B5O3NWf58Fb2EjX/g6RpwV7cVnmkQRMsrghKjNCETMJDyco7xaYJ43OXVppDYIpOtKwl99HjQ+ps0g06vL1TcLiSYZJCEhZLj84Dg6owOSQlb6eI7XaD3/29/4N7976M05MTHC2W+IZv+Ajc5hLf8uEP4PHlBg/OL/DF197AW1/4Alb3X0cEcPvucwjvez++9WNfj3UgBKvNbXAWuzgO6hLt2GQIxJDSNE0KK60lgOKUmzBm4M02YEzEcx0wfTIbA+IhZoM35bT89V31oOOkcY2c+tBXJ8OWuOjC1bRFQkNE9UXhPyWb+0SuATgCh0RsdfMPsWogAIgE7xghJHJC5OBEU+JMqcQ0jJEoSliJ39AScNkyabIZ7Qb+ZHVuYyNrwgdkQps1qIai1OZqmlnnB1QaD6gSRucVmk6zksGBRpstz90FTUGzzmxigmiaXWVnQ4Da9KY9xE3YlaZgovW7Cely6nl+f6A2tljze8qcBHMcUFhF1OUDYwJ2SDtp58/2O+3LlNk1ld9bFhKj31pmtW6yqxC3MjXqPrDPI1yYSstXx8GC/o2LKwUsLqfpaRnyDO8ueJlAFdor2R6yn3jmMEUKdqUd0XNq0JQd5SjU8ga3ylEriSegtzW0rFCvAnV1zaorxlEojN9FoIp21RvBq5TzTkFB/4FEQyukc4IjngcrBC+CjzXr181sXj+WN3A5lyrDZZmobpd+aaxL/R5t3VVea8mZ3EkJjICobpoMEGcbB2HFlJQNml+tH2kYG2f+1LrWurmWygJFRENosi8MlWls3wyjLcYMFT5NfLdl6GEtybPJ9UgDPbCF1n2y87J/YTaAh/nR2HFFEfJbRam67rrPV+L3B28WG892tGMqyyHF1sW3LKefdPM/2U6t40nl3B1wZXcJoGqkA7b9BiFu0VEAqE9bShkER0Bkxvn5Y1w8fgQGwznCa298Cc55LJcnWB6f4ubdu8BmhXBxhvvnZ4gccK0D3J1TLBFA7PYiUB6seiN2SFSQqTJ3Cb2VMN5kwHt+KxT7ioqgKw2weVWjpzK9x0Dw63ryppcHJPMxWTQEyZvrEaKTFzMPzCC950xInVCnCCDGRAKKWBq53GSOllKngDoRDLDGbuChnqipIoKL2AZKwRyJAU6eckGoETMVGl7rp5a01FFqH0gzSVudM8c7ZL+aQJRZMKEyRoCmJSBUSgkllk7HjirBggfmk/9oKELf6/CbbE2wqGKZgQ2epOkKCwU7xzDjlica0yfYjTaooqQODDTV/n10bO+6obLMgwj6VYnnUxDbFjHf2wYzdnlejQBQW0TVwmDN36fGnOoHjTQtmESHquKiLRE5UKvSsMI6oq7QuvbsqW7EfOtGTpVh2shAqQieErgm2mB/HyqvzPBVDkrHzZGUpaMFzZ0wgW/RVvNRpq1pLQ2K5RqcKdMqOxgDr6mVh/sUd1ZBzlcMtv0kQIQh0PXBmYoPADYwNktsJx7GWoVBAphSUHE9yb5St6YEet2cNaAmbdqmHeToSvAsFAy7YtjYTYcejmS5lMx7g4sq66tlQ8EHzJhbnLJyKZNYpVKyVIUE484ughVdLlw6tQEMqAdueieyH1Eh39jAkAwH5pA29ixuE9pVSva0FFgsFBwIKQikmpUnw1zKAccpyiaZB1kotycyIieZ1EVVXUgrspCYDuBqRYP2g8yJZ9M9QPo4GWxVZWSWAJYmnQ30bg/t0lgM+YDyLIGQxn1EZ7hKMxTfPOBwUu+IXBn5OE911b+Mg9WzgqzuE0onoFVffl428aCyJutopW3QDYvumc/oXNWCoPmpr2rcqK2WnzUcrmSQTxvzgJFiMhx3hJOFg18QPFw6DXBpQYKBfkPYMGPLPRAjYmR8+Y3XpRQPog5+cYQQAQ49PCWXi+3qMfrNOTqzHA9p6EghMiUBHjCquwj6zlODmimaQZsqMm86q8Wti1W7oEH9EJOCwLqRqIVDzm/+AGTJRNOpW4XXAngIrGjnPHKJrMOJPCEyEMhlyxWSYI/REjTtmyMRCNItJKm/KaGTTjGnkoiQ4j4AqYEcEZnA5MCmZ3mhaQwHoe6WOLAoL9g5EBFijNltggzHimDEqIxG3ruUZmSmZChMQUQ5WcBoEx2V1iQcMQhZPKSLkFslasZZa46krKzZlXS1FYQlUlpMNMigX2vrl+yeUjPJBtRKjCdZapV8fRAYuXgSnopo0nS/p/poIb9vEH37qvm8Jvg1PduVd6IdzWZcYYCuwkSLU68Gw8v920VbTREw37Nr2A4f5SIQriKmbZctsFEI1e/ayYYypZ/vFJOe4V0CrvCZkG9jAjBY0LXyGiFahecpUBNmRaUirhSbtRKH7y3XPlUyjKwn9+Gm0nnT35EP/A56+ERQy0dXzc4MhAgSl9ztJuDycoWziwuAHE5Oj3B8cgzqAHe0RCCCY4eO3MHKc20gNdpqN1wjmtyw6ttpBfKVgJq+HZC8dpMt1kILWA67JI2DnPCbJHqjRZbeeJAv9SwJnJQNWb6BeYeSrcQsNCNvMgtLCpi1t+uwURafbu7T/ErwRwe4kCTYyIBTOVOEN2ZCij02yJPaT3mA5LqbJXdxqeVcdQQnv2WY+kfjywC5ER62piTjZ2vzKZn08LWOxVCXzYxCidBU1nGVaQKs7ERVGZmV1mvF4KFaDtvqrOy7c6O4Q9DcJRtlulgVVT/bt64sLbVl27xFE0fChklTj90+WYqr8W7IlLvgEJl3Cg5WMujc2XVKSHu/G8slnr9+HZ1/HtdPFrh2fITFYomT5TEWyyUePT7D62+8hXtvv43H5+fYxpAaHBkhMgL3WK8CoprHI/n2d53DcQc44tGEPi1MuUCM/Le/wtBcozQsLFnv2Q8uCPLo9Tk6TqpRdmTmTK+dkIIUWZUpxFgqF3KbeCBIGucBSIUTZCwjYRuTAZq20AmzAAyD4KRoiIHTp27oNcqvtIlDTIvCI2lwY4q+kJhGunpCGQDgcpTgxFCcjNmgEWbxqUvFixGcqJ9ZmEaKPCzpOQrBc0JgaUQQphCSzRfd8NSbTkP/s1IhL2TdtNQEqCLuZF4WSmku3wGDkOhMZj1JKKItY2jbPsJiZZBD0ts22fV1qAJxB494R4CqT9uGPbLWkIbKZ/vyjtrwlB1t9aHF0K5axkFpqWTGNuDspCWDNszQm0JpptmMoq7YRLH5TdXvuprWw0ZdO2GPUDXDewvy2iDhdQZPWT6z4K3C945ygN17m8K1DgPKNpdHtYit8FZsvPbxpYm2KuvUxLsCsU2VcQg8tWjFjM3lFq997jXEy0scdR7ggMvLC7z+xpu4c/cubt2+gUfk8Nqbb+D5l1/E8ekpTk+v4drJMTrfAeTSIcMhGs8pEMKmQrx2TA9vMm+u6U9jkzEq+glpyrOUW6lqx64NFSPJiOyMywAn19xaTsmbHpNXnzHE/RfDAViWVTVeVByG2+pzVBQEIG65lNPZDbWVUxicZDu5J5zIiQXs0DNSuZA5WcTkzYjUOvJ5To2InOxik8Vt8k+OQg3SQVz67hxAE9ITmW8E2omlDOTrQEfvLFFRq9uqsFoeLUCDTVjeXSeJyTpCFUcAmldcF9YshnC1rGtG1rlT/JnGTaZWOoxxp/V+CurypuajVUaTT5hFsE9+nspj5ZspcYRQjUdDLt3Vzl3P98GVLRm8CH2MhExHC8KLt2/hxW/4CNzqLlYXZ+i3Gzx++BAP1vew2fY4v1hjvQ04PrmGF198P5gYl6tLXJ6fYbVa4fHFBS63vWgIzdSHAPQboGNwABYA+j07EktQAAzB0g42hXhK2IXVE0mnhH5GWljKpOpor7pxVD80a8UADL9toJ6B6qaPgDRe3hB9Z+rmyIkAsyghoIoEiBlaKp3A6RQeEMuEkBtKoESAABBzDlIZC3uvITaDMsoAADGk0l0EkcR4YAndQwwqLhInZBtXjuAYEQwyEADuE1NxbsjnjFUFcvmD71tmjBMTOBWvQ4Uz7a8VKCX+ZZkHg6WB56FXkLggteRKVLon1BY83uBd8Y5QXKvpTDnWTUQFTSt4g0qhuUbtQl6rCGfGv6orLbxvQb2ED/GC2re5b609qp4fWoam35emBYfsVWumWW9EppismmPHPTRwF1OOE++nflckeMzUJzrL5n0+Mak6lulfbRY+1YEdwPqP1JPX0aHl1BPyDIX8Gd4FMHNvaRnDCLzK12TR7pvyKZndvqtRLq+VCfpeQx0QzYKWnZUPVWPquiByQ3RV24UftOjhoWhv011FJKs3BuvLNd74wqs4e/MNXFsS1pdnWF1e4Pz8HF/YrvGBl96Pa9eu4eToBMvFAovLDd76/Jfw2YsVbj/3Ik5v3cDyuMO1m6dYdB3Iu/TXeXCUwxEa81TbeMvP6oEYWaPh8E3Erv4rruw8sL0CDSx4ckuOwbDpR9Wneu57JBnRc4rPEJFwTvOPFAzW2scimtRjffPVjVMPbbIewMgjOTkD7DjjajQDkiwdGEFUENnt2LkcxJw5mgOlFKRcLYfJFuQIpPdus6YGyFVIYfrhQMa1Rxx8IolM6jKzy7I4yUWaEz5PU/yXMZ6fDKKgZx72EJa06d7A1XlQNqOumxlwaikt66d125+vG2XoqF03OqxWNs3tMG2w1mS71hjFQYbN+atBMqJvdjuZSpNllAZ4GJrsBhmsLGAM9f6tCTvy71z6VPavzkfOjF/lJsU8jN1V4WAlQyGgxqFRfR/xqU/9b5y98mnw+SMQR8QYsdluEPvUsj5GrELAtmcwvoDlssOtm6d4/u4dfN3LH8Ab997E7/7BZ8EeIJ8ou2cGAmPhuwKxik1MBVlgNO8VCYE2EZ1i3E+iSdZNZf1wZGZvkrWIdg2RB2sDu0nTRRkwLAa9aUEJOUEUCEaQttF3o8wlG+mBYQLoxOSnFjgiwomWM+Y2QCIvRrDaGWQLgiikirIiAqK/jfDkJT8jUkiEOSJpehkgIoQgjCYF9kCK9uuQFAweQACxFwFMR2RoGwFiAZHeBH0XHDqf3HnIOBE68cHTlU6gNjO34RyMEKpQL2DFy0jCTONYwWDTZk2w5VFqrlYz4zgQdIv72QrC4EurrWnuE1EMIlTZa6AsTraE5REjq4k4j5o26q99se8kZsREW4IRj9MDE2u6sS6nGPfUb3021cd9UAuOk0zSfLZMHKfaWX/WcnErTw21x86+Oit3z3G5uwbK0u+JwZgsr5LrMn4pHqqAW6Wzxej7ffMwgidFgBm+aiC7isnves2o0Ez1otpVpilDUWSEd7FEYZKMhPKvBdF8tnB21NSWjFK90nZapYKe4jbXReshl+PYojuTw1cPvBtM4QGAOKC/eIy3X/88HvYrdAjgGLEAw2+3+KPf+PW4e+M2Tq9dx29/6lN4DODo2nXg4SMwR6wvHuL3XvsSXvjAB3B0cg3Odbj7/F289PXPY02UNrJTbbI/GwSktgZt8d363T44GN2ugJf1hIzap3y8elFvVOxUBR4+k4QGLKUcV+GZ7sXVglZvaCBCPrCybrqxSp/kzLI91qVCr6XUv8ErVgONyxWVROIGkfI45xFjEGsIOYhDTKf0dhAisryolghEVFjh6fw66MWY6bOH3m6RduXE6aCLiMSyAXDODzJpMQFmQizPyYIEFePcktn2saqpd0oTtDp9ZulCxhlGYTFejB2Q3c1Gm29TucWZlvyp5eS1WuOq1i37o+xaXskBoNJN3Jnvo2JrYlzLkIYGENJ26SrL0lbTopmHiBlN2tR6b+ZGn1HathUx7gA8cVDIw90lbOGVSd02BDy+XGMZUgAT1y1xcnwNjjw8eay3AeHxY2yxAfcR2w3jrXsP8eCt+9h+3Qof/dg34nc+/Xs47y/hl8cg16Ejwltv3cfjx3eHCKgHrI6rKBIyXFmavAKogLuDsT9NuYoEdnhAyKd8lig0mpAJc+CUkCODhWIEsWBICgM3xIIgN6yiCKCPYIpSnmzqoyoZ1IzNSRtcIrhOfMuYQcRgSpYEUVXGkQCOYEcgsU4gsFAdlwQOL2kzWZB+knXp8EjaYulnDICL4F4SsgM4gqQ9Tgl0TRgLDQyM32tjFu18G0bNLK3d4zemjNYqlHSsLfoX96IbJqr15WUqbciEdqJqz3KC00hQ1Fs9rwMPoarP4l+91EYMUAWeQ9bHRIJGc8q21ifg8llPyyHE3Kbd06xJmBrXq+aZSvM0bdLvuzYvU/lb9OZK9VK7jklyPUX7ZSJrH9Cp/FP9neFrH7wh8zVvLejaFRCkJSTX64Oq58ojrAFb7VohrCjTfsvrm9CSWFttlUZkPiSQN36tcltl7qC1+6BQ4tTtZiCELbbrc3jewnMP4i0o9nAcQXGLV1/5XVz/pm/G9SXhj37LR7BcHKFnwr0HD/Hlt97GK5/+DO69dQ/rB/fA1CEy4Vu//dvwDR+8gzW6Ud/bjWw82zUWzR3SjrKqVxaPrtKknYn3FMYshyPVK+sOQjzght3UMdLzrdShSqKMx9GkNe4M+l1E0tynGNTjl1P8tggTIDHZ2EJlPQCR0waeZTeb1t8wQQy5NULda5XfxIgUUy4JXKzuEtEerik44SsphpjGE9c4K46My4bUzpQ2XinAZTLLYGbEEOC8zzfauRgG+RXA4FJBZu6MSzAjybGcUyVZfpjOAd24fLBL1rEomvdiFqQcSy/Bxk1oIn2mdTBlmkZknONxERYK/Kw7YX6TuvM0yqhpfEtQzZhmiHe2ZqvS6/zb8g9lGWTGcIqU5C/ceA6M9xg1LTaDP5L1qCpa+vikzgBXjsmQW5JfENB1WJ5cw8miQ+SAEHpstlv0YY3QR2w2AZs+nXNHiYLkwSAC+hCwurjA+19+EZ999UsIoUcIEVtmxLDBw3sPk+m4IseOnh4ieI/gWUqTLcl6V9oddU8WJcSwDkZikVi/Z+JszGCygKTaYAy4xxKwhlXZkF0IkoY3EhV1ICZ/NZBeXcl5cQzuAEmB4CIPgRady5vd9MglywkXQHCQALxgjoiB09VIUmdysRiIvXeDc7YzquPUf0pBHsFgx6CYvCVDBCLFpO6IallBUndSOIxWmHwfxsbBeSosBZTIqiBqJ8dO9SGb15aw2ArEqJrevHk2AqI177U8xdad40Vo/lj1hcr6irzASEtdWxFoWRVPy3XXJ4dZyVKVUfcbWrd5qXNRRGY3zKuOUJ3LqNaSbT9XA3Ywo6g+a2DzsjXXrT7nW0t2lN/8XTGTK9FHk4erB1OKoEKTzwMuHmwdZvrY8hvdVczUOyEz034fE+U0ZJ/m3OTyZ3hPg6sEMXtjgbC6IVCjKH2Dp6bg2qK3mV7Wa8cNeTyyNXZ+lvmKJM9ozBVfUFpf0Yniu8lTWHarPNDgM0VTqxeT5vlc8o+dS65eVHUdJl1SPjqcHC3gOMBxj44jIkdE7gEE/O7v/G/83u/8HxwvjnDnzm18wzd8FNR1cH6BW6fX8MEX7+CNL76Ctz7/ECESTk9OcW319TjOcZqQlZxXopc0kb41mI052lFszrKrTYfIFs2C94CyTgeDO1W7Mv6YNlr50slBjzO82AH5AAgiM2pdNqgjg/NVk5GBGEXm5BzJKysZsiUtD7HcssWDHp+7JO865+BlUUYWV1qVQX06gAKiBBPX8iHB0LRWAkntgIeTjpEsXNIF6ViEk5TWHgIN/rWCe5EQnUMM/SA3Kz6K72y+FUMmkpFoknPSfkp2E2N+RRpDspDhyjRjBJDhToFs7StFOuv+ou3VcSeUSGHlLjKyls1bNaoSqYdn1aKoZW3bqDzulexl8RawXwxdlT1TYaVpzltzVpUzNH9NRw4QIgvrJ0WUWLXP0noe+lc1vyn3WhjJ1fLpGoU9iex4sJJhqpEp+CDjYrVC3K4BDvA+2Voslwvw0oN8j3BxiX7bw8lyZHH+7mPEvXv3cOfWTXzp9dexYs6bUCejSEz5jlra36RnAldlLlPmOnsrmYIpBqTIRyius0KFSJYpkM2nXx0GHyUkUsmiLFC3gnyNDSlxVyWCiF7Mpn1pxFgYgvVrS0EU060TDi4Tt8gxmyhB43F48U2L6gfHohBJK5mVKoEl4KSYvDkgwoGYQSQ3m0CJilxh5QBmCbjDAIeIQIATawYmHiLoKkOxpI3i0L8IOHhYtXESqCiPs2XIVtLSMZ2a/vp55lF2mquFYM0EWwVMugUZNwzFkxaRtoRUP2P1vFV1/bwWSmrhqagURokCDFfK0fCs4DbST08wA1WWOWqfSTNa8zwu5ioC4b5n3HjXQJchveJMg8ZclV49DVhG1zzRrefVCE/5YVVgZqq1a9wzgDxmVrI7MB+bL4eu1xneu7CMejHkwN9S5CH1oI7gyFh4hxg79N5jA0qnkg0o1ooUC4zdiTStKhc8DVdLt8C6YFg3AlvpPrzUNZZjNdAEf8EgjF8JpIIrrY8pPoVhvJKHIyP2G3S0RUc9OmKQ+OH3EfDOIUbGanWOV790hjffvofAQB8ilicncH6JzWYFAuBBOKKA7eO30SGmjcNV231g16Z4Sc13xg8nH+2s41lASx7Jcoyp0+wdi4ZEmw+DLEOQvbrKspzEPXW5YGAIQsgwSgZ7zeNwEFZf6ZplVU5xFdJPArE35cdkLRBFWcFJZeDE8hbRSUy4KG4ag/ut0gcO0g4HcABADh4RwRFcjCBy0JYWErPucYhEth7cJ+A0MDmBJGZDGlYCKKR4aEXwOZkTRHAmYbWjBUkcNYNgsuav4h7ePFzg0rVBSk98U2VqlvmsNsOZ10tbPIwiKhdUVJWRiYGsNyreY/daqD1OlI5mCx0eu6TqYa1tTsZt85C0TfYBYVAS7GpYJSPpXknf7XIfnZSZjGzfePWOyzWHWzJU+Jwbx2mhHB0f4ahzoNgDlBZLjAF9n/5AaeGGqIs9LfjIjPOLM/jO4fnn7uCLb7yJENV8XRY8GfMiUz8wHoirElj3rEYS0wgwTnhAEsPkppJnROYhvZ2XnJnLRRWr/ErIB3qeFA7AkC/md/LcFioWBFmDLFfxVFt0UIiIEryRwRKUUdkNAc6JiVXiNkROgk5GcIhCVBN3Y4pwASDR1jJHEBJBd6qqJAAiHiqJds4nRYo6SsndxRwZ0Tk4ThpijfGQTss1N8S6IzGkXrTGyaEvDahNn/0MdSxrFbAU2hI22xMuI9WQVmomQSjxkUZfBrAnU2oMokJwnvdo6jd/ddsLYq+41+ic4qgqXOyUtQQxPfGA9GskBHLV54qJHAp1W2vCPSUkjh5y8TFKM8UQdL3UdRaBQyfqP5SUTdITfdlqtDC4qfmZZOamM63YNJq5pnVT5dZMelRca7Ia6+NQ2EeDp6qe4b0JywdfSsrnfgOOPbwjLByhc4DzhG7ZgRZLIC7R8xH6xXU8gnj37RMczc8WrjjzZ3l5TsvDGo0wPuqSxpvvClOucZZM1ut5nwzcWpO7aMpVZbJ94AAsvcONpUd/vMAxA8edw9HCw3cel5drPHp8gfW6x3a7RYgBfQgynozV5QUir6ChqokcvItYyuGYmr3bOBk7O/GkC7/mYYeU/awHc0/dtnpXIQpj4MVAKW8o7tr5t1aD+lufBUH6GOVP5VGl82zdDpQnVjEZzALM10rKM93EJ+lWLBNkzXKISabLIq0JQC/O+eSd3DAmsmKUq9b18ItJDGwZ0UW5zSwiOkrxxpyRCc2oJn7m4BylOBCmvWCGXq9ObNSKlJQFpJbFarHAas1QCkFU/ONEhhwEzwLFpoSS/Y9G+x+1lmhmNjKQS0OQN9QeorBo8PJajlUZ1SYrej8hvLXyqIyl3zOtExxrdWVEH2jI36T7gvPMQK0jUjllZL1pftfxXcjkOwT0QHHfQeezhIOVDOpzVYM2crPegvo1PGLya3cJkb0Hus5hs91CKYzTyCaczJ42my02Zyvcfe45vPXgES76HiBKUfljRMcRC7HT6SnjaHPSLRHaB89qgKfa0oID5ZCSENtMZrFxlXhktigMIEY0CUAxVrqoNE4BeLBcAFBqP4ddXoozkMPeJMWFS5YB6noxNFG+ix1ckPe6UJKCwaU2ONngR4DVSS2k8JIaO0K5nAOJQQFrjEioqwQTmxsYEjF2TozIIgOOB0YWQ1J8xYGIgwBycqsFpYGL0OstBzeRFMzAMABQFviYgigjBtytZ9wSDS1yNJ1KuGrfKvuzlgLtbo3MI0PEbd0EFFHTNQ4HSSAYa+ar91uXDRjwMBPbMLyr+2NPOTJd1jKGKd6/nieYyWSaoiH7H1P1TIe5WaTJuJPGNOqteVOug8u6mxkPhFG/qnZkFLL4Ig0rTO929ZOq5ybfrrks+q5yEJfjUqP4Lsg4bcoCrsCUzZeR3+XE9xneu/D2//n/JVdOjnCdx7WbN7G8cQvd0qPzSxwtT7G4foqLC0KMDp1z8EiCFDdwRMHy6kx/MdBKlSGsgsHiPmMQDMHDbdSw5bLc/mXq3EXy9J2rEirNbUGrvEPX4tOArmOXWDauLTp83Usv4UW/xTFvsEAAIaKPEQ8Xl+h7wtGSQM4hcsBmu8W232C1XuNivUbUAwtywlIj9MIprcf296rr25LJTD8bPP0rBTX/ss+aiWnAT2c0VyRyGriNI/poZKmjcrtuclC7CwzPdbOv8kPx23wHVPFAIpdJGSZGQZY8afht5Y4Uj0wOxVilWKelDs0niAyYBCFH6ZaIqOYVhkFHiVaZeGVMVhRRV7gdJf2uyoJUr7QelBUjkCs25YCL0h4qWXingdUAkSwdJ8vgCFlZAY4puCQMbzRjWQLnMTJF7cRhkZTNmI1xzv4586n7Ff3eulWq5T7Z+q7pW8SpwL3Jfgxt5B3poG2iIY/WPWpPK06CXTATA2tlkIKGmEW9k64Y9w3Lh3ZBiRvTyupD4HAlwxQDJaDzHovFAkceQNiCEbFcLhD7gE3cJsTWljPDeQffecRNFCZLODs7w93nX8Dd27ewfuttMABPDHAAxS1oS/DLY0Q/TSgND277SE4N7BU4Z7MMfgKhdcc7pY26qDKyNxhfgTRk8stg5LIIUMMBjmXaqPWor1oUSwa1h3eU3BAgZE6IKJsLi4c2C1GPDqQBbXggxCQJlSDqHzHnWAlKoEk4GqEDEQMugmIyI2Mpm0NM9FuVHgRpa+o0AzlIYwrOEwctpRHw1JojQm9XRvLPy5SQxN1isNBwGaktCIEXhHBAtgUkApjklg0y8SvyFJrbLKyKXrtHaKAlZek0K6QK6WbATW2pq34Pg4CBYMlLtWTIOEJF0tGd7HbtMZLupXOGUFfpScuWevMVVUCxMRwR1up7zo/xGNE4+ajN9vvIKqTFJW2GignXwpXic9Ye6xq0yUwjmz50A0qN2sPV+12QzU9t+oquKO2xXchCp004Rv0yv22vxd0GDuTvkigpK4dnBAzBbLUtbNZxo+9Ekkf9Y9kIAgeOV1GeLskJB/wntZaY4asHlt0Gpyc3cXrjDk5u3MDR6Q2cXD9FJKAnjw0cLtDhcSTAL7N1WhFbxkqbMM8tNNaA4mWmrYYO5K+6kbHFNOi8rtWaPo+g0c5dNFTrqWXBqbgME0tzqGz/o6J+vQ6x327x6OFDuNUG/focYXOJzXqN1WaDx+crnK02YCzQ+QV8R3CecP30Bu7cvoXPffGLWK1WIL8EaIFAhHW/xXqzAiLD+XG7J/sx0TnlXwqFq+M+2DVn1VzXMBUnR7NmHGvJAI20DAybI6rGocYdswYaSyDnd1RZ3FRyDtvKq/amPlaDwFpnkPxiXUq65RWmGkV2YyCohQAgSgnxHWaAaLxJd7LIeRAyjSVGlOCQgJiuDpYQzOlQbSdzSAUV+MEkigUdJ3Vh1gDpGoHCJYlVlGWcXREkL9FIbkyHZi7J+CzxGXSYRsINCsTdhcOpLrVepgEHjExJQI5nQxh4ubrt1m4MdohahIntJ+1uXyufFm3zRRxWhs3bog87SW8lI9lT6kp8HxGi0T68GpdmvVbBsaNhVrTz1fPmmj4ADo/JYIUo2yECnKgJQ98jx3ANSTO86DyiBP1zTAicTM0dfNJa+Q5gxvnFCqvLS9y+eR33HzzEtg9YuIiFC1ifvY2HDxi3vv4jk76PMAMwbABHzT1okA5FsCsV+gyKsEhZK8UK4d9gAyHTzqRA0MyaNiBzQBsoECDZ2NNACZAKI5esHIBEdLNW0A3xbyEbcooRCEHMy3QpMziKnxpBCBOXGyCGxFaQxmYTMd0xUCpXzNkYkkc287pRY7O7IaSYC6x5DIViHkhFtkCIKWBkjlAZBgUEgxD1PspKb5+IJw9jgVS+KhgAzleHDuuKEiMzU6NfhutFhRuo45hSbKRn5IbHVRE50q8OXW6nFGvkiIKYZIEXZbmQZy0lgyXWSckyvLNSLJsydP50uor6jUxRt2vUl0Y7R4Mx8TjjkOJgK62RWYrOGqYxGf/CtlVklZpy1yaCec0bJksVQ9rb71FDqvQ0erUz7yFpbFm23zsZlelbgcNm/nUJMKqxarVLy7P5Mf5+MOi8T5mtPQM+MMO7Cx//ju/AervA4uQO7rxwM/OdiIiH64jLLaM79jg/32Cz7nHz+hHQV1PfWJCHoEZhwsuDyfihUNdBKE3cC+G1ojP5o1qrrYLzpmRX5U8CXA4dl6+GTQkD69Uan//cF4BHb6ELG3hSywTCugcuVz0uN1uACc4xvAdu3TzBN3zk63Hz2gkePnyI2EeAYuLLLuLt+w9S3Qf2hRrzbNsK81rbHRvprwqWh4/eTeSp2zTayEzVtePF6J3l80BTGZvzGNy2+4osbyitVbnFI8uHif4nDCErIMThxrK02VZC7ZLTrMiZIaRDrwgTFNHychvDAQk3AgAnMutg4Zrk0SQ6Spyv6LJ7bJom2cQb+U3lPTtaOdaXwXojKuVPa82U5PqUPt2kkU5nSv0IiRWDtEnkx+iiNJnyc7LuvCrH2HHZCywXc1DuQCGfVOWwKhV0X5K86pPcqPsR05emAo0GOTLjFA17HovndsRr1Kzdz0SkHjJfhRAfACMaXP/eU1+RtTUuh+SbeK9/VsyxzWnpAXbB1a+wrIViAN4RvO/QoQM4gCOwDVsgJt+kFJMBGXGJGSGk2A3OO4A9oiM8fPwY73vxRZwul3iwfowQe4BWePz2K3j+9EMAgC0MAWpAbl61WLh6VmRo9Kn9AwXC7byKcAKGrSwyoSh82Shdo6UR8qmxSuxJJwFGeynP6oWp3imSL9NSPzA/XXn5TteIvCkeaE3MP/R6n4zh5FLATwyPo1IFAtCnKzGTBYJUYPpS79nkAiGQB8hMnCMnJ5MEuGSymvoZc34nlHjMi4Wg56ZzptosQXJUU1ygCsMwHXlKkGs6Q/K5Uy24SzU7uwuKBLnYAt4N5nB508Ki0CEyYz2WMJVZWfzJTFiLtHhcCW061xpfhWjCz4xSW50RGiIwMgf2kCvfpA3BNJmBHIW44FXDkBRRdy15MXqvkRKjtsrIy5fKZy3YJzzqGqGawpqKXK5wXJExYBnlzQzLDfRIq7HjAvt9xzMrfNR9qJs9amOjiVP58waEJt7vzFw+brW3TmjTjnDTjO+kImdPe57G4mw0TzN8zcHRzVvYXgQsjj2oS5uKyABTh8vtBvAey6MOp7ccHnzpEjeAvEbU8A8Y1pkKroeeZFv6mfjRIEAfhHqNdPo7yxTVu1a7irXKJe7XlgxtC7v9kDcxGPhTUa5pV5MuMcF3CzgHhL5P8gUDqy2jZ4fgAhAlGPU2YHW5xsX5Be7cuol7b72N83VApJAsGdYRZ+fnQBIvknvwlJx5AA3dS5+eYtNCRUFTFUw/runwVFFT4nLNszWN9tkNYlXZBh5kkJpXK35FGg4d8uGXlKVyyGDqTjkvIx1kMmAs3ii3iUFJlox6iJSEXRa5jUUYybchSZmWeUeV2eQwLbnFihmpZyAECYyiVgfIbgoZl7Q/BQIUtVVPtS/jPhebGhXaGplSTDIyC0pcPRyS3IkUa8JJZMPBiqMh5OyAHL8tssRZoNxfW1JuHhsZlDAchgJ6i2gafsm8i7fbsoU0jMbSQismQ6yeaz2TsXYOBLuOdq417SOZZ8WXoRygrQQQLMhQy1BFDEKeliGL4JsTMvihstDBSoZRkIqhnRJXYQ3eXAIUUiN98hPy3QIdO8TNGtuQNoKJgDM6cug6j+023Wfw4NEjfOCl9+Hm6SnOL87lepkNXDzDBz9wB19al0J5HX1UJ6oe/DxOBvGy+awVnqe42g5J/olNZKVRiniFBRiPq7faI21r8QlMn7KZvGqCyZQ2hU60iWox5iJAQcYtDqbsuQCGWCjoeIv7hEMy2fJDfITETChdEdR5UCSE0IO3GsQRJdZHGuZJ/hTx0hgk0zAigFwHEIOcWEmYK4aCTCLl4dCoERisEpDaOFhX6GCWA8g2Xx5PE9CSKDMVjSIsw9LApRQwyJnde2q1rHZnYhw08apc3Uw0zAMwXJ1DhilxSShIHX9tqXYNYGh3TbBaeJn5HBWuX8N7O7RVbyKb6TdrT7xTcjzNLHRU7bXuQ7pGWsKNbc9OwmjWT72um9PS8rFrpONGeVnAbozLuKI2XJX0FNjTYJ5TTHDHkD5R/U+cdte8PkPYZajwlPLGDF/lcLE6A9hhfRlB3CPZrhHgO/CKsVx68FnEce/x/PWTzEtjJd8D5fpSRcEhyKo01QrMNjvJj12yHlXp83cR7Fu0ijF+hsY7oKRnLfq2C3TT4SVfgwXkdPk3DX1Qq7w+RIS+h+s3IAK8X4Jchy4wfB9AnAL6xZhnEQ/vP8ALL9zB6bUTXG4ep4MKhlx/PbTH7eAlU64hNdSHPZO0/orwJGXsEiumaH4zYfViypW8hQ8Zbw0+5zVj+JFaE6pMCsJgXl+3Y4Q00i49WaPqBjSXL5kEB843nJWdHhzYi+5qZEK1nCWAiLLbrDoBs3NyM0RjcHhQJqSmq6w6JOTqs1lOWagcjqhcO9SVT03MOOkNEywHieCYumauKFN3B5XFrrzRZk63bjgToLLqTnbf1kbX600UIU0XCrOeaqsF1q5PjFmdtng2Nc47iGQh5+7IPkWv625r2l1xEKzl8RRdsXE36mfAMG51GpJ0noays2JQX14BDlcyVINYE61rxydYHDmAe3DYpkipMWDbb7HtowT4S7dJhNDDMYM7Qgghl7fpt+AYcbRYJn+Q2AMcAN6g6yJoLXXuoJijzQFh8PXRybAbH01vnRwP4botYIw039yYEJJ25sVVldEs2myobEGW6DYZffXMm9/5xhBZYWzjNQhhCVsMwXdAYsJJyXpCbwoRghUpxUzIz3i4tMcRQN4B6OAoou97cODsdzY0hHLcBhAb5BZiSG5wm/DJEA4hRfRN4xRBctUkolhTcFIyOMhVqOKvx9npdYg7PBDlVmiYgTsmIpaEF4uOGkuCKwmDyYkGB1kp4cgJIRTktBQ0S5YlQnADEVOdnHE4e5TY9KrXEKOifEpQDn3R1TjwWqP0KdOM8rNJXza8IOjZTcJ0NVdf1bNr6RUEkscEc0QqJgRHZVZFuuq97W9D3pp+UTMB3t2nq8C+ciZ55qFCwwQD+2qFq7R11xC8l/o8w7ODuLmA7zqE7QX6yxW2mx7Od3B+gaVbIF4uwOtjLHGM64sjXEwdvkwh1551VwjAtWQ6JRuY7y3RpSVUN0WFHUhfOgSO8+2N/QAMG0axFlB26GoZqQg4MTSAAahXhPcOXbdEtzxO7puxB2LEdrPGZr3OpvBMDEcxb1q3m02K/3X3Dh6fX2IV5AAiRnAIoJhcK0K1cSnY9I6+7hqj0TtTeH0F3lNFWmvV3/i9jwVoGpu25q+twqc2U/YArclH63OnKFZEoXyf15aRT/QLFTtSFcipWAAEglekcwTI/kTl1cF0YegtjU5DzMZZLBvgNVg9wF7cMkQOTln0NgwWeWg45EoxElL5UfqQ8FfiHBS2+42vBHCT6JRyJBNl61nOmrTUnhijWGekAp09NNuLLWWN2b02mryUxl7HDBBR3Ow5rFwSgayIMF0YIRabd1wjl0ljH+X1pvIqChRp1jP5bA9Q9ddKMNpDYPdeviKNTcKvCiIyz1XJkC1rZLyy8oGGen2dTquw5kQHwMFKBkv3CwLBybz5crXGOqxTkEYO8C7hkXcOziWNYYgxIwRxOtXdbjdwPsVq6Bwh9ltcPz2G7zzCBgghpkjPLJtVo/1qncQVX2gg7g5Ac520EHdXOv3aGGQ72VObkYIA8/h7XacivyIfUDE529eq/CaR1/LyoJi6tCIHvcwhMVtO35ldimqdF4VqdF3SCMeYTuKF4EUNPGAjDTqCg4f3QKQeMVhrgoHApSt5hoGJBLlFQhQM2eeO4Shd35Mi/aYzC8rtk202c7ae0I19QZQJEgjHJXcLp4yE5F0aiPrm4SFeAoEpygkIAS5dneksMQ+cBYqk3dUQM5TdiMi0vQXWdWRw9VBEkllhnevkjwpQdoNhh3zbCGMgQqqIGwm4ZH4rjijumPTaHEvklV1bGhXNczCKq9+U+FnBJve2QZ2tBYZRwhfMqrX2RiOrws9kgnGdu2DXutPOHbzJrwqdqr5F8+34aRprzj2SY9mYotJ47K7CX/PJra2/UUAr3U6g3fzNmpYr05wqf6/7jKX5iiOHt3SG9yB0XYT3QawYenQLYLEEnHdYAmDnEBGw7jdYug5rJnQm6DDzsK64QYN2wdTmtYnvU4jYWteNfJYOFBvcRrlZ/tAfMGsLRhCtM2kayyeEKOVl6ZL8WAjVqtEwbcknd3IQQgBCv8X24gJdXMF5hqMOvnO4du0Y/dkZMsMiyBV5BHiH119/HR/68IdxtFzi/HwFwKNzwHLh4RlYghGYEIyAbl3hGIZP8dD3qGliey5HigYpv47TwGxcFVvIQ+X0W7a1D9cOpeN1GrvHrt/VdVrrFJW7qXpXt5WRZBMQsruD1pUOtIAYxGIoJJxhL9aOUXGeh9L0a1ApUN4Ymdq5dEU6dR1iH/NVlgCqoItW7uLslQAA5OQ2NJHciCjtzkK6xSxyBLNcn6qHcoys0BhuX9OYZQkjVFZQK4AmGqgMrIKTGTS9gj1ZFA8UhADEEEDOyY1oSgOSMoNyPAlCpJhurRDET2tBJ2eaC5O6QltMEeUGqfKCJbgmDzI0GYEud8nKTmY+FFQRYeXQ4uqIes2NG5vT1pa4NQ1q5kMWww9aV1NtaeXdV16TBtCwVh2XygP9SK4siW7ZWcyWD86swardGRevILNdKfBjXXbepBCwWC7g4dDFBRyS9phDjxBCumt6sUCIPfow7Ha8hyCbT2ZrYJyfP8aLz78A7x16ZlBM5vbEDC9+Ovv6p+2yOPLMhcNKgFATxBYBbtWdn8vGjMxDbjSaq99VM/IXK8iPNjvyqRousEQQtXXKBhSU9sRhAXAP9FHMCh3QR2GEUKQjRJAoGczVl0I09M5fufgxX4lEcOmWhUpCcTCxCaS8CDNIMV0hBBfluVydqRKLtCrdoxyzMOBUm8uwbKf4ICXwQQbT6bVABHuRq/oWDlk5yzRMAEe9UUVM86JYSxBnwq0+a7qws5YROj4TWKvji8GOgohz/8DIcToiUxpjFqcRGXvtvd6eobzD4pnF3VgTnYgyGjCVPsnK52s/N1Uy5Lp4wL0ignTVluprwXSmBKcp/G+CGZOmsmFX5TvqtsyvcFuBnecDgCr8aDSrADbvtU5bP5UMZjRWT0kwm15bjTKvYnmnCvQdw54F9oL27+rL1DsjOBw03jN8TUAf1mB49DEghIDOe3TOo48RkR0QezBFhLgAuw4e3XCnuyCHx3Br7yGKhrxGa6JR04ZnhHzqrjBuRBt20lZqp6nlEq3XPs6KTJVdgFEcoYIXcRKcNSCmI+D4+AgLAM4lK8fVpseWexCxmPsmC0bvHXzXwTmHy9UKF5cXuHv3Nh5evp7iMcUtKK4RVmfYOsBfP81xiByX7bLm2YRBDrDEwhqJTo1bLeaN+E/1vVnOgc+LNErXDtwo5L3fIYUbfLf9q/k0mTQ6ljlGk6vYrWyI2IuiwQE+ysEX9NBLtrYiYIShKXkTm9KEbJWb2iHHOgTEfLLvBjPfAr95KA+A3KYuVqqpF5FTTH7FnXQTRtq+cv6kQsGgJReHXiqrgkbycU4Sk7yajG510JJSwcq3VIS4lKchDDHXxGfaORriJ1CyrgicFA/D/Gm5LXOj3BOMOKf2zTlExGSBLK1k65MtygjrIlYUU9Vkg+NmF1QRFJrWQ1WraxlMFQ3NfDWtU3RpyYa7KrXA5bq4KmSFnhSgCgRgsBSzVSuN7bitgIC8r93htJ8BV2/r1ZQMsv7q5845hBARQw/mLRxYov1KBH89mXZIJ9FRO8HJd4oT53CecLla4eTkGMfLBTaXWpkDcQqa541mL29EKlDEzEjXYqrA5MnBIUNRIH6jnCm8czwQ1BF2UfNrAQ1+liEahK/fZy0dl/48o0XiDLPnwSdezQ1jABbZjI0SoSdGZEJ06SaRiIg+MmJUohWzoiG7UVjTfpChQTTgBFIsHeYARJbAVTFbUjgNqIjEiDh31In5mw4MwIiI6lMnI6neeOVcKUFNFIQD57nKXmo5Ci/lsVWWkP+V44jIETEASRHCMkeUlCTsUp8ciZabMrFNi7+mksO1l8XEEZIVBMWBaEYnygMWTYLcXqHSmeSOSMRIx47J9IeViUrtlvDLuEZzz40S54G9md9CkGucLRRq5iU1nlvGcIjVQQuuxAOuSh8mCs/0ghrPqbSymqySjXBoqpsS/EbVVcysfm+Zzagh+zi1TTNVwVRbG+mmwBk6PpWF6np2MPFnbJE8w9cALBZLMMdkgUmQ07WY+BonpUMfNyAkHkUeeQNa49PoKr4JMKxk71qwRV2JPE204Yrkc1TpIb7ImmdE+wfxDkD2YhzRrVwWD3uRwEmm6EMA4haEHt53Im8woKbfTu6n0tMw8mAQXvvy6/iGj34Tvvjam+DA8BTQ0QabszeBYwfPp4VA3WoTEYoTP18p8PeRzVqkvtLcSmaVcQ+dR2uRkeW/A/MdlrAs07KFWkFcbxiVn3mDLyq3OvkRHRDEGtPrJl9+R1CSRxhZy5AOPoYYWoN8ojeSqZwom3VGiqdAENdcpQGyxqOcsOUNctr3IAAEDxb1YnYvBZI7RfRgz8MuTecOcRivLNyJ/Jkf7ZgkAhAJnIU4zWOteW0ATClfFmbqetqQ5TASjoaYGCJbwszfUIsVztJ3Mm8GbabpMJvRZzkcNHEzyBZvPrNcK4WrCFwcTGl7qyIsHLxOJn+ME07JF08CtRjVqA4ANNxojk9j15a1+lK/lfraem9oqVppOjPWPDGIecavwDgOj8nQGMmMGETw3sMTo0NE54BOmC9HhusjNhHYRoAR4LwHxQDnWILgpY0lEeFivYY/WmDhF3BwCIjJykF87RfkobcK1pv7fGqqiMjDoBeTJ/miHeSnAAKKU8LWGGXIxGWYsCfZL43SClFrNYMMbbObGScKBVcVpsRdx8XpXBIALyZrUb4zwJyUDTGmk/MYCT5yesYM5nQLRIrHkYI+RkTZ/CfKoURPYy7oIDlmOPaViVlSY0cnsSFILRgMG8m7DSdXIPikKRZ1dw4FJStGA90U1I3ZpAGYEg6S3miRWpzSOGR3jLzkOSKAsqtFmvukZImgbO5GcsWraqAS80/aceFD0jNtiIkDQSqY8aBkIoAQhyugWGxDaLhFo4lLEem0ABUBQqkx1nqV2CjeqfLHWjfUeFoL4xmvePhdJK4ElpoJTa6dRieb64urMg6hBcozq3JbzKY2R2vSCDJmpTqGjWptWfmmj2ocdHwNKozyT5UP897+IAwHO1q3phsN89Q4o5pb0956bqbmQemmKhvsvGl+VZCOTJAb7ZmyouBqTCeaM8PXIJxeO8VmvU77lJhOPWNMEX0cCEfHx9ierUEdgXyS8Ho3yBKWZqlgp/S0kptTcocxclLjmX1XF2Jg17o+WHBuZMybFCv0m6LsgQ4g8gaVNCd3S2hXdn01he3dzMrCj5wsLRe+g3eLxLuYxErQwbsOTuYPxAjMAHVg+Ts/uwT3jOdv38a9e/eAEMD9BSi8hedufT0e89DmfCotHbFy1L5xHG2e2t3JY3SIDOjqQmlczj7Isuc7QdgMf8jzjcESLb80kE27szwzgFrbgpICwpPcZNUhBzJXV4r0m+A7kVFBoiBMVXr2cgCGtKcAZXksNYsB5wSnk5WDMyca5OSIPIQsj3GIGVdId3QME5eLRJYjsJzhR0BcfxIyM9R9oLQ5YK5tEKpxFvwuXAYQjGIkDWh+xxjcEtTdNgmciJz6p4fAJERL5WITPt0sAqkn07yUIUdz4PxP/tSRVgFVZQZrfW66hmwMIi/z0nPGmlobpueIVKa1rSh+NGhtSy6oZQyVc2noULM8V+WtoZbRalmyJU8SAQs2gXNJLBNULqqJMwb+Y+XnFgnLbWWU++Jqbq4Ch1syNEArdS5p/GKMCBzAISAGzlfZbPuk7SPEPCiOHBZd2ozpfbMEwrrvEQnoOp9OtAUiEgKrmUdx3YkOAg+KhmKwJH0dV6I5WPskS66YojzbRayLV658to8x1O+n5I/62Qix6zaLdZVD0sBbBAqAhiQYNUTvnScC4GX/Lulj0DEnhEjoGYhRgkJyIvQxRIQgz6JE9+UhACRRIv46zkkLm3ptr5FMwSYH8pfaRpkxJP1CMhsbSFwUEzcbxyA5UGZCV4zeUJ/Oc6H1Tq0CQELklbAGOOeh2uTBucGQW2I4iXrDcCIkCdEFABdBYRj6jAcEGQfO9avhWd6sc2IQKX4FkvJDbBDTb5rEV6sg0Mbm9aR4XlOlK0g3ygCscqFQ0BGam9EqSYasHG/1R4SvyfeNsnZBZihmsVhGY4eGMAhMWblgEtRKHGfmmtE+ZdcyLf+uGUWuQse2fr4H6qFXpkVVIvXDHpWpzKh+wdO0C5jYjNi+mHKsJVbRUH3H5TjZhJZ2WwuSEdT0sq5rhq9NIA+QBwHovEeI6SjIUQfAgZD8uPsQ0FMEd8h0wNIFxdF83bbgY0YruzBFEs10T09UMy1Pj9nmyw8O7Vfjd0vK3JM109MqQV6nSt/UWlXfmb4UazzisKCRJk+SPQggl5QNQXkpAPLgCIQ+psMQsaAcWCMhskOMwJtvvIGve9/LePveG+l93MDFcxx74FIaqfVFGEW7zmlrPoGRuXE9hi1osaip6XkWioEmjX4HQK1VVC5XJbBtiG6cGMZ9d1SQ4al+cFz1Mg/RI2+0dR/AIrikQy5RTDDgY/od5TYzDkDPCV9CHGRDJhEPLcNVcGI+kTcYadFGQo69lQOAE6CHPM6Z6ySlP9GJdqTi2Wn9x0FIqKX63Cxxv5ggCPZ5VjpwdaxGxqrBuVS39ZGlUsEw/BpWs1paUlWnEgdVbmRLadLWV8KRyVbLAfmZGT8tlG0m874ataHZKNOaaRmBpT0OAy3Ih9pV622PirqrhhjjDoBkQy5t8mzKkX6oBcORnJ0y0lh6TCvwRl1X3lLxFV07o463nl8BnkjJYAU/MhOTtGCEGCIQQzYN58hJ8ZBPBlIv2ROco5RPNkEhBGw2aywWHbwjiLc/dOSJRVFIJQIW/Nkglv5ZLWpNvFtIMGndMCGtPy29pqrO0eZBEMrR+FQ5599ReKtsh6RQiNWz3EUrDch3u9jywtfM3ZDPMbAAwFHu5GVKRD5SUkYwsoAQQwRHlqi68lukGZZdXKprmFgyKu8cA0JWDpFL+mJHQoCHfg9dEeLNyZKAWZUYQnwllkP6U3M7yuVQVOIoKXQnaRZlXicVj9INWiQNblnaWeq9xtbmwBmJYGiLrCNthc6RUg1lkoTEhgQJdgoXjCLIXeQBP2o/LQCDmwrKNcDQeTJEnkw6arSjwSBqaDV9r3Jg6j2X9GvKy9AyzzoBm3c2jZp9KoOoC83jx+UDPYVvnrLb8RtkkvKdPFAGZJtr6Xazo2P5Zfg9wYBbcz8Fu+gTTRSU6dA4S5nUjB+jVJJqGYzd3c/PG/S1UdUMX4MQY4TzLuNLv94C5ODFlOfy8hLMwMXZOcK1U1BnkWwMVsGA8mtBO0YITaUFJqo0E2LI1eGAQgrLngbdbtJk16ajI35IaNLHcSPK+hwROnISv8nBOZfdLRhIgpLIFcwB2f2RGSAHIo/HDx/iW7/lWxLfF9nD8RaOwzBvNFZG1nxOX0XzTsSXkleMuzJZZv2sGAqdj2eCAO88ZANS+V7Et2A51EJ6prFMWl2z5voF35M/VmUdKhdEkTWdWruou69sFDgyPHuEgGRxK5YFUTYOWekQo7nlQWRQqEw17C70Jr28m1NFAw0bbT2lIpAEB+dk1WDxjCFWs1p/blCBQ/bQaXhYiu/DuKmCQdefPQKTZ+I+Ufq98GCZwSXuEVyOL8bmafq3ZrxtoW7nIa2ZeFVOFLf22YNPrUJxIvfTpJ9g4M0mDF3O9C+PK5dyWi1b6EFHpu/jKSpoIWGQgxKNG3BescDLs860Qcs5xP1TMCj/yOPDZd9gy35KOFjJMBJ6cwPKO1DV7IeGKACGI+q5a1q46vskxcmeKWJ1cYmjo2WKjM9JyeBk0xcwBjs5OogFMSrX7qCFomGCWog48i1zw/ea0GvWaBELQ5ra+qGJ52zKaHUS05u9ltBtn7UQMDM9s1Hk2DBBIhTaZ8N/R3Xr4iryupTfy6BHqY8jUrAZnyYoxHSnNXz6TIEkIyLxoDQuBDPBQB70uCQ4FPW1S4Qc5NINFA4S1yPNAjNAViMd43D8FCNSuErRFed5zPYL4pMZTUetUqOeZcqCR2orFdrH6ACOQVK6vLm1+KbgxSTI3lBhJ0jXJulk2CMkC5VEo7542QXCJAFg4lMM/dBidM6V+Fmz+sLkbZeUVUsRisfUzjaCgvvuSVstRGeeWZ1R6YtYFSFDa5VzygidDnsiYwURVSXbqD2Sv35lmYFuoLPlCrW7bT/rCPCTliwotfNaQJFXPgs6Vb8/aLLa0LSQaFSV02j7TD62CRtt3NuGXS95/HMKrWd4b0HotwClg5AQQrr9yvmkTCAGxwDnuhQ5PhNnQ0oqxBl4lcju8jzj8B56aF+Pklayx16wspb5skvAz5uqBu9oLf+a9uR3lYCS+2QGxCoaR4c8DRpHiHCQwHTJlBaB+8Sv5biUoDcGsNSXzN/ZAev1Ja6dLLPbbjKFZ1BhSTm0X0/ci9N2MymW5kQ9NzDtrnlplb0Y4vpzJBPWic17pb+2PTXeFQ+vCrv4d520EjwLi1Ee5NLChXKi2hyTw+BHHgLFbTL8w/BHTUOMISi2lMkxxYnTuI+R5ap2Rr4+kyMjOocQQlJCkF5NyYOgqX0RC4BSdpP9DiSQI6lcCrkqUq45Nxo9Zkgwb3kWILdVJN9k5ii8nUbyIUdZE2TG025WKtkNrA4jxr13KA0DQg2zpAoT1gM7kOypaJCbJK6FhRQmLA4TZk4w8nSpnFmbLapMZXA+h1qr6rFrt8b7Fgq3xGOTJa+nkdi2K9MOmW4KrNys8eeBUg7V0CCMhjL6gLK1/PKlfGQzvIrmKp+TwTiUfBysZGgVqEjpSGIqOAfHLsVcEME6ig29LsjILEIygTkW5mx6knxxucbJ8VEyLeIIilE2kkYAjiWi2TaOBGI2BEbKYIchoAzGxB4YCF/e2Ko5Fol108QoV83aMXYT+Q2256Ac5juA0aarKM90RBdsbcWRk1bl2N91+cWi5aGfiugAsqa6tRAdAPJi2iYT4bJfWnLbiEyDK0VMxDdEc49xBPJVlPLMapQHdYME9AkAySSrSw4IEgtE2uEiGEE22D5pj5kRwlY6Hc2YqHFawoeBBpajZa0QzLQVD1KMCROUxzCn3D/5PlyulIx2ySCfxd0050mJ4rKKV+dK3El2IF8mIjTgTv6kgRAr8VdmbXkFF4Uhn1Tl95OI236ehSaLcPuzHZTIrlWdSw2Poc+aSobsWFlqnIt1b+MYKOERjqCnN3WbdHxbSkENGpurzoiys4vDb2q/qzNZn0Ob1ypya/pE9XwdADldxbD25qd2mloeqdfFOwW7hJMZ3lvQhy0YjH7bY9v3YGYsnAegPIjgO4fr16/jsjvCui5gYo0VhwwT+FtmQHZptPw40wVDa58E90QEy2VPlWFpuXXfeJpT9Jb7Ryui+TgjoJt/B8A7gERLE+SklRyl6yw9wentZZzSkTgts4MEZN6CELBYOITQFw3T/WMhCykfM80ZWX6atsfGd3skUMwnqrGuup7jEjCKAN+YmguDYyP3XpvsivPYNJsmjOTRlLiR39Sb6TNXtHoHkEH6ahjy+FRGpblwK9tocg0o6US+YAlWrvEdWCaWY7Kp9k4ss1UeEwvcfJAAiLKq7HO2huBkWQMAECvu5IqQrLqHjuq4GKRyQIgRMQQE3pqOV6BZ9ETLAXoVpkWuWpHH+m9zkodnRINLCeUJgZkEXS/F7EwAD+1E9WGP8c28ZZ2EOfRtCzwo15Lm53x7Zy5+lHdEwMct36mcNXlUKdKybKrFWRtXL9Mesz50f9VSWB4ELYZhnqkryLgzKAJvm8d74XAlw8SgqylLjAHcb+Fij444BQABAPF/5xiFcMdsrhPETH0Q4gkgj/PLFZ6/cxvOy92tLiZbp0ADYa7WghWigTbiFNpPRo7MWWVtEjxrHaEEqz6YfVZybDHUUlfrJNGORZ2nWRbGSE0wyhSbwFRiTXhsvqyg0feqXdO5McQ9ZyLka0gZQKfzQgDLdZFMqohQhYOThcbiVpE2/bGPiFVvYiZaStCTySQ5Cc7jHEBucD8gAOwBqAmZNEbCGIToEGIP5pD7qZrpZEkwiBwZl5RIUTViJExDO8lJI652foXwJQqHEW4RiZJLWipWGaIjB41m/IrgDFGk3Oxhoylp8tjJZ55DI4FpnoxOgkuFMqK1aIwRlKsRdh8QJoOwVk3OVemnp4Em+DS900oGjF9kWmAWFQGl2VkcsrYsAQiDIU29TrOiEcja+1FAnwnYqcFugaGN+zJcCeO4mgMq8xemtBP15OVXvdshZxS4eSg6TbVjX/oZ3rvgHKPfBnBMMaTSCTcQwxaRAd8tkGLyiKTnD5/3q+IHwQjUaK/zFt63oKYlh7bJigNTLlP7oJYds1tflcb+ruU3K2cRJfpHzsE5gqcO3qVr0NMGcYOu6+CzhaLsAB0N8qpwzfX6EteuHePxowfJHD0KJ6e2DOhiut0gnx7SEJdKfuYxc8oH3CBD2dhgU3TbWhLmQhvjqWNyFdpflLMn3yTU7dmXttqDGhEhf2nua6fKoraMng/H5WUeG31e7cyyG2SeMBl3E9shiWmUrX1ZDsWyCwgjyaSRwaLUCmqyJG2IgiiKL1F3ipArxZECfzs1fQTEGtxATPKxixHsCf1W4kfkjtHQdxmI4VCgMWE6GQbK2A3ToAd7g6LBla4gubxh31SXJ6Rz9NY2iyntFWzQR5smi9EwcpdJWuCHqSbTGrO3AQDj8ZLTpf5iJA9dRSTNcm49CFy2OxdauSBzLPe8Rry8Elg53EJN48HlAbymt25NV6EdTx740UjpfuHgOweGgw8OnWPZ2KXIrsx6TysNiC8bQIDTHcbOp9sHOOLi4hzHyyUcpysx4bYAAjgSAkqXCStI2s1FPQn1wGhU2swMgJHg6s0Pu35Gvnf2eQv7aqp6gNBuJ94+t2WStL1Ol02jDxQKamFc29faXOk4O3OKbxUKEH6eNbvGHq6XglXJoJ4G9cY4nSTL9TZuiPybbj8QI0nxrVSFg1zQmO4zlw16lEGITnBQzMzS9WMd9IqrZG5pjubhksWF8/AxxZIIwSOGmJRp0tHBpiG1paCx1p+mGGB9n/5J8X6UaA8ma7Wlgh1/rR0sFgsFAZUSDrWhsoRbJlxCVWT8KXB9wnwtn2Q0CGnGZcWrXe0acYeyzOb6qpJOWhjRMB0a28XJWvGUmJ76vOXIvburA4CsVIs0CCZ2AVumWLTHLlzzUhUQ+ZXRZhKQr20thDf5bQXTFu3Yx5yaAmk9JzS081CG22JMmdFjKL85d5WQWitgapS02Vw9CFeQDize7xq3KxY7w1cxLDpCv93Ce8B7PSDZ5JOjxfIIm36LyAHkBsVwRgBd01a6Nmt3UkJr0AKN16vQOtRoFVeLG/q9zncIWJe9Q+lhnb9oW02zTPts91vvlb+QNEY3ZnofPHPa7AWW+F8xSv0Ech7eDSPIxOg6h7Ozx7h+eg2PH99Hiv2f5AE7zoTBDTayWFyatuZ2Gd6SX1b9s9awTIMsayyUc94sV5py9ETU0qQm39UyeHrOdrnMTcEoTxye1TgGKp/lMqpxsV92KRucSVeXq64s1kddN3ckwqbKpXl9UYrRWCi0rNjGg8XgsOmiIZilKi88crwCZkYwd3AGFiubmGTFdKOET0oywYUgUSmJ1c1HQiGqu4NhZOw8wGm/FEInuM5gRMTAchW6wQlyQ9567MxkDqSKS55ur1VpHvkDajlLUibL00ilBTWRuCrLAo8g+AnstGvKth3SpWrLIeWV+S3/zmuZjCxKGOHzyFNEnhUEwbaxHIqpn+1FOJEnt5UNXTRzop+73HhrqBWbIx7BA23JeepxAErl54Hw5JYMAg7J9DzGiNgHuBgRYsjE3XsHH1MaytsjCccno6cKh4jkerHZbNF1Dp13QvD7TJmYhhNA26SCmNbrYVjzTWF5ZB6CinmzaNSqfCnzYLoyWoOSdjKIZKPOUfm7nmEscGvS1jP7wiK0R4lceRGqBr6q27FswuKQPjO0aBQ4rISdEfqkwQ0xZE2dmpkNZvwpRoEjEvu1IUI0ifULOZJ4BIkIc3SiVEnPOgKiKLdCQWZcvs0ik2FOypIUpErcCEhdHZLJZfQenh2iB/rQI/ZOLHDE1YeA4XKfgejWZE6ZUCZiqq2MQNZEy0Kn5qQapHcaU0KyKUeEasupzNJETGQkl1ASGVftbzv3bPJlDTINOJFvEZ2oql5/OwlxQ4pwKjBcAZQwatnqCuEo4bDGbfPSB6dp5Df4CqZpxoetcOWqxjElqB5VOwhVHmYsojJprsOUV1hbicUmFZIoMjOe6o/F3FEaKoop8+xRahEbyycanumnpbWo3k8XatIZ4bAqZrQarwKa1xjX7GrKDO91iMmKIe/i1CUP6WaJriNcrtcIvEYfQ7JEM7upQlC0eNmQOyzsElinXk3hdWuN7nq+E2i8lorKD4BdyUblot2+0XqODA4sFrSbZIIu7pDqdqi83omLoHNIFrYUkcIvEB6dPcatmzdx783XhCfoZkna3eCd9pCrIHuCBqObuYRveRpoXZDx85o1VmzCnN4qn2XIxQWCVz7LUe127gMCJienjnnQTNrYBFEjA5U/iwOJev7jBJG19SjUInl+X607EgVD1IkTmT2yWBXIMTEXZZUDEIV/DWdHXMYBMveKdqbHgUUW9RJ4mwnwSXpzPm24g2myKtCc9UnKshhlP23vOsTOoxclQ4wR0cd0nSabAJWitLCy2tRmTmXo7ALJ1rZhisEn62HHSUWn7U1FUCmjUHI5yXKnlqjaOaM80HWUXQzqJhtZJFvdVi3UQ5Aif4MeF/Kxzmclp9axc+yarNvWHN0DiK51Qc95eOgCUMp8B1n/NPIBVZlVOZNNnejvPnhqJUM6EXTougUi9+iCTwRdNXt9BFG6HZYowFHa6CUkU5MbFi1XIgbb7RZgRtd5dN7owgjF3agt3EuNMooD8zkKfDbBdTPztoM/WEAV9TNVxL4GeRZsfRNA+Z8EOcBbVVxNx0fMwhCKsvA2MFD4H+ZxNbuRYmPAQz6wMAb5U+IWGWLFksoOzGKpwojbMNwIAYhQIBEERF1NEBeHiHyTAwEg7wDnjdlOGiBDp1IgHD/cScJgIDK4o+xbx0LlEoNAuuHE9p0IER56phHZwTuH6CICR/R9zJY4ZXzeYaAHAiotk0HN+AWSeBEl9UqRjaXPFv1Hju+GKWAg7NF8T4oYFNF42WTXa58hzXRqMlj/GXwh86yFZzkC8NCocrNMthE7oMFYLN/VvuwDM1SZV4MHRYKWFS1ucxLoALFQ0MLqdtOQBm448bBuI62G5nWUCUlZZLSP2YylFkcYFICVMJflIx4PoT1ZsIcUGe+5pAUt2tjcMB1gv0fVZ1HmRH0jUFyyY2b7WXDPIZtVElyRRx6Mrlfg+TN8lUKMPRadz7Q2hpB4BSc3O2LG0fIIYI+OHIKhK3ndwdA/BTICcwMO8Y23S81a79ToPqUQa1WxC2dbLgv58xkgu2UFOxNhqu2MEGK6RYIZjpJ7C5gR+z7xfJJNEBE6Eqs/jogi0D16fIYPf90HkLZIDCexHQq+1iJYAjVNUdpdd8px+tOg48bCfeADGE7rs3+/CJxKkx0GPFK5jYBmRPS9p44TL6dcNQoaq88sD2vxolpeNC9tPDTtj9d10oBa6eyqdsi51JBOBUCVS+Udi/IwIikeQgzZrVblMgD5EEnbqJvWfKU6xPIFlA4hyZUb1piUVuqKmctiDMFFCSl2yGglp4FQ0TGbVpgUnggudmLZmG5lC16CpUfpTyUfJjmo4LhS3hAbYajGLgAunoj0mn9HLktUHj3czOYKvHUYbn7TW2FI5NSMSjJ3VDU3yyyKRzDrsBpGS1cLKyqaxmeVm/JhdmyTgIynXBQxSqsXhFDrpclbyDS2adLBmn3UFvU55iANOi/r6j5iPzVPMig2sugw6+Cq8tMTB37UTUtqfEDfbxD7LdCv4Vw62XVi15KufolmkBOmOUfwPulyOQawmK6HGBDCBqcnx3C0BSEFlMxjQYMfXJ4XqhgrDYOe24zyx4joTfR9kmGz5InGx6heDM76H40h4+kUM60WSk1oW8L5XqZtEigPsJHqM41rpNc8ARiQXyweIgZNdFSGygCHKDdFJAVT1BgdWpjc4hCQTidSPyNAEU6IE7FYO0QCXIADwdsZc6qooMGfTTiD3j6hwt8wVOLuEAlORLfBYoXkNDlpmx2SKVt0Dp6TmWaMSO1WU7h8fF3NouA86b+mDVwMtjwztN1qzhPOls4lZBAg83MhaMqXSIQWJT4jgdYQEBmOYjEQMFJSWOY1EgQrQUMZR6vOJwYl/Fc8wcnMiEWJwIPwYFlqnUffZYZm5QH9yUYQFMWji6LI0XGNQznFGFZgfXiL5VcJDJoGLEtAC7fvTXZl/kV5giNNs0Auk+qXJ5q+CRo3hY+AYfj1613cjsevWsl30snpJj1Vmhm+umGzXcN5l/2M+76XNeFAzmOzWcEvTkB92q1wZMBXNBloxi9QxcNV8K6GvNmpntXQimXTws9dOLtjee3OZNcfN+iNKYQaz2pZaLRJisrXEqNyfikxwBj53qgs2AyuDzFGeLEJ1iOF84tzHJ8cg2IPT2FoT2P8Rn1ojEneFNRZGOjtrqA+PTX4Yj8ZyMQvSxbCN6xHjqWP2fXiCoim5enGfa91RINZHmJlmGW+KvuIPk+UNRp6MzZqlOAgcqixXGC1sI1y+BWRN8Bpj4JKLhULawzjW5unE9IBa7LAldkKQ8MlzHjZtyxACFay3gA2LBROgUWkjiTTsvp3FpuB4RAq6VA8yBGY5YAsxnSop31CcjFqggp1ap0s12GRQbrCejZ/GZ5FWVec5UdGgEqmYu0bka2WZTrS+NZ0Qp9TKc7YAIoFjcEgUxW4WQ5X7ioBzZt97MFMS7lQt89+n5IfR7edWOAhTV1Qvs1hKm8lG1r5LJMaaryv0k4lyHshLvnWrphnLXjymAxFu9IGzHeEjvSUOR2JMsdkhuYACsnMPG0E06R7L5YPPvnZO5f8dNarS1w7OQZ4DWewJS3sNkHLBLghZNZQT05N4IFKOczjOQZE2aELpBIshMcld4S6cJumatAo2aHE+wpQn+xRNQgF02wsxszUpHP5dJtgYjJQvu4nkCgZCBIrAUlAYx7U+5ATBkAKCEihHRnEKcIWEYHJw5GDo6RsAEWQRCUhuKTc0tYzUtTprApUZdVAulJ7UxpC8pdjM2Hp+sqEVOmao2TOFrkDA+hjkPgSBETKzCm3wSXVgMuDy0PwyNGAAoUmXccWCelr3DC8ydaYCHgmxpyfJ7O/AVFNDKICLBG6Km6RVKZDqL+VSbybkBkbAb0IJVdxwdCoxBqcUTfmliEwMASPAgYrKCNYq5zBJh9QyRDmr4Zi060VGYmt1nflLnJinvpuFNQ1c+v2uHD1OTmdDYaYx6pOZzvJusaQFSKKP1VxT7VZexJ4l1F3hq8EyCSnYL+MGHr4bgGSiIFO/AEJDN9Rc3N4laoOkVMm8byxCAoaWwkjT4u/Wp2ettdgTxPNcm62v1T2S7m13GHSAkmJqpuO1BbOckZiaVHkykRAomE2MabDqwV7GYv0brVe4WjZwTuJx8N7xvwJgQHZcOXG12SvqFOlE3tazzCbKB5OK2v2PYWPu/oUpTyltV7nojl5ZeFx9GUMrk5iGp15npoI7yiHqh+KD7ZcY8CQ02W+DBU5Gb26aYtGKt9gpoopfR7t4UR5MbmjmGRKnVyzznzeFMg1qyBw4Mxi01oVGxrTQLtXiAggSFBTKndCyUI8TVoEIXqGiw6AS1KzcwgcEYJLsjdEfm6YihARWGLpjcGMsJkA5c/ZHSONVnqnL5kHl1qkGGQOlCx1xT1atwBFDAJTScZDQoE3FqdqWSmvlQa01op9UIskk8BD+sbjociqvTZDQ/Qe1gMPlkss8tOUrNqKc3JlfkRISquIIeimTKy90SLP+4HwZEoGczJJLCgdt0DcIsZt2pSJX1AKuhKSZo4jvNiJ5WsHWZFStIaOQEw4OzvH6bVrWK0epcVJSIsAJIg6QMGYePieF2uW6scCK4FBcqRJzmXf+UPB1m03EgAKrdqkpUIFrQWwj+FdaeNWIWOLQVmGl4UWZWhGu1e4UAiCOlE66Nz7SIjRwQVALBuB4MDUg0OUG0MGPwshxcO4sdU6S0UOiBSSeaMzBJAdXI6PgCxwEDmzeRpmjPMn5U1/YgJCzJ2HtYDwlJgCBE+YnQhHiahHB3CAaI61vXEYY6uuzIxsPEUDkaAsSOV50M2XEh4tw+BaIkaUCaBuqqFDgMEewq4T6zaRm1WVPZIMW7jXoHKk3W8Q5Ra0qhvV8YQFabPVDcLtqcwy/Ux3dT0o7pvsBQ3gQRCaYkZsv+scYii31Z6a/tVl11OWvxtGbt0i1JojM2wVzKaETFu5PnZ5GY/bzboeh3d6l7w+G/VD0x4yzzPM8Aygcx4MIISYTPGhQdYYod8CCyQiTxEUGe4Kt0tYOEiIncgHHEAfnzHU67IFrYPvg/spBGAkcAPFVcDZvY2QrE18Jzx/2OUxJ54/3P00CGFsiIxzHn2frBcW3oE4O1K221zzRtvGPfwom71XaSxvgaX9AlP77fpgYBcbtvylBcpPCgs9raORtq6wFUKqaIcZm5GyoS68gQO7IN+ypIUqD/IYDotMBykC7NOtDi4CHFxy15UTdiZKgcNZZE719wWGfYdYxoKBntLmQg+7bPuTZbYR/EU75wAEiILAuUJxkTaUBslI5DVGMoWhdMCVcDQpBiDfPRG8V76dlAmOU0j0EBl6DXrrVqu8RyJAg6rnBOpGoQtPs+SFMqyyaMtkJAtcgQhKwS0ht8Q5SnHTCMnCSPPWX60WoWEVW/xuCT1VwuJVvTBs123Wet0aGVfLy+XWZcZpOtgUpbidZuT+avuq9FPl+CeAnM0BtRc3aEf9e+BgJYOvBt4SrqVzuHG8wDJGLHyKo0DZFthjverRuXP4y0tstkAIAZEDENMC845AHMHk8qH22fk5Xnz5LlbnERRTMJ+sQadhMvf5zqlirtAC2QFjIF5ssOm3uHbrevKfmRDuW5CtHbhEpHxq57BbCUDFRzGRlqC3GHis0hRfdfPDJfKyzWsYe4VPxcJBld+2NeeROckL1A8CgWeHGB0WzOn0uHcIW4cYOLlShCFgDTikCLmMISKv3I2ZNbAScCsCSZmlbYrpelQVBiAKBge5PUJ3MyTiByXiDAzaVpbyiQgUWJQUyho0MgRyecxISgfEFIBH3GOy72Ue81hOhM5NPdBK7NXm3gQWS1pvuadZbMM8D+WpVUNEOnF3nPzryIvipjKjVwu8mmDWflzKXOtAgwTkWxCKLrSIuVkc+9ZWQbDLJg9ppLx93hItSw9biTJdAtqaADKbX7NeCvM2lFOrebRavW+7XtfaR248t2XVMCJzhBGNsZa5KmNp2l2MzZuftUCSAxNZAqEf8n3KGrPFZEfBdM1cWcVDC4rnUwi1S+CYfnxleEKePsNXIRB14BBA6OAoyqm4g0aNIwlgrcEEs0saSlyypKTFW5t173luaQXVL01FU1aeV8HTWsas6zqorD2JlIYX9NRkbZ7aGfrAlK5KdxQB7gFwDubHGr1XT6OR4jYwEZzzcPDpdDkG9JstTk9PUjqigS/TjnEzE04oLQ6KPsKUQRU9rRMKuBph6qqrRu2bixHvNJ9F1H4yfG4icxHsUCD747cYDsbpW7Sb7IurIqqCWuCZMpOMKO22m71IcgDG8IEQAyMGQugT3nAQRUKIIscla9aSXw4yHfOg0iLpDEc7USqsJHkUYDlQHZRajCRXqhWAzkc0B2MkR9pEhHSqhYEIIUmoWT1AcgMLpb1TZDlYYYlXxoPcqMWgk26p8KqCKpKwaHrTnIxsa2sUMU6YOclqUJk9HTIk1zS9ZSLrdKRaa7mjN3JY33OLMjSJZOMG234UBzvAYOk5nX1E3EfjYtfzVBlV0p1uo7mx47IyCigiPcn6IbNkdb02aHLj8V54YncJ7Ytjwt2bN7B8+UUchwt4z1h0ulPvEALh/GyFo8UCZ5dH6PuAEHtEYdo3rx1jtVlDPXSYPSII55eXOD09xdtvQhQWaQFZjbD1XW+CEpyC0ssrFZgD0F/02G42wA3OWkR7er9r3gp+0NgU7F6UQ9kqqE8xhNaLnK/FiMx3PaXUD2UuUycPOU/1LCMzxgJAdlExHc5m3A7wfiCAsSPEJUnEX06n/yEFrGFOliUsCog+BsQQgChBmfJlx4QUwElOLsBi0kOGYiYtbiQWgu1SoEVpKDnJZ7hSKlldHjTgpLpSpJEhHXSCXIPowCAEcRaNLinLAqe2aZBLDSyU4kRI+xhy+psEIcqD78pdGFFBhNLyiVkJElnjoFDWYhMiOueyMqGYL/lHeYko5IfNuwxx1neYP4sfmRgV1L7EnR3WjyPYQWebzw+lp811pTTAFFZE7zUZLbpPuWOpSVuR38RJGNEKHvJZq6AcjwGGxklbpvqrmmcty85Zkadav4xyfqwJ7sivz7QlPycUGg2q8xgYoQlVz105LnWbm1YVu+AqzHaGGZA4RgjbRFeRFLqBQzoVJMLRcoHNJmSVsxX6FO+zuMFXE8j2CqE2TUOe2VXGVaGWeXYdlDyp8DlZCJWPLKglpRwxJL7MPUgixKUNmbwjRjozTtcDglL8e0cOIAfmCGLC5fk57t66BaLVIDqgLfcV8lqVptXe+p3NO1mwyLatMc+PWnSySjh1olk8VlEKlfy6Z75H8t+OpjQHcoe/+5XWTI18VV26uc7v5DMH+IsEJzJaDA4+MLj3OX4Dh5C+hxTwO90qBrF+IOTDL8VLQlY2ZOtYEZIopl1y3ooHhhrZZmdaQj6tt5aGdg+T404YC4MRnmbZFIiUrBwYQC/vA3NSrEgZyYolivxA2WIjfQxW5gXoezO8Q3tpNA7I4+GyewRchL2FYif9ksKtEk6tUbMcasYvjwUq640K6n1QocRrIeMOOazIo2uwOA1uFlcoNacKb1kvZbQwtOOqTKe119tHW3Y0cwRPFPix/J4IeL/dIIQNOAChZ1yuNtj2Aet1j4uLLS5XW/SR4bsOftnh2B+h8x43b17HcrNMJ+BZ6CasVhscHR/BE2WGn52gJyl11eYWh9CvSmSQzOaWR0e5bIYI21LPrsEsDj8t0zdf8iaPxhsue61bQ7ZuFFi+35emBQU/N33cMVzpd90/br8r0jQKdh4gcfpjMWFMVgtJn8mB5dohRh8iQgjgkEy+WNxu0hVWQaLoSm3KoAcyLgTZmK3Z1jEkXoPhP6qBlQfMLs+xmqlptzItIYh/WTLf0NshAgMxeoQoEX/F7pONypFZkCPGHAeCkfpkCe9gwqWMLaVkTlYNdsOmbI4ABESJvOykbGSlAgHZFDUTKq1F63fDe61AGSNjiDdSb2brDaX9nELTfQKbhSvR0QbhVaZUux3UaFL3YYrBWOZAVXpU74ryWr9pUDAUshObjbbVPynDrsqaVO4UuDKu236detcqb5+ZngpOgGmvHc9GH5TO6PheWdFgCmwJxnaeCmHpgHqa4zfDexpiTCfinfeAd+naYubkr+4cvHdgbJNroEsBgMlsMACzNAsmsb/uq+LRiN5SY81WaZ4KpoiXrum6v/sqnBKItbxdTRD+58DooBZ5ifd2BHQUsdXLAUluOGONZ5/K0duEHj56hOun17DZXIrJfEnPW22x75VW1F2p4yqMbmlq9E/LoeLL+N3kXsLg4CGoZ6tgGBq5Z1M0+m37ZgucKKe1MS7adSiyXmUf4EyWimd5pnTLA9OwI2WAo7jMRogiQuRQscBN4miSsTim4OWQIPca30EEzcG9tdgeDjIbgOSyw3Icl5HMMGz5al3Gh2U5yLJ6su0o0Sl4DIcPEegY6J0c9MUUvyS1lLMMncxlY257c2DZxKswfRkpGHLzy75rGlWcDK7SYvegLywYeqMiakZbDXaBEg2bRRBGrhF5LA0+j9bsHkKdu2z/JiC7KB9Qbkv2HP2+IhNRXBmVuYv47krTgIOVDJ2RWNksVgLw5r17+MLnPo8btEW36LBYLLDebhH6iBCA9abH2cUK203Apt+COoejxRJHC4/FosMHvu4DWC4WCNttLni72YAIOF526SJBl0ybCmGfGvMXzcA1BkHXPEHM+QNARFgsunSyrVb1mj8M6VvQFMIJpZmPSeInCsrR521byyKHh0Cx6AmVAG03J4bZ14smKxhMGuv/WNyI4cx6mRA22O5qGgsr12/yZ2YjnScA3FFeqV104NilIFxi+RCjaGH7FOghhCBmVhILhOMQ9CmrsZ3Mi7hA1E2U+a6bTRh8FVMeIZUVDyCiQTGFpDjpAFE0uOTjyxqrIVU++N8R4Dw4Onnr0u0ENGplnluXK2fzSqi1WG4kdw4xzosRkbp0RaWdDBrakqfTmC+4KOudB0E2z50dJJjxqFpWCzJTcBX62JC/muWPTOhMQ1pry/aBMdALLetQn7eJJVLSL9tgbqfRz9yWRnm2jubm3cBoDTfaaJ8fJKzS+PtU3S1fP6CkO1NFA8PYc91xfW/5FDBIF4yRsqk1nipot5joPsH3ivx9hq9C2K5XcJ7QdengY/14k3y0Abiuw3p1CYQOfpFoteJEy9MKGNMKS2eLWEeHbqpg1ny11hm712oTPw9E2oJ3t6T2Vln7yn6K9URw6AhwiCkmQ9hKIGnAxR4+9uj03kIZGecCnGN4B/R5wAiPHz/Gi7efw3p1D2TcZRtkLX03P5SWKk+t2+yZgU3Atg/w147GdNf6m2uZZj5b47DTMlB4XKzqmdinFXwq4+UUc52AvFFq9aWZYfic5D00yJNFWyt+foVlM+JNJPtoHWttOhV3VkMsZVOFGqSQIxB7RgwA92lfEmK6ySFKjIfIUW6rkAClMnM5BoMGLBU32NwOtSRQlwkC9LrI3E4RTBz5fDhlzeVH5vPyW2/A45iu3WRRMARGlj9TIPQ4WNROAQ9tUkjX1quFcRIss9KOAaZBC5CbpunMPoWKf3UekPd8mte6d4phyZVi1BUHa/U7DGuxfr4XWrLaxFBeVTEwym++TIhFVytvTwFXLf9wSwYjeFsCCAB96AEibPoAJoLvjhFixDaIKbtfIvAaWwbWEmgn8gbbLeH84hLeEe7euY3X3rgHIkqIw4zV6hLXr50M1IWGyKwKIz9xZwZ6h/TswuBG4BYEShwL7AaT4ZaPy17mTU382h3BH4MAXisPWnXlKwZl0dUR4vOilH/sb+UDllG1GOhoYcfhfW0WDh5OBZptb/Q9P+JGeh1zUb2SBOPwzAiBECRoDy080AOBQ1Y8MJISIm+s+5BjObAtH0jX/QghgxDyLPTZASHKz+wVkjoG0SgY8vWZJAxMNuzBeQR2cu2lBL5htV5IGJ0CX0eIk4eypGE4ZIzVHSIxLi5MBll7xKkt6vfAFjHtvUD1nGgdASA/4EruE4a/Yh53CQzm80pCwQ5o1dWUjczYNBWP+jsWU12+rtbEVP2tqpvlVT/qsmrGvauyqfHMONN4fgg8LaOaKtO6hRT0SQ5NDmmTxblWIppIM6LBT4mM78QYzfDugor7HAP0vjTv5Jo6R4ihRxKbNLjgmO9OgRWO9Xeud0fm/IrLzxqV9wmYV8LXibWRH7fW6jMi7vvamQ5mIzowFh5YkgOxQwwRLqQNS0cMzxL7C8l1wiOIa0UaLUcO3jlcXlzi+ul13L9PeSegW6FBNjDdvMJAugiszrY4u1zh9rWjSf6UccLIH6oocIyRvFVkmoBDklkZLD+bkGH3gZUf8u89QsEha4Ehcv2EDH3oVZ12g6pfmjIzlb+zjC0N80iymPOE2APwaV/SMRDZI4S0eWdmhChWt1EvaY85kJtatnAxUwQ4EikQ6aDJyJ+lRa5BFjLjrfPHmYSl1Dov8mkVGY6BSIwYSa7fpqwUmRxP1tgQ2gxG7wghJKk1u2wKTqSmDTEo1AI4T4xaGjGloJlZaBugRgEVY/V69giY9phRmiCO+5byVeUlXQOTctlUgVNDvS/oGMo2tr4/yVp+lrLN4TEZJgRiR0C/6bFabcD9JqEQbbDtY0JYUNLwMSGCESTQiZeNad/3ePzwAZ6/cxtv3ruXrlxhRggR9++/hZdun0rAkWQa52CuAsLYXYFgCF1Lm2Q+CUB3BCwWC2x5QFSQ8WmuJzlOBzfLBZtFtQuKkzW71qYymhXWCpTUSDZqli44ZwaiiO9iCawtqBbeK3DcWCc1c9bHOjZshkuDzZh0HJGjByuxihH5akbnCFgCXezyFSshMEI2bYtg2opfHcsmPNXpMBC5bB9jYiBkjzLCoG0WvzZ2MDc2pLz6OxPQIggRwbsUTTiSxI1AClKVFAYDE5E4xQDEqsG6c7COQ0iMTFwymtfXCDXL7dF2xjLUw6B4wCBZmYLSDSHIxC7SkMea545M5uzcN9rXRKKnEFKn8LKuuFm/IoR5PLWRvTLxfVZc7KppdyhNGnGNdsKzUgwBpj1KbyxMHbfVZWC/oG/QdPJhiwnvI+2t5k3ILzO8ByHGHjESQt/DOYfOO7hkR40+qCN5RN9vwYvEmdRMeZ81wsj3d4cwmtPkf6qH1Q6S6vc1VIoJnkrXqt9U+TSCa13mE+UVutF5j9NrJzjutrixuIaTJbDwC5DrcLba4P7DMzw4P8dqtcZmfYF+s8ZCAsw5tXZE4sWXF5c4Wi7lpirZOPH02Bd93zEQKotSdDhaLGrWmvvT6mNOV/OpQ4mVbcPE8ynYy0sn8li5OMd5miiIMfYFH4ns8tDKVbVAqeVMbupg5NJKJm+xm2j956tCuRJwo8ijgFoepOe+S5sJZoJnyukC69WSLIoGUTFIuVEEcC0rkriKEw1NkUFQuXJwaU0qgaiHtNKBGIb5JJLYC0Ko9MpSRim/6aHiThUDJRk5ybqUNy6pWQ6EmF0whvTItNKBhitTSdto4lVwPl0E1zJrBbUsMArKTSO0OQi/n4ROZdmksT/bKSccUNmudtdkwn5yI80ueNbyzNMFfoxJq3d0fITF8gjLDlgcLSVqaNKI9SFiG3qQ2VFz5HSFiwM26zXOHj/C8cLj1vVreHi+FkUF4eHZY3zkgy/J1ZUhh2RQotIi2gDyIp3QMwx7KQI6B9CSwEFM6AB0geGFDmzUbF0QRzeYI7BMnHZMVD3jjU1AIXxYwm37w6PXRSGt+osNVEsBI32MVYaarzUJupzaq3UIqGkJmF0PnBBzNT9LrhDJ3CowALVGcIDEhk6CgWiBO+qSksH2mQHv5UtMaimXnG2kEog5dYRHgA8E9oxITlRYA4N3ptUMACyhpoiB6ArNOkmiEIbmOB7mUQVQHbcUjBIpzrD6tQnh91IjyWfKP1hKJMWPh0dSNJChaGqJkX9RUualrEOQhxQbK3e0nBw/zLMGf1RhQUNfaH9BmQ8k7f4kcgxQ4++TQk1wn4gh7ChgX3n7hLQ8V/YZKn9tebGrrJpR1O8K4WuCKNoyripAZhmm0Y66TTUjt89HwmOrIgt1Q6sClU4Wr1uDNMUEIMJUXqBmHGlijJQHmJ8HHDTM8B4C5x1CLzcdEcM7D6J0pWW/2cB1HYi2iLEHoy/WecFfUSmhGFkoTzzQnPRVbajRvpAFDpEYp/C9UfZkWbrGiqiwuzd0h0BBcvnqZemlEbdunuJbvuVjWK4ewodzcH+OzWqDy8sN1qtLbPse3WKJOyfXcdS9gKOFw9HRAg/PLvD22w/hXEAfIwII222P0Acs9P4/dSmMhiZIey293hcM0zHSpRfkcbRMhdq4DC35zJat320w3mbGXW04LGmZrybee+rVjX69ANh8r8EBYxc5GmQqe2NF0Z7KJddxWUXL1SPL+42NX51XLjJL6y4iBSVnPRAy+geWWAZRLAGQLEpJYrQ47Tyl/UNwALFPbr+clFshBR6DROECOEm6OSgi5LpK74s2OjBYdt0ly08ybjAd1aCNg0SJdJMbYnGmBEoxx6SU9Cj3Vwd5GNhiI59l3XQI7LPc6kBmkgs6SOnWwBSbjUVQHayKtS7ztXUGlgNdj+gkqvXZkId2QUscuQro4aiWldvd2jwWE7G/TfvkxbrbV6UBdRlPC4crGRqc0J5KrVYX4NiLCbho7zhd68eREUOPGHrk61so+QR5T6DtBicLj/fduY37D78Idh0CEx5dXODk+mmqOkR4YFAyUK56DIZAN5HDCAXO2IY5IVp+C9x79R5uP38b/rRDqFWuu4ap4uKunvEpPwX7SAjsKCJqtXEZZ9zVsDFRncrqq99sPqc2iXazo/U4Pc3XLJzK1s0rRyCo22RMt0yEmNxv4jYR25DsX6TupPV1lJQD7H1uPbnEmYiAKEpQFwI4BBwxYX25AoeIzjMWxDjqGBfnF1j1jMX1W+BuCRblgXNeAkIOt1YwgCh4G4WYk/q3GEao8xezdJJaHkMUJuQwUq3CknaWmyHS76TfGi4myktOrj9KWWjQ3kAZipql8eAHT3K8HZU4p9gR2U3OnObb+8h14q3LVBZCGzgyAhpeHEy8TJ4aahx+EoLYIqStGEM7C9i1eZU0BWPg8Xjta/shjCXX0yg0K1y5SP5EY9aQCSd/tzZOow3VPm5Zg9JpI9gUr9nET1Eaw1VRVSMtDwO3k9Xtyu+q+XyWjHmGdwdOjo9xdnaW5JiQNOEOQAwBzBFHiwW2geFFKCeHFNfJlFGjlFru+Z6xXvegpYdfCpe1ONiAYl2jXOJTokCt3G8VX5R7oAStAnJwzwbXW/7OO+vHYIF4eXmBz3/x81isH8L35+hXj3H28BEen62x2kastgT2C3jXoQPjaEG4e/cmbt2+g+Vyif7iQgpNsupqvcLpyXE+OXZAEYxc69cv3KA/RVvlPYm8Qt74zGv/MT03I1K+h2FMzvF0lknYhVsARtd1mjiJuQBC6n9sdDK3q/FceYSrG7GjHJusrq/YhHNbZskKEjbxFjgpGBDS4Vcfk3V1CLEqO12D6hyh55iuVJUyjxYe5+eX2ERgeXoK532q0w0BvaK4rqbrGiUQuIp1zIgcwDGATBAwL+8c+WTlrRa/nILwO8qqgXxYF1niLQDgGCRu2SCUEUjcwsSClrnEAa5HWn5HJ0M3vFOrc7L+LXJClawr1QJCDvP0+k213HBq5UDZZZcglhfO7LHsPBuZyzRkL0zJe4WYYGXhCmqZ0bbBrl3H1bMnhENkt11y2VXrasFVZZ3DYzJYxyezwXAAOgJOjo5wLGZhDAIcIYSAGBjbvkdYOGw3EdER4HxCaDA6JtxcLPH+F5/D6fIIn3v1NZz3ERGEs/NLdMtjMQlKlD1HUn8GXM4T4F3p0+WYsV0FHB2dwHUGfXhAoJGwjIn2tJ6VRRbfm12aovaaYRejM22wPIowJvAj5QWV70a6kV1cyG56TMV5TyzPQ0h/kRl9D3FzGGqKMSLEXogj5wIiEtGPoUt+sY4kQGe6e9gJIX789j186bOfwQdeeA7rs8d49YtfwPGRw7d9/KP42Nd9EF9+/RE++9p9xOUC7BwiOSASyEU4OMClO43JOYD0FgwZO9J7kBlwlAm63nusvVCmwao8ILlqUvx8dYD0fZb7IjBEUjIBT/OEitIiseQR8nCWhMg8K8ILVekbQolmN1rZFpI2UWEKP5oSVJVnYuPXKkqhxSymqpkiHbVybSe02mXW2hQ50HbuCxhXFbn75b5NQuP9s2Y8lvm13DF4Iu0UVKQj6caUUTcEfxUEsiWVfh7QUaq/aD218FwrLFplzPCehs57EBgxBJBc/xuYEEKfnjGwWa0Q+RJMW9DRHnNteeEjQIHRX26w7I7TO03UyDx1Sj4pQ5sfDhhtgJ8lftabzKtAsa6vKC3rxpUYWG22eP3NN9GtH+IIa/i4xbaPIN/huFvgYnuB1eUaMW6AsM7xGF54/nncvnEN9x/ex4YZAYSjzuNyfYnT05O0AYrpakEvNMe2lzBsaLgmahV4BpwHrp04RA8EktAzZhOy15rDbE5GVewYv7xZ52k++DRQ895dNH9ScT8hg46UN1NrQeZD+QIDObD1Iby/Dpae4mWldrAoF2JIwi8FgIMEH5dAhY4Zm7MHeP2Lr+CD738RDx+8jddf+zKuHS/xjR/5IF7+hg/hrcstXn98AXQedHyMqO65McluvghE7sFw6cpzuUktyuY+qpmsyJpOlAkOPrlzySY+0Y04dFTEwHRQNzC4LFrx8CUgWVUM48fl+MkGqBDhnEYOKxNqCHOVUPPJojJlR9ATYyKXb+pRRULep2D4rQ8IGE7/64VR0cEpOGQtFHR9QpbQOibpsuadqrPxcEeXDoKrWC609rJXJMt74WAlw9HFQwAwJuqEzhE6jriJFU7dBl3cYBH7pN1Jdu6ICKCwRc9r9E4cfbwHQa4GDBGbiwu8+cVz3Lh1C8+fnmDz8DFAHpvtFq67Buq6dCsABiWD7WgrFoB+jE7lrUJBZkPxhwDELaPfMo5OT0BLNzgu0VDnnr3PwTBZxg4Bo+iPNPwQJpKDFO5KxxN9o+FdzlthclbA2j1sRcTVNSIAcGK1EJjRB4mlwGItAM7mKjnMIgP5jmL5HmMAUYQ3zl0ktyl0jhHOH+Dh5z+Nb7z+EdD6AvHRq7h0HTYPbmL14Bri6jE4rOBcBHddUhrwEJQRId0bTC6mu4OJEF0yk6AcmAbJxM5MCXM6AUuKA5K2p+Ws8RciRwxXs0r/JJeOJ8kXR4n0k9h3p3KjnKYwkoGcExVCGfhRoxKTpYayhq0CgbROZ+ZRCLkDkpuQMevS11mZYtq8Cw+tyeGIcLcytnCvLhM2RkZZV4sJHAw7OzLWYOd1aBQIU8Up02xWMaEUUKHAFQ8MuHGe1vcnhUOVMBYvbP1XbUPdfm/tywUXGWVlmkc9l1yNY1UFI8GYy7mc4Q8X9NsNlosOjgjOe8QY4QhYOo/lUQfvOxwfA1vnsXVu/0YZYh3JDPQM7iMWnvKdB1Mbs0NgB1qX757l7hJyQxYZWWoPncybGRjeApQ3zTTKGFlycuK1xIAngnMdttsAhDVc3GB9cYnLyx6Lk1PEmIKQM6Xbp4hSQOh+dYmbxwsce6DvEz9mB1yuLvDSi7cBBBCS1WxAZYnKQ5tZ2pdjMVUCqT4/WhK6BbAFsKIkc2aaJO6lYdj75aGwLsEt2tnCk1opuu+WiJ089cCKd9JKGsTnQ6Bl6QEgy3yjNmo7Nb9JY4eUzUNr2cYwCgoerBhYlAvJfTclIgCe0uT1MVlouxDQP76Ptz77O/im28Dy8T30976AM+exvrnA5u4JtucXWJ+t0J1cg+sWIOfBJAdngjeevFgqkLQj3UoR4UFBbqtAlEMotUxIAU1jYFAIcmgFkEvl5n2a7l1ikqQjAxohMYkQPMhkMZaIHBsDmicqSV0klgzOMFMS7c8QG48zPtk5JgeQd3DOyXrGcLsfjB7CnKFFya/dc+bZPjwbvec9eYZtxUFK32Yd2sgrAFWfo+xcGC4Xaa5K6tViuS6vWW8FO2N2GDhYyeA+91sAEbz3cqrEiOTBzuMjp5e4880vgPs1wMl6wTmP5WIBjoyL80ucrbZ4vI74zBffwsNND5YIzRGMbb/Bo7NH+ND7X8LX3b2Jt8/OsQZhy4x1ZPjFUfJTIspKhhx0roEou3zl9BYEjyS0EhJhIaQBD0EIyqJUMNTwjPl2Ew7BzSJ+ghkLHaOaOB9SJqHMp0OwSwCyQWLtptlm1M2HukVE/QviVsMsG3RAXSTM3QrpX5lwZkrEl5MwocFxiDxcpBTHY3OJRVjhehfAvsetmzdwvo74zGdfxee/+FqK4HvjObzvRY/FYpGCk+qVmOaIIUZRfTgCRUakCCeSlnNO4iUMt0qQ3D+cFAsaC4Hlf1UicA7OaKVNRsksQSS3iaQ7J1TBokoMEA9KDiO1OVEwqCItmaE5OC93u6vPm/jZiuFGnijREcJT+l77jRU8Z+JzCor3B2zqsryzQyBVJcfe+q4CV5D4c/smxmiUdke6ejwKeU84wiGbm68E1PTkqkElp2Af8z9Es6oo28SLA4QG62uvU5stUXAl9JjhPQKb9aV8S0Q19Fuw83CcbjAI2226Ni4yOITJq1cV1L3TEcCRJeCZm76cwewNxi9RENldvvN1LIVdMMLjAxGbJO2+9W6tAaylUY52P9EmlWFyfSahAyFse6wv19huLtDFNWLoU1tishYkMAJHbJnzCff5o4e4ebzA9aMO636NHomfPzx7hNMbLwMU0ikxhlsEVEky4nNsDqqqucixkHQDJAoKvQq+YyTfeHG56SvGUZv217BPHisa2qKVOzKbEGRFul0bmVZ7Cr41AbuWj6W7tCstDW2LFT5qvlyOyqlxWG8xhUZIwRkjwL1ckc4EBDYHRAAwmPx3noGwAvUXOKYexxRx58Z1nF1u8YUvfhmvvf4GNoHgTu/gAzdfwGLZI2gMMGHg3uvNNR4aPzHCJSVGBKJjcIjiNpFuq4hCm1IchxRGPIakDE1x6yIiezhycF6UD96BWFwtYtoAJXf2mC0nAGSNDiPFnMjjOPIJSGNSWBnobRMEmOPB4hk7pDgMSDKq9w7eJwsGpwoGN/BaxcUakfLzipbsOgipcdHSJds3AvK17VYufxZwVesK13het8W6Nk8dfE3WLXl3XmYwAYfqTg5XMpx9ESBC13mQdwj9FoEJvDjGndMed45vghCGGAeUOh+3PdarDpuecRk6PDx7jIs3N9hEuSMPhADg8cOHWF+e4eXnbuMzX76Hy9UaW8f48pv38ZGPfxCdXyQtNkrEy98FuPq0BFYHk0TJUBzuprWFbuGwJEL0h2tqnilcgcHnDamMQ2aE5vMQYbpVb/2+ZRJqi6+DX9UnGJDxRZQgOjFt2JP5gjrBJWofgaxsKBgGDfcJA4nIssZzcFHqTCZmPYDNao3OOZw9foxNz7hz9yVc3HuMew8fYbtdoes63O1uARFYep/OMCSyVOAkQFIUTwzpZFLcpPgRpD50zosG1gmxJDAxnChCIieBhwlgEx1StchU95SrO4PFQZHtRMvAkijCEnNhgAZC78T6ImmDXVIyqMaYkAl7Phm3DEMVcEZSKAQJabfX1pQyUgE239SaUmHgEB+5PDDmxRWVxU3hpyUk7StjF+xsU4OBDUx5R9m8r+CvHOwav4OEYLSVFFOQx6lBq8TIJ/2W72R+j8oy71uDXfOUFk4PdGj8bIb3Jmy3a3ihk4gRHsmk2VGKK9VvI6LrwDgGOA4ntVOKLB42NgBh4buMJy16wzXOtvgyYRzPoMLlq+Bhwcexoz9FI8zPHWkT/yrlAlv2lIJBq7HuBHrA5IS3ETO432J7uULsL8AcENEh+oUoM4RHpgAaYI5Yrc5x5I5w9/opHl+sEeU2s4dn51geHyNfk242+QVdM3zQSyPzhhVm/kTG1Lh2SrZZlA1HDDy+fwl/tER3vSsC2NUK6NZcTg25ylyl3LQnUwN07EfPaEznp+RvESMmYR8r4+r7wQdkjWd5v2DLlMRs/0QUTUEdk0kK68abRfYjVRwyNusVFp3HgwcPwfC4eft5nPcP8eaDC2y2K8B1uO1OsQ0BFAICgEjJBZfyVXZiGav7GRbZkpDjGjCS+zlQrQk7QgyopStCBDuxgFV6hlRHuh4yKeoiUeorpas2Uz/LAz8AKT5FMajJYpbV0lbc3zMCm3tX02GXxCSRQy6ARBYdFAzOmc0uGbl0qDLjsuIWMdJ19rJB8RM4PkUP7bqzcRMiY+dh9T6w60JJnunaQdBSMADD8NYLpFYU2PpHZXGZ7gqk4cpwsJLh/NoN2aQnTGCXTpq7bgHujkAIKQAKqQYqaQMvN4+xXm9xvFzg+sLjqAMcRYCGICjReZxvN3h49hjPv+/9uHntGPcu12Be4A8+9zq+9UHAtec84mYF3y1kcU6AWYCqwcz+umaz5Cl13gGiyQNAhKPjlGgdgI1B6JZAbYVUrQuNtNjx/NBnU+XlzX8joOSujdTo+e4hzf0dCSOt8kRvAGkfIiNst6AYQUGYLzukK34YjiN6jggckwkiM/QsIo2t+pqJgiHH5vBgUHJtoOTDlioH+n6L9eoSjAjnHI6WHbw/xbVrjMdnFwAoaYVDD4oBnTiDRWY47wZ7R8egGLNWVfuX8IoBhHQtJFy2sgAcHDkJUiPXFsnOpyeW4KiQu5R1/gYKR8LMVDHhmICQbk4e7g9OlDXd1Z4INpx8iqLBSXyKpABJAXU8idCnWmOzJnL/lNmhFCqy0CcClHa3UDI0kM4SsZa8U5TfeF8jmZ6GkdS7S3HR+j4Fk1rwHZmnBMBDGIo1Z50U1BsFPCnvy0Ka4hxNjPOBMEVb9N1V6Jjmt5YCAHL8lpyGyjwFs7QCyQF1NnHNICuVP/eWQ7hav2f46oO4WYM6D++9nA5SsnDs19hstumUkZbYYAH4DfwxcqC3mqZlmUNwOG4jvAr7O/IABvdoeJefc0k79LluMEebzB39zZtlCzuJsHlsFkgd2Cxawb1yZ1ILy8J0WspN/N/wI3UvN+kWADwi3HaDLmzAvIbHFimkEoMco/OUNh8M+EUH51I8pCMEuIszfPDOTdx7+z62PSFSh4t1gF8epevGkPKqAh1AOoHVPlBpSV4NRRoPAAsanul7L3+8YfQrRrfEGBdqmnbAxklltMglPhKM4mNH/l1lW9hnLVi36RA6PAXF2jhw00eS0bbT7sfsOtJYY3mvT9mTIFmW6IYd+plKSOuawX3AanWZ1naI8H6JZXeE5bUNthdn2IIBjuiRXCwW0LhbIR2uwYNdBLMvlIsaTBYuudJEEDgmq1kN1VVuGtn0l+XGMSeHspz7QZTkUD1kSvoAB5JglexiCsYOpACSHJO1RGucDWNmEJi9yOvdEDBVLGZJBErnCc67vBnW4P+6LhRvdaJ0LlXxMkwwbEgHsNl8TcWLKfDS0JK8R9R0Rh62+PY05zotGUG/79oz1jS8lq8OEd7q+gowdOGdPLc6WMnw6O7XZXMhjWIKJG0YwHAxgmMPkqtRHBjYMt64XOG1V+7hg3ev4+UXnod3BO+TssKRB7wHdwv0rsPltsfpjeu4deMmurfOsAmE115/hE996nM4uf0hvPrmm7j98kexCYw+BITQy19IbhzAoOVzco8FB/TbHkfLBW7fOsVy4bAAgUOAA2PZHYG3EYgOi+UCHTtsmVMAQI/BpF2gQBYhfFy/5PGEthBLf7QE06sI/zVhz8/qQmgP0ecGIh/QtkLjmOhq9sfyDPQXl7i8/xZOjjwevH0fXedxtDhKARWRTLgIwMJ3cMtjbOEQXIcAB47JpcY4CEDNNxy7pEGl5LMWowdH8X/lDTabNbb9Fg8fPsBieQ23X3oJtwPjwSOP48Ux1usNYr9G2G7gxLUhEcVkwsZObrOAT1rdYoNjLS2S8x67FC2YmJN2Wi0JfNrghxjQUYcYQ9Iey3+qOR6YWmJwinoxJE22K+7MFELvYnIREYsF8hJUEqJoUGIsXEmVDOgGvGv5TTqu5l9N9AXnHQ/CacHEG874ampXyZtFPotCU2CFFm1zQHv9XBUmtcaYFqxGCc3Xq7Qn96l6WI+PCq+NKndCooOSx9Ac1t8TeeznVeFJmFYmIdq+CXp1yPjuOl2t67NgfXYPrUtiEg849LTIOMO7BktH6SYJsOx20wk4hx6OGMfHSwReYrPi5EuNAUe4+nRu4IMuAsx6ejdstO0BoUUbe4d9FooVx+R3TRss1Dg8BZPvdmRqLa28EdBNAAY5oCMkq0Ul/k4CzIUtwmYtpueJUUVyiOxkLQmPJSeusmngHAPXwgrL/gH6zX1QeAzPGyy6JWi5AC+Bfs2gbZSAci7FD/MORwTcXXa4dfMEn7u2wKPHW0RaoucNeiL4RQomnU9HgSLeVJbf7Jw0xt5BYsgYyPRhE3F5HnB07QTLY4ctBsX5ofS2nh7dLNmNWk2Dm7T0HaRVe2n0ge+zTFspGvKZjiQIq15uhIkS94KGaxkpxaxikOx60g0iuv7y5tKnTX1PQHAOgZPlbQgpGH2ynk0usIyA7XaDsN3g/PEj+MUxrj93A9dvnuLo7AEWS4/Vaos+bBE5wEssrygUg0XLwao11FhZUDxL+6wcJyjz8MGJGAAG0TRhgJNSiJEOsiSgo/aPYnJbUEwdhpQG8ZI1Bt5ICilnRfaESSj0kofy/DixPta5SGtgaL+uFb2JhUzxJtRaSRvlH+2CI1zN7NwNeO+4Hfsl19OSw54S7NxleoKyj6jSZF5Qv9SvjT3nVFnFF66eHwpXGO+DlQybG7eg6i82alwOafF4AJH7dG1N2Ca/eM+4x/fxuQdbHHdrvHg3IZgjnzdgMTICHLbk8XizQc+Mm7duYOk79NuAsHV47dW38aVXvojPfuENuFe+jNU2+Sal6Kvpz0sAEQDp3mPxUVp0DktPuH29w52PvR/Xbyyw8IzNaoV+E3F0ehvUE7bcYcmn6OIS6Bl9JMB36BZLBOcRyAF+fNaZCZ0IDTZycBogDGY+5p1FltFVbIdOCsqNl623JWTvKlfbVPv0TD1vBVsb1cOADxGXjx5g/daXcev527h441UQR8Rrpzg+Pk5+WQ5YesLRyTFcd4reXcclPNZIgRaDKBnUIiAtquGe8WQVM5izETPChtBve8TAePv+A5ycbPH+jxzjeLPF0cLj5OQa7q1X6Ncb9Jvkz8lqXRNj2vBLLIZUXcyLneo5BjJxZWaEmPw6U4BIJ7jBQgiSX266KziVESKD1R4tDiOopz0aCzhSTCzBEYipaIea23lRLOSYDFQRc1LrEpMXyFF9tfYsrAhly/ireg7L4MkIYijxjMzDKAyBGDnmQ6zyWCG9BSMiafowmXYKWgLLIfkOKLYWkKZA3xdKjopO2LG17Tuknbb84SrTAzKaOppMkId5rNs19fsQsLQjuzvIo2if7ym8bmsLdHNQG4HZYVLaPlWOrgFVvI3qnuE9BYvOY73ZoN/0SdD2HpsQ0YcezIzT0+u4vNzg4qLHyekWnQrvRsmU8UeEZr22mcUH2QrNhSxgnhVrnIdntRwxyv8MpeGmmxEa+C398ZER+wDmLUKI4D4A6y1ivwU2K/DqAtiugH4N7teI/SVWqzOEGPDi+57D8bEX68BOtiEBSfHgEy8lDxLV9un6DP/Pb7mD/oMOLqzR+eSKEtFhhQU+8+rb+J1X3gKhS8EdOSl5NpsNoiMssMUHX3oOn3v4RUTfYcsOj84u4dwSxBEdMQJRDoi2S7mrChYdG8UDMvmUhnsWK08GFgsH36U6AhIt3RfjYwpqC7CWTPhOwbPim00wckqBe4ZfewC0iXj7S2/gxtECb7/1OhwIi8UCi8VCZCNKyqrFERbHR3CLIzi/BHs3Kt8D4vIKhEgyNw4B6VazHpRcczig36zB3OPhw/s4unYdL3zgg1iuCdeWS3QLj7f6h+CwheOAZdchdh4xHcGmdeO80IsUaDJdKJaUDgyJmxBShDKWvY6Fgj8pXYmVtM6pfBXCoqPk7oskjyZ3Hw0oPlx9XpOSwSKGC0RThSlHRvQxM2pHnHA63a8prihyGCafjpK1brJ8csX1u+raAWBQTuQ+6vgN/Z8ifVauqq1h7MY9y7MwdFV/u3H6Kdi3Hlp7rCkZFrveKR9QmfzAdj0TuEJhBysZmF1G/DwpBIR850gir8yMHimgSQzABkussUBgAjgF+EtXA6YNGDuHbQTO1xu8dX6Jty8ucXrrVmLG6y068li6BV595Ys4u3+Gx5sHCPAJ+TQIIIDFoksm6gRcXq6w3WzhiHBydITrRw4n6x5HFxHXri3QIQLrNd7+4pt4FJd4fLZCtzjG+9//9Ti+cQPx8QW4ZzjfgRcnWEePzdEtHN19Cd3p9WFjZAY6WwgZZgOg2OwXsiqN39fpDoaqDJr6sWd1TC2O+pQ340Ad5c1IPF4ydjFiwVss/RZLt8XSh6RrDWtsVwFgRucItCAcX/M46iLOOeIyxkwUkunW4AM21NdltwCSpZs2yIyVQw4AtVwcg6PD5eUFLs/PsFh0CGGF45MFuqMOHHv0/TYF5QHy/eg6k1wMHskvll+qlRUfUSGmgTgpJhKVTfMch1shjHdE0vYGq9QxvnDsoGYEjDQWCATXdZmw6K0T2gaKBHI8aIid+K35QfDVyNYkEpK3RMoItcNaHyTb7EIhpz2t099a0VCMHpVpi3owRlPr8+7Md2AwaX0iAjqxHq5aVmu92CrylWcYFDetfo7K27Fp3cWQTNaSQVULfCS4VeXUAuvU5qJq7uj7PrA+zeOX8s6aK/MB5UvilvkkV58tpUVTSK8HjMs21fM2w3sP9GYJACn+FAHbbUgulWAslwtcd0fA0XXwtWNsEEHwRRyVfOIqOOEjgMDYbDZwR11hGj/l+Wlxrk7SXHN7hMxdNOQq+9pdgjNfXuLeH/we4qM3EDaP4OIaS+qxwBYIW1DfAz3SSXBMG6eH5w/w8PFjvHz9j8BvYjqAAEGPFbwjMDuEPqJbLLKw3wH4xg93AN8Fokuuk8zoNz1WW4e3Hz8GoYdDhy2zHGZFBALunz3GrTvX8fILz+HoM1/CZWRsA+Nzn38T/9f/9Uew4IATCqDYoZfO6XxGo1hV/prpgJ6uRrm+MoqVUwR6ThbyxID3hO7UA12KHaUmgQTkGwcyRENyXEWTZeIyLsVpej45eVegVVbBdUjRWv5OV8BWRpUrGTl2Dmv9NfHWMd9ucXH/Ddx98TYu778GAmPRLXFyegoHwnLh4Z3DzeNb8I7QuwVWPs0lB0jsBVEABUD8p8WcnzPvdhKfBRHot1uEfovOu7RfQcRmu8bm8gKLjhDCBrdvXIM7OYZ3QNc5BJ8CMg5ypcoGRtaMEmycGRGMEPs0XJbf6NAVshRleTIVPDg76JXoBFK7BskzCPGsQSkoZmRKMrc2kofCspZEZEOXYqlRQIqPFiLYOTAHEBycA3oCCC5b+KQDM8F5EJyL6Qp6QbTAgAtsBD+XXVwoNXi4EICAFJx9LK+o/ADI2jXyGBsctPsyC7vo7wiMoKfpxPNlJLva7/ssPydlsL0NKpvWkuEOyTdKegUh+WAlA4C8wUvIrVfjOTm9ZTjyiBwQINH+PRDFPMg7j4X36MMWZ5crrBGx7BjHISRLBl7gwUWPV+89wt0X3oeTk2u4XG1w/doS3/yRr8fi9Bhv3X8E9FuAjgB2YMdQ1W+MCdERYtIcIiaznRBSLIB+BRcucI1OwAScb3u8/tpbeOP1R/js576AGycn+LaPfwuIAz712/8T280Kz925he/4ru9Cd3IXl4s74O4Ii5NTbInytSmtcd83GYdsIp4U6iA/lr7X1zGh9bPaADoW15cIhD6KGWBS5jANAVKsj6IuXgJh4R1OTxbYuICF73G8JHBMMQvyhjsCfR9w7803cWO9AZ92iNQhek5RdcWVQa0BAOGvtIFzDgy1jHGJoBElPIg9iICPf/zjuP/gEbgP6JzD9ZMTgAmPzx8BYZ2IeEjE0O4j1JAsqxMqgq6MjzSqbojDGuGE906cZ1OAyjhsmnIpBBjPN9FfC4GX24YJWYmgbYlxuAIzUhRGLsoMMDQ4ChHgAjIDgM6NA1gIPlFEJJLriDhfsVX4/RpnNSZpoiNjb4iBkFucaiwK62K0S/hVsPZDRZPM76dZQ9WS2due5ntuPEO5rpwRuLTN2TUApi9X3KROjWHBVPYIh/XrXcqHVoZW+mY3rNB5lUkz48Y4bIxaNw/lMur8XCWYLLT8Xkf4f5a0fIavPGw3yb0yxQMg9H2Pi8sLwDsslkvE8wuse4/QLbG+vIBbYogvACT8ViFW8MNFucmqY5CVuEg25wfICMWSMRusVsYpHGwJy7p5OhR20ch+s8bFm6/j/Au/gy9+9rdx9/YSL71wGx0xYgxYna9xdrbBxWqLVc/wx8f41j/+/8DND38Qq5NTbEhievkOAREcAzwRPBH6EOE7gjMb8oBjoScd1udrPL7/Nk5AWC6uoXNIboPs0EFcDYkQnMOD9Rq9I3SO8dzd23jwxiNE7/Dqlx7grdcv8HXXznF5vsLr9zc4uXUXt1+8BXIOfUzxmfpNTLdQyamvQ5KHYuyB0GPhPDqXlAmLrgMzsEDKHyNjueiwdA695NfrMkMU3kjlBrSg0ZaxGPOF2urrnYCi/kPS7vjdBKHXupYCyVqKSH215vSCvHkYYsDS9TjyjNMlpTGMG/RrUQZtk9vqtgvwcPCLEzgv0pcf1pWTAmNIFYQgV1YiIsa08U1uDBHb1Rq82YJjxAc++HW4uFzBeWC5XOBaWGLbR7z98BE8KMX1ciony/WVeXY5K6sAADG5e2TLPXLZGtreTJlpQxbNKONjKgfpJgqVtzRT5osV0qiGAxBFSFL4adCDHCvMWlOoEBwBch5wUawkWNzOklTNnoYYCk7kbDlsTjEx0u0XTiMxSqOiaGQpDn1niCJFkrESXcEXK6dneqUnpQ1iV1ts7RItDsHjLF/QUHV9UAsgB7JVrfOobNr586D27m3oVwAOVjL0sS83rJwikzJLcCQ1ARLbmYgI7zs5DUjX56lJUB8Cego4OVqCFkdYBcZFD/QPL0Gf/zK++dptRAcEXmO5CHjfB26iOzrC7/7fSbstjkzpZFjXU98nM3JoVFSlTAnNOURcXlyAcQJggeXxTXzgY3eAGw9xefwyThYe3e0X8daXXkWPG+j7LS4ePcZzN6/hPK6x2j6GC+t0qg5qBmRqAlVrWB/vwYwnnf+8PNW4ZE89ma7JF3snLZAWNwfGo9cf4Y0vvYajpcOdO9dxenoC5xcgpNtGvEsbfI6My/UG/miZYlwQ4BYODilSd+cI29Dj6OQY/aZPV+yA0W83IA54fP4Yp8fPw3UQy4CY/sCDkkEJi9zeQGJW6bwDuUWysNmuwTGAOeLOnVu4XK1w/eYNnG0i3nrwAP32PEXrFW20I2QqlpgWycaPCsKUh5SQFRuD5sCBeFA05Az5/TAhRRBNSuPO0aZlUCw9dsk5sdpgIGzA5BCiBwUjiCSfCUDWHMDCrJOwlmJNkNybLEoP2dmSaCGYfKEpsGiUCTJBFA1Il8Ro7zLhNzjmhs2zCuSWSUZbrgE98Wvt/wh44jXSgqKPz7DselNcl21pQ/28tXE/tF1Po3w5dL9tYZSuwTQngzbuIKJ12kMVDJPtwqDYuUoeQNx9uFII1X2a4T0Ni8UJmLdi3k4pELA7QoiMPlBybSMH546wPDpCD19u/BVcwhXPQCcI6xxhsXCFSyW7PbjT2jwOe5OCh+zKv6uSfbhb08XavQhItPri7BK//b8/jXt/8Cnc/9L/jaNlwGKZeCQ7h22I2PSMbc9YbSNuvfAiPv7/+nO48fILeLBeIQUuTiHgAveIHNGRg3eUgtN1bhDWKSnmKaYN/hv3v4Tf/Z0v4bZjfPs3fzMcd+iIsI0MkIN3HeA68NIjHp1gK1aQL734HD7z+gNw73H24AKf+fRncefGLTy8/wiv/ME9nNx6CctXbuBss8Z6vcF222MbJHAzR4Rtj74PAAKWjnC8ILxw9xQf/9gHEbHB8a0b2K4CotwD7Z3HIp6g30aEntF1C4RuCYcOoA58vEjBL4maY09m4DPK1XN8BSXulDJ5Cpr0VJ/voKtNRdaonUMAQyJzuODkLEMUDZqFoIEuCd3SY+kdOopYdsk9Bn6BJJMxgOR2cP+tt7BYM24d30R3CmxN0EA9oE99kU1uTLIZQqIFSWBKh5lxs4bed/bc3dvg+/dx4/Qazi7WePhwjcvLS2xXawRaY7NZIfQ9IjEifDGZERB/ATVN4RwTyzs57BrtN/T03gwYDRv5tEZcslpg3Zal1ctchXP8/7P3X0+WJFmaJ/ZTYmaXOQuSkayyeHXX8J0lwMjuArK7gAx2RfCGJ+AF/yAEAgie8AbByi5mlsz0TE91d3VVdmVmJQnq7BIjqnrwcFTt2r3uHhlZ1bMzJVIqEuHuRtXMVA/T73xHyKTpe/060kQkMjo3b8gwXJnCD7MilKhIhKntYrBqV0eVdckYrEv59XqSkcwnVspmZnlqlMdsdEycI0nKKfH6rDJ5jWUN7CBTZDoY3f3bHxrPv097aBq+bdHj3pu+48T8fWyP+2zEh2zuh47/tvbOQYbjFyTFQUnKnmpSBhXliZgwxCjETM0mSTAmAJEYIoNNXN1uaLuBedPQW0cCrrYdN7sOV9UkhD51LE6XVM08T/LIAV1Sxs7EkEhZYacUISacUYbfmkRdWRbzGdZ5gjhs5VktFzx+MmNx8Yyz03Men52z/lHLD/7+f4EdNly9/GtktsC1a6oYMEaIjhHCdfwF7hsnhdTwuALMt7XvoCcOzyt+eB4NUwKpt3U0+6WHUcASeEhC7NfI5iXdTcvlrSeenTJrlrmKQkl9SQx9YheE1ZP3sfVZzrJKGIlU1uLRdfvGWxpfE5JWKancDEwi4jAV2Aq81XGVxGqpqZRrBqf9NJAMoYokhuhwBJwRdptbUhgwEglxwDjou47tdkcXAtvtjl6EGmG73RCHAeeU6jnBRCvafc3ckpIA48uyRaiKIM5gU8zBDbSfMY3KUQoTWJbPKZVgmOQgh+yZckVIcZqPogJX37UqF7Epz7SiVHLAIoHLHCLOOkpuRERJsGxON0oUkJ4+oJYjcnnMTKzYMapSnnvyfhzTwPP+sOPhZvanHo/DUh962v5tOG3/thzBY4F84IRyWFbpXYwwc/Tzd2nf5dzfV6nee/+ji05l0kHnSm7leCJ38Nt/29/N3DNGx34ej8XJH+U7lrl2gGySfzvv8Y/tf9n2/OvfMmuWYDzXN7cMkliuTnG+ZrOJJAz1asV2DZjEbJaDxuUCZYykfZABgb4d6LuO+mR2kJ5jjlMOj1qRndNrT9u7VKWZln6b7i58A29zDo8utXfCJucVmff66pK/+PWnvP78SySA7CyJRO1gMa8xXh2O5BLbocdS09UL6vOnsL6FylOWe61EJYvEkix5MSEHG8jYP2Py6qdlY9e86Rti3DAEg8XR1A3bNhPPYcFY+hhZtx1XQ+BxXXP+3oLK/w2YhDGJ9e0rNje/5dn5OdcXhr/67C/ZDhUBS8jegJDz+7EMvXI6mZQ4XdbYJrLDsbSnLJuE6W744q9/xctv3rCcX/Ds2UdI03Bzc6X2rHcka3D1gk30sHrG6sMfY+cNcERK98BHmorLO/Jrevo9Aa3Ewyk779KmgQ43CX5N7/lQv48DKHGIvPnmFf1mzelywfJkRV3XGCzisvcsgomJbkgk56jnNR6oLJhMPG8MxBip6xrIQa4YCVFLltK1gCJkTUYz2OxaFIfcGEhR0SjKkp2wknThKxksEYkBSZEkAed08We7WbNdb+iHwK5tiRn9srld027XUM1I1pMzXCnksoW3oBjdxS6TmN9hDgKYzDtXyB+xguQ8GiO6kEQ5xoITk6u1FQ67mMkg73Eui+8m+yCCAfXdSNm10yCFQpjLB9SS7cA4YMf0BRIm5oBAHmwGpwEECSSxRKdoBxvJeb0Ga51ynBlwZfHPumwr6N8jwqW8C9F7SI5rjHaq2Y+3h3z56bv4Lsiub70Yh+P8QVn7e93029tB8GnaHhAWwj2EmOVa36Ivjtt3TpcQEV0NFcbfEfIg08Fp8sQ21urqadKOOW9wzmcCH09Mhm6IYAO7kDBGGNZbvvzmFcbVuGrG9Vb44vkN5xcVXclbz5HJgh9KKDQnhZy3nx2mQg4JlrryzBdzjYqnQN9f8+qLb/jFLz5lNwgffvh9Xq0u+Ozz59xc95ydzFksdvxQ5go5QsZgWJZ16tRxmDM+visOx9t3Nfp/X8N6vPfEMH7QCDaTgXOPQ2SMUPvEe49nuCBU3uFcwMRb+n7IFQwMIQ1YV9P4BZXdZ34lKYGhAEn/GSLWGvq+BWNo5nO89wxicFWFryqS9VopwuhYizGRghBiIMWoJSiTKouUiRpL7uzQbolDSwjdWPs8hIEh9FjnSVbLVSZJur3fgXOEqda1ZiKxSvqEGi3KOWExcU9GU/qAJB3zKY2KROVzQMb1eUU8FKIni4wfXozknLyIshhr05U1Nar2wYq9dMrk29kwVaUVjWSdkwUzMQeiZCKJYw6eZAbgSWrEGFgo4+MIdiYFMfO7tMmgPDbSf692T3/+bQUt5L4/jg2td7zW2ze8pf0Bebd3HmuyOgWMzzJd0Tp2mAoM/Y6RdM+97jMsHgow3Lvvbd/hSLa+CyHlH9u/561eEeycthO2/YwuJm4HoWt3vHpzy20b+PCTEz764YcsFk+JRwUgj8VAydv3xlFXDmv3WdBTB+2hdu9wmgQdxv1TBPMR/PZAr3/bpPmWNt5a7m4fUqSPkU4MyekikcSIWI8xNXV2rqig9jNsXTHEwC4M+MoTcl63tVkvOvXEEiDOkSh0kCW6kR1+8QyuZrA1Q+owVojDjna3o+08vpmDtYjxDJLYRMOLdYc/TSxPVxjpQGDZzHhyUdO4HSezE5Z1wvQtqQskW6lutTIGGJQuKWBTwMnA3CSa1NMkizdbKl8ReuGrL97wb/71rzg7eUT/M8d2u+Zf/9mfMQwthp73Hi34X/9n/wmz+SmDd9j0PaDRcXLkfBzwgGWTwY0oyEng6J7vZo90kjxw7Du3SZBpSvC87+A7XwZQNHJY39K+fA7OIKsFq9UKMU5RnE4XTFKKrNtEdXbOvH6knF5prB2Rq/1ETVmpamKIGOtYzFdYA20y40KNYb8QONoxXjckb4ixPIYFUTS2lYQ1HkRLoIehxxjDrKkxGFICZyuwDjEBEQg5GAWW5MJYgaE0l0uRl7QJEYiiK/42I2SB7HMUIkszLu6V9FeMpq+XBV9JaV8qXXL6ccpIhnKvbHia/LsApJjRtQUrkTuVDgMMBjMGR0qZeSCjBdV2TRZF2zrDvua41YUw0pgiRHkH+QIiKE4s6UfKs32/SGDNHduzlGcfx9+3jO/v6jC/Uzvu08O7/hdpZvLvbfun7W2mzNuudV/7DsSPJZLFOIjH0nuwH4h5OyZXjoilNkAOSCB5supIMVYFiPMeEIaUuNntOF0uSX7G9Tbyz//FLzk/O+d2G+i6QCJqaRmrgzNJIqVE3+pk994Rh6BQOyKNsfRzQ9+23F61WAuV1Dw9dfz8R4/xvubkZMkQeoazHXW4xmEYNlu63QUSE3GI+JTuOOujIVHe02T/dLs7Ou+t7zr/+1YZ/VDkoExC2f+bDpqD/h8Z8PddzwJWEk3lqXyVOS8EZ60qdaOMtUjQXDafsKTcf0FSRHP9I7XT9JXKa2DCW838cjbhnJCiybnrgkf2StVogEos1DgNDAyRGCMxk/VoKaBESoEYutyfnqHv8D5XqogJ5W1wOFOBQN/39F0HvlZcQCzfoERdyeQ2pSylzcGDjNLJOZQaZIijYE8lYpzhaZIjU2UeTIVpefFlW0qRMXpcvpsxmaG3WJFllGgfxo9sjRpiUbB5qUxAc+FyfpEii1Rc2HG5YaIgpmxLUyk1OfT38m2Pjd6pAfT7XPg7SvGHptC9l5xeO59UUBhTg23UtcI+ZektQYgH27sowN/jOzwgOu78/S7dvbP/SHjd+xzFULnHAXoocDXKsfsM6XuiCfcqxLe813E4vmNwodzX8HDk/4/tD6edv/99ri+3/Pbzr/j6+Wve3Gzp+kRM4Koli9On/ODH/wGzs3Ok8ncRikdjYJ82H8dSxsrj8/AYzyfoj2Iy3Xcbsw8g7H/KCJtUG36iQyf3FFOokvUiY6WViQx7qHtT8MV0mtvR8VFnwgLibT5Hb269wxhdGXVOWfolqE0nmXUvAiYZPb6kDRh1MpKU0np683Hl2VmMsboiapUfYr3Z0smcgCOkitkcsJZtdHz+8orgZvz07Bnz5ZJut+H0fMEPf/g+j58sMTZgbUCybZlEkOzoIEkDKEmdWSMJL5HTxYyFd3jT44iQGqxd8oOf/GNmq5/SLJYsz06Jr9/wwd9Z0G932LhlzhXLxZyd7Eh02MzRdN/7n8rKaTpscawM+4DDnXMf+P13VSDHDtqxKn8XZ+TQlhaWTc3ybIlPPYu5hbBliIkhDNS+UkSLdTjTUBmzt7ElQSpIg4gzgvcasBpihzHg/QJnIUSTOduOxm/uUF6/ySkLMiJbbSYatMYqn0MKeKulWI1EKufwzZw4vMQYq5XQMn9Z37fEocc5TxKb01X3pOFi93Zame9JSRWyvZfGdyplnmHYc29pdb3kbE6LKCTkOTgwRnIkL06V7fu0hxFZkTckSWMlwdF+leL3QUG7jimDMj7B5KNmA8UW4vLygGmin3NwodjZBqzfL3Y5a8dnLjcb18GOfu75Kbg3EHqfzP297M372rcMenP0x3eyQX/H7rwtmPLO1/4d39M7BxmsBZE9NBzMGNBShadxKx2jgrOOnA4HFoYYCVGovFdIT27GGF2BTjq7U4ps2x0nJyfYesbQDay3whC3XF1vGYYhQ+cjzmtOT0yRoR9odzussTjn6NqW2le4dM4Mz7CsefX8OX/9i9/w6OKUjz/4hOs3L7l985qPPvqYmesI2w0n1Y6h2rFYntANMz775acsVme83Fzx/Q/+LjNJhL16Psjvnfpk72LAP9Te5bgyVw/uXc7N260ILgsJFZQFgZLfO0V4vF0BqW8pOASbIpvtmqquwVc4p2kxdaVEN0kSs6bGWzOW7nKiQQrvHaenK9q2Y1Z5BEOqK0LUqKkzykCb0kDoW4aDek46mDQ4oALIVZbkDDEZQoiEqKgG6RMh9KQ4KNjFCrWtuNxsERGGYaDvAogGEWIYCLGnTjFzhzAx7JRAVETTNhQalgV5Fu7j9x7D0BwIcDP+ngMPxx94Ig0FDQBoACOfXwSrsgVlzhEyEZJCSPMHRyPZGoRI6L2TyWSYkslQjRpqJNEVAwwuwwwRkBJ0OHb68ly+z5k+GDiHlsPDxvS3Ca1/2xCyB27z4Py7R2mVOWTuO8zsf965ZlGwZbjcp/zu6ds93dgvDhyff09/y/n3Oi3seTDu68vb2n0y7953MvljX9/76Fjz8NAw991oesPpfilG2eE9yns/OChf/K3PeTSuj5t9YPsf2x9OG8yKl9dX/Nlffsbtpsf4GdY3mKrCNgvMbIWdrxDrifljjwsIeewVZ2WcuwL90B2klU0h6m8bMlPE5DisJ+PZIoQh0O12xCFkwmPBEmkay+linldBEzEEYlL8tfUV0VYk63C+AudGu+BtLfv2d5qF7BQkMCk/n66k6mKClu80eXsSySkQgJSAuupIgXG1MxmbiYp1nlrnskuiK6AxOzbeuoyajVgjBEn0w0ByC2KIRGO57Xq6aNklT7vpcJe3LF6+wc6XhN0WX9WcnJ/h68y94UCMqO43QhTR6hjl2UiZzDnibOTx2ZJnjx/z+sVvqAl4ibTdjtvrl/R9x8XjFRJ3ROk4e/QId1Ezrzy+f05VWfq+J6LYxMhEr9wn7x6IQoxoqqlyKvvuuYx7SJZ+hzbVdwd8S7LX/W8T2bDXO5W3VPMG2wesRKIxOCsEEikNigZNFlvpWxKT195j0tV3hFnlic5Qe8XoO6ffyruo3FTJYiTisy0WDfsFudxZSWqH6aKOOusxhREFmmLHMOxIscMS8A7ms4pN3wLCECL9EDKyNjEMrY6TA4++3C87/lKc5GPbK4+GPPH3hJFGF43EYIzadiYpwlt9EVH/S1RCJdGFv2gEYmEBT2PwQHkhcqqEYeRDGJERFN9v+j01Zel4bOkjmAMH1yBZRhTjOpOX534ao+Sc1mrAxGsOMdZneMIRcsEc2aNM04Fkb47fGWyTY8bB947tbdKxTLkHjzFv2/n2m/2+iIv77Ky3Hn+P3X4ctHnX9h2QDDoQp4pydJ5yB/ZOGZq+UCLqphSBEZwxSIwauSYrphL9zg+33W4JKeLrhq6PJCxDEIagE6JyHmcd1pUVWoeNgpvP0VIolsZavHdUzjCvKz547yl1NRD6jm67Zbe95fGjc0LbMa88oduxvnrD7uaGm9evCG1g13Zsdx3vfeh4/mbDR0OLl6TOaxK8Y68UjdG1e6vRyz2fTHEM7zfuf99mJr+MviZgYyTttsR+i5GBGAPzxRKsJ5gaW8+ImQdDJuffd30jKFszCVIg9pp+IL4iJmGIkVmuMtL3AzNJGmCglJ4sVSkMi8WC3a5XwZRJXFJSh1dRAOAoWZR5ROXC0iKJKEEXaowKe/WZnXJKuFzuyoDEQIohB74iVVWzWd8SQ0SGiARNsfEzi7dgJIAECtWwMaUid2a8NdNpKuwxWSpWJn7KGKjQNJ6iPSSnP2QI22iVMg6O0Qk05fgSvTbsj97PfIPJnBfaj71Al7yqI1oSKJcFUhSG5hnmtD0smRDS5hSnrOBGQrI8oIxhgiu8ZyBOpet3HOR3TnvgHsebf9e5ZI7+GK/7Owry+/Kdjzs3OqATIXAwoh6Ye/fe7+7l7z3WPrC/BDXujfTfc613fc/Tft0xOmQvZ46fX8zhfcsremBB7v4XcNTZqVw8tiEeQlEW+XcQ5HiXMf37aP8/tn+vmrMrhlCx6yzJzDBSIagD3vc9tu+YJr3Bofxg8lNTJbQ8cO2d5vQfB2mn7R6DeBzq5nBMlx0ShTcvX/HLf/MLhu0WQkQkUJvAB08W/J0ff4JngDQgfUe/a8F4oq/ZxYqbwdDjufjgEx598J6WVLynO0fdutNyajR7kuaEmbD0qbrMes/o/hRjXmQSRTVkQjyRwq+lloDNq7VKkGxy1QiISZ1Mi8kVrwRJQRchvCMmITmIojbGtuvpg6GNAhHebFr881cE4+mj4fq248WrNXW1oJnXGOfHF11WhKNIRi4KmKRVA9CyhvPas6g9crLExR4fLDZsWLhrYt2xNHPWty3ti5fsNj2hT9wmx6Lq4KNP1DaJ0wKDD+ulO/Jpsv2+cXKf9/O3IrYmfbNyKGvHANlkohwP8cO/JaNcIn2/Zdsl6maBNQ5vDEik8p5d11MvHFXjtYJdsRklURnD+ekJm81GH98IlTMMQ1TEgdUSklqyWw6qoo2vS3TeSt6fUtLUhWLHpUTsW/p+yzB0WAveGrokbNZrQkz0QyAMWpViZjXIVjlD5R3WeMWqpD3RZUEIIGhKDowZ4ceVQ4oPpmnKGslR80zGIJOOSrPXuWT+EmPHC0qJbOSU48IHNm4fzdPjwEL5zvvBNQ0zaPcMo02KyYT5uiBXUnKV0DWn6DqLzQTy1jkcWpEQYzDKfnloMDHJ9p2Y0WM/H7JTD8abvop3RSC+zR47fPZvv8Z99tudbZPv/jtV5Hrg3u9y4H027O/a3h3JUMJCU4vayQjNATMqjOwTqsAw2aExaYodxBqjsLgxapcyNC3S9R1d12GswVcekUgMie3tDSkMNHWdB10ayT/6oWcYBhBo6irXqvVEK8jC0W529HXk/fee8ej8hMp7bq7e4AzMZzN2uxZnHKvFCf1Zom6WeFdjTEVOeAKJmLDjm7/+Df2bFzw7q6irxHxZUzdzLbtoPMk4LXVjK1w1Bz8jmlrzFJ0lueJG320P+W/Hx5TPUFZNRg4UsnMeBvrXXyE3X+PShmHY0pyf4Ren+PljrP8A7BLI5Tiz7DmImE1urCsVZcDLqEFCjMQYNAqZDEPbcXt9Q716j6r0FQ0gxKhok7pqMMbQ9wNhGEYnPIaAdxXOOmrvcwTWIE6yYbIfWymfk7JBkzIhjRHBlDxJEtEknLM08xoIOEl4hEoigQFii4SOFHokBVJJdMxv0qBQrVGguywkVRTrdinhtRIssJlkyWRjSAkapUD68nFTBTN93fpoGVmQBTRlrkx/Z1LU0hSBXtJBskGWv52zpfqG8qRY5/Q5rNPtppQA1eGeJmkSY2DR7B3CO+PWTOyYiTFxH6njfUrgXXy58iz3HfddHOGD/t/jaD904rgqMwkUAGPd54OW9t93Ol8f6ux30R0H9qLs3/lD7d7vNd0hDx97j236rX0r5xw/8p3r3DM2xj68Q8DnrcpccrDAHBpqd64/avAHOnPkOL51Ze5vSSn/sf27a0Y8Jhlq53UxRKLCqo06uk5XUI7t3Xt1tAUkJCREuu2O+nT5cBTugSbsxzGGkcOHrKslJV6/eMG/+p/+J1K3hWHAycCijqxPDM/k56T2mjhsWdSe25s1zjUM0XPTWi57x+senv7g5/yn//U/xTj/1i6+LfBgsoFQ9NfYT4ptYsZJp9nt6gZpEMFgMweDoMhBYzwiBrG6mJBiVBTDJJWvVDYrE1xxeVrxrDhP2q+S0ki+q2PX9lxvdjg/o0uOl5c7/vW/+YzafcKz9xfcXm1Y395ys7b0oo7OZqvcTYKiJStnsbEn1pFhe8FNumG1MAzrNW+uv+BmvaWi4tmqYVV1vHnzGfOwYzm3XG3fcH56wXZ9y5e/FtJsTt9ecvIjGeP5By94IkQL+/8dr/0BMTb9Ft/ajq/5lv3FHjQxUqWIEa220cxnRGMYolU+DXP3cke01lm/C5IGYuyJKeEWC2IMxKgoBVfVpBAY+gEf4z7lIafRWJTwnRgxXpP5Jc9fY62iq53DmD2XytiH0seJLWzRcW2x2EJunyAMMnKLNVXNrJ7TB0OMrQaKJOGNLooZGYhBK9O5vNxu81xJeYKnlDIyNaeiJyiptEmOnNBsUEhGj2MyIbhIllF2tLsSSkBeSB0dabQnx7R31HY1+eMWDrpR5x1/9mzAGVP8vmwDHwzYMjj1ZZqxrybbpjKmjlmrAYcRoWyM8jWYYudyAO44+Hk02A9M6m8xKsv59sHVjKODcx+mh09Nv3T0dzntPnvoXebhd7G7/m20+2zB36W9c5DBGXMApxMYB67JAYUSjTN5DVowWh/a7HGxISYV9IUoMg4Y57AmoyRyznjX7WiqGYumQlIAhKvXL4j9QF056top0Z0wlvtR5aXVJSqE03nF6aLGAdeXVzxarTg5PeH09IKzkxPaXUccElVTcXl1yc3NNWEQLi8vmc8GtruO221HszhliIMquBTZXX7Nza//ZxanEWd2mMdnnD97H6xF8HRdYLfrSMbD7JRBGt7sLDs7Z/H0Yx598iNdbT9aAj2GxBxH2Ca+3kjwU3I7NadSBYOVRBU72u0V61ef4cINTW2Qagu2xVU1Jl1gzYyYh0Bx/AwczCK9n0CIY61f5xWyGPoBb4yu0GRUixGhsjk4QCnwozVwJcBmfYuIx3tLGHqQpOkzKRFipF7MMc4wmzcYasbOGcYcMyWwUXEcJZEShGHIVUUGrBNmlcGZNFbNmDU1Q99R+RpvhGVtWa3OWLcdNiVlCpaIaP0oxmQSm1QJjPl1e/PIOafv3qBbCqpFor7CMRijqztWzBgUmRIlydH0PXRKJzlvo1dUAgsTpEMiQ0oPyxhZi5YDdWCdGnImG3QFlmZzOkVBE2EmK+8mo+jy2LBlKk+l5mScln1vs1Mm6ueOQfs2P2/6+/E131Ug33vcAydPo8d3+jUxmsy7lI75jhL6ILcw92W67cCbn/Tnzj2PP4Rw4HD/bbcyLI67BxxalUfH33cdBwfIuXHfuxgFk4yf45UgAHPvTfc9Ptgt43+Hhlcx9vJ289ZR/8f2h9BSGGjbLVUFQz/okAgGZyqcd/gMx7eWCVrxsI36OLOyx75ju75leX4K6NBM7zoHy5yfzNsDkj0Rhq7l9voS+h0MHTb12CayHgbWL8+guyT0G+xizvryCu/ndL3hthUG5vSD4+VXM2TIpMgPPFfpzp0mxaExWv64HDdGSBi9BCULzwH5PI+dMWToY06ltaQR6ehzGqAwBHTF01piilmfWkSccnMZozqQHpcGTOzV2axUN4oqyQx/h75Xh3ixPEWMp4+O9TrR7yxp8LS7gb4fiOIwyZGSVhgY+p6QAiEMGO9wsWc2n0MM/Pbzz5n5gfcu/j5N5UlDS1NbZrXBpB217TmdG5qmJrWW05OG0G95/foSqoHQXWMQrZYQFAkKEYyMelhGqWnG3zVlLmNHitlh98NklP2Hn+2gFX1sJp/tbRDpvOaCC4H18y+Jwy0ubenbDSxXNE+esIsnVKdPtbTg0bXMRFlYUUSCFVFkiM0lEYEQ9Ts5C8Ya+tDDdk3VdzQ5HYdcPSGkwG63oZnVWF+x7Xv6EHDWEqNaWtWsIWGIGY06XVcqLogxBidlgccU7kFtyeFCzayuqazTBRvnaOqaGLJ/0gurxtLMlnQhYUWQWKqT2Qzt1w+VAJvM+N30356sUUxB804/nKar22Rw4jStyCoSQP2gvOibUTcxl52UPEqMSLYphGQFE0u4YcQ/fEvTjo7cEBMbsQQx9quWZS7rM4zcLSaP4DzYyrmj7Z0dIoPBpol9WsZxHuNTpMxdvT3Z9sD4f6s9VG65744uqE3tv31X7g0kvJMlMJHvY3cegKI+dL3RN7zH4H5ITz3UHrLJfhfT8d2RDEhm1t1v07Fuxocr8bBCmCfkFVQrGJPGldPKe7xxiMtKQSKFYEdrDht2247FxRKJkhV2ZHNzg0hkuTjj/WdP2Gw3bNs+Kw2TSSYhxJa5t8y9pbYw9C2bNbx8EXn9fM1qsWA5X/DFb3/L2ckZ682aV69fkVJitTohPhEWs1NiFG43LYvTU9Y0GOshgbeRD95f8cOnBpMcVQUzcwMxp4b1A+12g7eeWtZI9JzaFavZeyTZUcVA8tXhgLzn6x2TQ02Pd+iEtKIR3pKFb02iNoHab6jODStO2V61pGHHo7MlNBWd6TGmR+kBHUHMPvI3nTX5KxvRlANH0GoOCIil33W4uqKuKrq21QCD9+NISNOei/ZXQsBV1Sg8AZxTCFuIA0Pfs3QOKk80eVUlC8QSvVdxqDNQctnHkGokJhg6hrTDpJ52e0O32bC93fL+ex+BJB5dXNBuOxKB88UKiYEuFeGn2Z5YkwlvJEPa9OVIUsIevAa4MIJ3PueIMhpSUVRjppxjmtUKWIV2ppgDC7IP1B3P+PsFwLHblvuVP5aYfWqEwtH2ilxtNxlzrWyZt6JK2qoFuP9eE68/ZwXt0ybe0qaC9YCAauIsToXy8bB/6+WPnc2/DWe5POp9197rtMN5ePQJvrM7+S0nTO83TsfSz/v82LdJ/6N7yZ1f/hdowsEK7Du9sLdYw9/GU6Em5z1O2dExMn68w5vd371jCxkKQYQp3Fmm8N8fX/OP7Q+lffbZp4Rhx3tPz7htPCFoNR7rPa6e4Zpa5SoaXE5FvpYLZIPOisKXvYEgA0Z6amcIed+7DI8SvJoOvWMjNoGWekxaTo+YgyBZX1aVY2iFfrdjqCraXY/10HXCdtMTq4jEmjS0e+fgO7QieqbxBCHPOdG5JHlnQaqWNEdMIoWeGHs051p1pzLEe9XBJqkN6By1tWOahWRC8YSiRofYZ3LJiCXgUoC+B6eLBxogMNl5S2Cy3dAPeOuYz+ZIhBAtMXm2m4HXr96wXt9i6lMNMFlPVTliioQUlRfMJBpqfvazH/H9739EfG+BN1rZoqo9jx89xhpHU8/YbraErqVve/rdjsuXbwit4esXL4jGsloBZoM3ELctn/3iLzlrhJNZoKoGmqbC+IaEJ2Zb07kaW9Xga4JtSG6GuIbB6OAsaY/TheYstvb22cR5GsWhOTxnOibLNovaBj70xOvntK8+xcdb6go215bV6uc5gPAUd4+jM0U3qI2fIAZd1c7/uq5FMDqO40AXW5JEamfxxuBEy38HSblcZmKzuaXyFc1CnX/nnXKHIbRtz7wKVNlRN0fPVH4ZC4yRecxGBzoHurzDA0PXEbsNm92GZrak8pZHF6ds19fMTGJVWWQImKSLaU1Tg3GE7KTrIpmQbOEkyZ5Utj8FDU4osXq285Cx6oOgVddA0wrEOhIR7/LikbU5ZRZSMoSY7UKXUcDkiVs0Yi55WT62jAGDe1Ry4cMYozNTg0n2+neCPAIU/Z6U58TmOU1Uf1GsPruxGnyQQn5pbQ40lK+W+5PMOF4PO3e3TRcbxm/+Fjk8yl+O5scD7T5/7tvs5fsucHzK1LwbH/XYXp1sunPP/CAyufpD19kff9fOepe1nfvaOwcZNPWBvcQSQczeIJMkxDjkAywmJeIgxGFQErsEMezz062RDAPS/HHnPM4bjFX2WIzFugoRizWaMhFiJKUBjGV5siJKzGR8VsmLsrNk00DjDYvFnOVqCZKoa8vTZ+/z+MRzfnYOGH78o58yXyyojOXs5JSN7AhDYLfd0beJ9XrLm+tbHg89X121/CyvSuOEsydnvPfhjOtXPUhkSIFXz18wmy3Y7TqeP3+NdxVPngpiK4yN3LzcUiePMz+96zBwd54U5+4+s9UCaXPD7avPWc7h2XuPsRpWxg4tKa159H5D/f4n/OoXV7x5eYllIA23IMLcnZOaBQwGUkXMCun+CSoYApI6ze2MAbEhwxwTlfOkGHBWjZqQCRcVCpVDT0YFt/eWZlbjK0/btvqAKeFtRbVYEPFU1hKyY66yyRxNCDMiWCRjoZ1ziojxhpgWLJpax2AIbLYbkEQKHY6AIZDCwHZzg3OOmHpSirjKjVPCpMKuW+aojPCyEKM6TSkiaUCco7JeayWLVYVm9F8ikaxyM5RrGatVIgqp1N6OlP33Ph4bJR+hOEF5aVsoVSLQNzYR5uPcFA3+JaOGb842xEpGYqDj2hx50wW5NArkyVi8S16ZBetEMpmj/eWZBB0Xe9X2bm06Bn4He/ju9SYT6z5ZezAdHoqG3PMevgOP0L3tQJhPlNy99yvH3Bd8OD7s+Lh3aL+Lq3wwXuTuvrt9OH7It9z/IQNi+tuBcXxP0EBKYHwMz929tBxddbJzH/DZn6d/F6jGHwMMf6jt9vqSs9WSP/npTzWPf7NT+Wo9QTyxWtLU9ehQl3TFaZM8yJ1AJYlu6FTGGhTdYM2+nOC3GLnxLYcUxFpdVTx5/Bj6NqdL9KzcwI8+WPL+s2dwMSd1T6mqmtOzpwiWzbZndduy7iwyeAZfjUa0Y088+LY2NWoNe16GskJbfmhabcKIxTuvKFYreG+pas9i3iDOkowuaKh/bMA4rHUj8V65mbGOYJVrwYkjisM5i7NKHu2MIDHS7nb0tmO2XBGdpgWWD5QkQkq02zUp9szqGlKgGyK/+puv+PKrr3j95pq+a+nahIij7XpCihgjDGFgGDpm3mFTZPf6a55/9piLlfCP/8GPeXp6zm9+82tiH3n06DHr2w23N7dcX12RonB6csFivuDJo0cYVzEkmC2fEE7OVUengZef/QVX6y84sZcs654f/+SHLFbnRLHs2oHNpiXhsfWSzeB5uUt0/pzHP/g5px98rHIp7eXTOI7KqzwSuweyWvRdHnzr4nyY/aptTcLRMzc7nNvhpYWh53T5iBkDNvZKNnjPOB4RBGQ/II94iT0xDhhgiAMxJuaLBcYYuqFj1jTKz5GU+8sY5T1xHpCoPGzzGb6qGLpOyRsB5y1N5UjDkIOE5m51mKI7pnaGye+ivEghQ/oNMQz0Q0/bbpkt58TQYr2HFAh9x7ATKlsRQqB2juVynoMMaEn2vMAaUhz5u0YUQ9JAAC7l9OCcJowSiBcy8RIMEInEISERsFYXnJzJpKglPVa/haKA8huPKO+BgRQyb9g4UGT/4OP4MNNhNAYVzZ2N5bvakQh2au1mSbE/UfYBCT1qqrtzAEZz7POAniBRJoNr1Mf3tKnZ9jYR/LZr3HfOnb+P34e5x2aeHn+f7WMO0+FLn+61Lyfj1k62Tbtw3N9vs1SObbc9FuW7tXcOMninrojzTnVlSuo4oZUkEKCqQHJkLTm8EZpaa9uKUSIeZ9UJ8ziiMzjrSTES+oEomcE3cyAYq+iELgQq50j5qeumYjaf0/YdQwQRg61q5ssFKQR26+s8OS0BYQiBfgj82Z//JS7c8l//H/63fPzxM/7lv/wFP/zB96is5c//7F+TkuHs9ILbmxvee/Yx51WNrxs+/OQTwuIW7yAR6UPPZtiw66GLAyEJuIYuOWKXuLoduNkGzs9WGF9zcXaBqRawDthlRWUDgfqAZGnqFpbJVWiHSp6hiR12t8H2O2J3y/byS6rbV8yWNVU6p141VIuK69srjHHUi0fU1iExEvqOvt1ivKJHGtYYs6M3EFiS8OMsGB3JPFpNFFLqMbHDmUBKAykFMA6S0HUtJ8slXbdju93glx5rShnLRCQi5BWMFLm6vuTs4jFBAru2ZblYEELP0PcsTh6pQEqiQZPDF3TQ9nwEJsOsMhrBepqqprIea/J4WTRYG1muGhbzmnbr6IYB2zg1OIxQVw6sIyUzBhlSZhgGiKKlOPcKKCJD0rKYLpEG8F4NI2+UFTkiBCX0Jgrg7JhelJS6+EAKlFWGu45RjlwbMMfFsynsEWYygiRHhFUXmRSxg/JaunwjscqmlTKsV6PE+SolsCPo3xm5McrMUVcc9XQqne9xwMfN+fz7hOtUGE93HwQW5Pd358pbPIYuGxidgKnBcW+njpvh3hWg79Lc0d9TVX+sQA4OMkf77ulEplC9s310/o/fxdFLPgi6CHsNPx3D0/PfJahRrJT7jh1xifud9xsFhx2YBhkOcYhFou5tlWPVqVPwIGxxUA0gjtv3AT/JVsS7PO4f27+/7Ve//Dcsl0vOThf85V/+ki+//JqmaajqOfOTRzQnTzh773tYWykEeDLvhH1erk8Jk7S8YRwG5suFktSNxva7NQ93hGRZvLNJiECSwGa9ZthuIAwsvGW5cNxe3TK0A5/+1V8x9BussfzVX/2K80dPMNTsemF58QzxJ8yb5uA2x9Dfh9rUkPYYJXoeYdoxLzZYDFo5THPME2IFl6qsRxISFfuY8v4EinqtKxCLZISrIBirtHbJoEg8REnk8hfwzo2OW9vucCHknEZFHnqXV5INiE30Q8d8MSO0W9oY+Rd//pd8+cXfMMSA9TPq+RliK10QyAq6do5FtWBeVzw+P2NeGfCRIfUMSbh48p46wmJZzebsti0Xp49478l7dLs2O3YVQxwYuo7bbcv1JmCCJwwBcXB2At//4IKL2sFwzdKvsUMPyWO7AdNtsbbGmo5Gaj44vyDUNb5KNAY63q6LiujeS629AjETGHolYIgQWggd8xqaSnAEJHYke8NPfnZONfwJf/kv/mf6TWA+qyD0+PSCZfWMlhV9Uo6NsRUdPopoJX4UlOer2JcYtbu1whlqU4WBQlYyivd8DZFE225pTEUKAzEMmMojUatT1HOHz8+diuV9rOPkyNEsfrbkRRmjQTLyuI4pMGsqjIPlyZzZvCEMKiO8texiTzQBV1mw6kNVWEyaIH9FF6NSiLkSjNLjJ3IpSUHRSimnsGb0955bIY3BGi2jDkasosWtzktnFCEiSe2/kARMzErPYLwaqqX8+v7xZRwhBb1wkLY1vi0zCgVjTCZyVN4Fg8E4r8hZa3VROVcDrLwGRUA5w0Yn2DBWnjs0xiZ2gTkKGhzbQexPMZPxp+t290+Qu3bf3sYxUgIiRwPn+Pxpy2P8OP3zwaBBfrQSnLiP6+F4wwgYuceefbA9ZLDceS164F0mk29v7xxkiLHP6QwlarbvXYAcMdVtksDikQiSKVKtdVTOs1rOePKkYsOMKAlXebrdjm29JYqyuCKCMxGJLYkB5yJIykEHnVQpDgoRlEAyioJoZg1pMHgWLOc13hm23Q4jhlld8+HT97h88Rvqeoa3De89+YCP3v8e3kLzv6ro+54QhPPzcxDPzfWGzXbDZ5/9hq9uOn7cd2xvb3jz8jlPnlrWm46bdc9Pf/53uLq8xtU9u7ajC54gc1682nJ59QWbzV/gfENsVnz89xacfK/F1M0R+YwqSSWbSVSmozFbnB0U/kfExo7t7Us2L79hc/WK2LU4Irsb+OyLX/Pk2RMeffQRz3/7JZ998TW3tztqZwn9Lefnc8LQY41BWnj15afMH4FUT/HeYewyr15MzPOyUpci2+trmn7DYu5xRpEoYjOcyVhMZXGDGjxN5bXcJSrAhB5rBkyuJ6wDOHF+ekrTNFofO8MgwxAgFp92OtKnYq7wDqhxv1eSgkjQyhJDIAwDAvRdy2a9VgTGMGCMVVSNV6hdiprKM6trMJU6nUlhWyk765ISTiySAkaU+CuhJaAkKnwShBQtlXO4rEi8AWMNISsRJTfSHttkx2tLKuVfGZ9y6joVIVqMslKPPHP9HFSRGAk6IROWaTDPOe2bsWWsqcupwk/FnbFTKWj2/+f0FjN19kbS1/H1v9V8Ppb9duo5Tz61jA6nOThRJicbps/44C3vNrn7+5j7X/p5LGC/AzRhNEyE0VD7Vkf7LdfX6ikyUejf4WEPrKTyuHuCp2krRt7B9xtfv4xn7OfalIKzaNzDNh0fB3wS0/6IHI226R11vO3nQoFn3m3jne7GJPZnmCJT5M65ZQCayd/HjNnlWnYS5LOMuRJaCu3wjn9sf2DtH//Df8DN9TVWIgvvOFvMMpIuIkOPiT3OBAoRS0l707FjcCVEZQNVStRZT81mC+WQKpTx+eyDdsSqO0rfI9iWm+xX1nZDM5/hjcAQmBkBGZjVM+q64aOPP8JIz2Kx4NmHH+Fsw3bTs+sSN7tI6K3qJdnPo3cReebod0cJsxR5kedQEYi56QpyygSaCos2VsZ5ZeIexZn6iPcG763mlVPw/wkvEFGSSBsTNi8COGuY1TNOTs7posNawVYOm/PClXtpQCTS7QbWmxvOVmcMLUQc7WC4vO3BCn//H/yE2WLO5dWaTdsRSzwyDKRhYEgD7dZiZjWIwUpiuzO8ulrzF3/xVzS24v0nz/jyiy/ZbTbc3t6w3W45OTnlV7/+DR999Alt5n04PT3FnJxSu4ouBoZuy+oDzf1fX0bcfM5Xv/0GYxr6IfH8mxfMFyvOH4FtlrTrV7zp3vB0fkYjH5OwhOM0TNkzOhSknnI95O+TF4wdYI1gYsCHjirccvvqK27ffMXjHz7h/GyG+MjV+haLwc9X1LM5t+0OUiDJAOGGRq45tx9zKxWg1cwCh+jI0QNLAxK3GOkgdQgeW3m194ceY6CqZ2xurjHVckzztGQ+jhyx6IceO/RU1TyXdE25jLqQhh2VTXifiAQs1T7QQHY8J85a0Xn71A5FIYVhYGg7iAlnPCKJIbTsNjecnD3Kul9RS5FISoEUW6wVcCaXctyXEM2UDJoGEXQBKsZAJBCjEGOpTGa1f1GroyUxiNEFX7L0MUmJJxFgUPJzMUH3Wg1yWKPVJyrAWIs1eRkwMyU71C5OuapGqXwx1dd7HzBb5pPgQrHLlG5F+custTjrVSZ4i3UG57S0rbc+c5wpn5t1YE0aSVuPzYuyGDcZ2Pc43kdb5NB+PTCPJn5GWTQZ50S2Yw6PzYGWh3T9ka0zvqUj+a5j7oFAhchhH9+hffuizj2G0cEN7jGgxj1TpOa9J9/b3p340dqcV57z93Meexl8xucbpoRYhY2nPDgL6oEEm/WaVy/X7JiDd6xWS+raZ72hDqLC7Syh27JoZiwaLTHpGJDUMa/g6cUKUks3dAwpEmPHzfUrXEz4FGnDDu8dznuc0Tyl2iXOVrPsdN5ijeHVq9dsr6/47W9/Q+gHqqrhq29ecHb6iBQdQ9fz4SePueWWuq6IQ892fcsrWnwLKbbc3q757PMvMMFy+fqGm+stCejbgeXMMW9WGO+ZXzyicvDy+ddcfG91kKdmzb5sY2VaFtU1M3+Lc4OiP8SAS7xpX/H5b39Nf7tme72m2+yQqLmYrvmMbfznfPP6DeIqjHE4I8wbmDXP2G122A6SD/zf/l//T5rzj3ny0c/5yd/7J3zwg7+D9Y1CFtkLWYPyPgxDi9ltmfm55ss5S0ghl5XSvLOUBG916VwjhArBt5kcxjmdlN5CXWXWWQmEoaNpGlarGWITxmRGhzsDfeKFTx1gVJ4YK5CEEDp22w0h9NTeEIee1bzCO0NMQQMaUgB8OXXAGGZNA87rSkpUOBuSg2ojcgck1yYmC/vRAbJJDa2MgLCFhCcrwyrXjYwiYAt9pIC1pKhkPClmQ+0IsqbRTLMX5NhRhhpnShnhESJnx2ixlgbylcUal8tXGoz1WUGXd2eglAwq7/YoCc0go71bxsZhyzmv3yJLDPqIo0N34DjL/isfxRrui+buV9zu9uWhm48jaEKMZcop0wCDHP18h1aejfyzXPdtpIAPp37IqM+m6ubuk+29g4NLHSvicXM6vEZOYcuZnfvjslIu38Mc9DXDc2Tfrz3I2mKShtKs2Y/iEbQzEl6kSUCn8P1kTp8MHxoJSHPfHnLi967NXSU5fdYSgLuzz2anqDysPXx5hVNCuxgPz88f/Z2V6R/bv7ctDYHP/uYzvvnyOev1miTgfcV8dcp/9B/9h+yix5l9ieJxsuco2l47CZaYHZuW2WqedRoUSXY3lWiq2+4fv/nAyZwQSJHddkO7WZO6llh5fAWbtmfXbvj6y9+yW1+zXC75q199ymp1gcErZcH8lM4tqSbLkubOfR82JI9FTJk+I9dQ2YiuWqoTl8i+FkYSIgFEg9+qB8eTNK02xhwYV5mhj6zv0ljBSq6YlLuZBIy31LXF9YX9PvMjOYd3DpO8oibQ9EfnPVUzB3R1d+gD1hnqylN5r86S91TW0zQ13oANgcYZzk4W+hyhxfuaejbn9OQRs9kpHzx7hvQR6yuePnufs/MzUgwsVycsFyvOzh9xu96x6QJBl3xVh0a1H6pmQUhboltCfUZwt4RB6IfEphfczNLFxNI6lrOGzhjmtcGbAUuNL6XhMyOkQ7AmAhFrkq5wlwWVCarSkCAOdJevuX3zAmlvMP0NTdrx6tfP6c9XnD99yu3LN1ytd8SUmFnPzfWWRePoug4/bOm7ge7mFdgZzjhwDQUAJNm+0NiAIDLQ7m5pQouzut8YXXg0OIxoqVNroKoqRRIYQUNDovwMmZ8kiGBsZHEyx3YeSVERMM4QYlCODrINMCq3/OuhOaLjsDjgKNoipIFdvyUkxSNLEh5fXOBsIsUOYwKKM6opzA8j0fl0/kxsG9EBj6lznYxkSXhN90hRF6Ji1G8VRauWpRx8qLJESoJERRCLyH6BYipaTAkAGMQ5GINzuURrsnovazPRelkESyPx+vH8L7obo8+iBWNMrhxRSlIql4WijcE4cM5rRTnnR9SCycSbI2L3IVMujx05eMDjnh1uus9m3dtY5ZsXa2ucDPe0txNkFs6zaZvSUh3cXeROPAQYbf13NUBHe/o7teOCudN7pfssfA4t/GPc7d32znaRhKHcV52hdDhyixFWiErECBLVufIoQYvEAYm9RipRUqV+8NSzJcuTFev1LeWlemdJISC1QtPmyxlWBiwD3kZWi4abG69QGuvwTU1tDLOkpXSsSdjKsd52bNuWi6dLupuv+dEP38fZnm9efIWzcH11hUmRx4+fYhCWiyUX54+pqgV9P3B5s8ZYobZWo6miAup0ecKj84quveXRk6e8eHVFt418+P3HnG1art5ccfnqTY5SiXIA3FxTb9ZcGMlpBEqpbBGcRGp6Klq8uWXGNRUbjA1YW2NooHKcPl7y9IMLNo3FxoGbyzfsNi1dN7AdIsE6BgFjI7OFokVC1MreSQSiYJ1n2PbsupdcXQZubgL/sV/y3ic/Gh1N0MivNQlrE8SelAZWq8e0/RWWyMnqlNvdhqiFisfobUwRIZBMxEpUTg5RweesYd7UOKeswSIhlwBUyGQ9W+JcXlkhjtDQ+2B/x/E0hWYmQrdl6DYYIvPZDFKA1GFiS7u5htTTVE4FvjX4HDWtvMPPag2KiRIrUaK5GXEQk5avkihIhOTKyoiogsiC3U76pRBOg+CV7KbA24xkxSFYr2zK0SSFsgl3IGtH1qX6ljlooIGDXG/YGpy3GiF2mq9qMwuyM8pAbDIpgsk1mZV7Qcb3aDGjiBkDClE/gkKCzfQBKQdOpcJd0Wj2eybRoRJdLsH3MbBwT3R6+r3vtiMj/UEnO7e0HzdTUMbvSyg5xkze5h0UlNA7aYVjM/7bjkHf6/E2Keifo3FVDATZu+rTpmNhepuJVZYDFsX510BF3Hvlk1u48fQxrEBRvbZcPE92HQf7AAWUmkXlepPQhylolukH35+rQQvuCYxNx8k+oKMnHs288SHNfuXhYCXn+Lp/bH+ILXSR2lbEfoAoNFXFbLbAVxVt33Gz3XBBAhNHI1BkH/xSVzhpqbg0cHv9hr5bM7Nn6FwpYz6vyE9k4X7UyMEPptunskUyzbYV5ouGygjSVCy8ZeYjZxdzTk+XfPThBzjzHjEKVTVjtbqgH4SuS1yuOxy1pvtJ4DB1qAz6+wz46V9jjQMse5LmNIE9F2j3NFJsrcNZm0uNA9Zq6m0mTTYml1q2bkxps0YD2dFZtTuEHDiw2XFyDFERiMuTBWdxhlvMEF9R1XUmj9YUwSR6XN91IEJVebWXBi2ZWFeexlcsF0tuNy2DGTDWszw9wSH067XaqSlhY8/jkxU27Wi8p64adn3getPz5sVr1jcb5osl1jn6dsv65pqryzcMQ+Kb5y+JWGan72H8Cc4kbi5fs17fsms97c0lq9UM6gV9D2Fw3NzuCLHi5csb1uuB9fpvsH6ONBcsL37A6vFOOQmSjAAZR6RyHU21xZsOZ7VimEuG9fWG3/zqM6w4Uoi02y273YYYB06WMzbXrxhurjChxxKo5zMG+SWfff2CdTcQ0oBJgZPG8Xd//hNub6/xM8ev//pv+O/++1+wfPwT3v/hf8AHP/hT3PKEmFNoBJNXu6HtO7rbG2aNKFrUWbwXQh+pXIWiCEQrkIRICgNKQJCTKFLAEvFeg0515bAWBpMYYk/VNNj5DFMr5qb4ptNAcxHlB/ootyiColWE2HX07Y4UOhYzR79b41JEhpa+3eKdUYRwdmy986RIznUqWulwPk2nttHpgIhylaRsr4nLAYCUsp1qkZKGFHNasrF5nSpNdFTRdXbvnRswRqhw+Pw19Lp6TkEypGiVVwwZgw17QvS9NWEgL3bZnB5hxwUs46zan7mqmbFgnaZKeFeKA2hQyZQaondFDQUZpd1P++e6I6OkHH1HTk2PUGxUsdKzzTGmCn+LrWVKeYP7Wjru+j3oMLt/prfade9mkB7wBL1ju7skePSGBEadUMaMfJc7fIcgQzUpBi8UchLZG2cThRSLgWe0dKFlrFCkQiENWJdwVisKdEM35sdIyhEiJwzDkOFdibPlQqPtJlHZ/YuMMWl+3WC1FKEMmLQFhNqfs6xrApEnK89ZM1CzY9k4KlcRTs/4xZ//OWenJyCJMHTsNjtevbxkOV+x23a8vrnh2fccu5stTtQRrZxju9mxayJhGGi3PXEQbm63RNkRukg3BNquw0qkMgmDZXVyrszBoc18Bfr5PMLM9MzMLQ3XeLcmhkus0xyyYRDq6gwrNRdnlvkPn7A98/TvLVnM4G8+/S3xRp30IYle2UCfnXuGkOFFWakQqKRn5gJDvOXzX/wPPH3yhA8+eB9bL4iK0VYUgiRcSowVQGIYI5SCGgLeKc+GMrqL5ld5g5iImJzqkjTY4K2hWcyUETgzSzvj8Ep4ofli2RjLfiYwifQeTQtzZ1JENusr1reXVM6wms3xQiZ4ajH0nJ0uiGHJdnMDKVFZpxHhPJm0LrKS5Chyx+QIrtXqIWJIVohWv6tEcs5cpFSSKLwGQnbaM3lYKeVU5hBRlUWMQnIJn+FpRdgXNmyZKIuSC1ZKT1qraIVSa9znAIN1Vsm1nMfmVIiiAHDah6Lg97lpklkZ5DDiXt6wFE6H+0Xf3uHcEwuN+wrRgfqm47sZ24QMcgxwcCj2jnu0/z/zSpS/TRG6+7NHw6HsHP++G1g4DGbcE+F6azsak1NbfaI75Z2iGXpyyTe+v8n47/Bp97NDn7oc80Dk5cDLnu7YB83KL5mSSk/LgWdI+zJTk4DRcdrB3Rj//oSyWmTN4TGjbTY93hYjo3R9Olom4ycbLQeSY3qtBHumhfLIh+ZJWSY9GI+GMR9Wgyq/Z3Tqj+3fefv6y9+ymM8xwKyu+fiTTzi/eMyL12+4uXyDm60w0pNSKddosZLhz0b5p3TRINFtL/mzf/HPODl/xPnHHymsmYyCK6PxzipcCWBNZM844iYrSzlgqMWSk8Kx06DHGJPT/CLWqj7QEnqKeBzaHZt1R9slLc/oLJJ6NPE1z6mD+Tbtn+V4nJcAw94WVONUuZL2RxmDOieGvCARCVGRhQaT7cmcMovqhpSU0SllUlUnVqHkEolJ36dxPq/ughGniAkLzbzhxC4xzQJ8has8MWbOhry6GlC7JISexnsYy/xpGUWL4LJjlnIJ73bocRi887owIYmwXfPo/TNMHKjQVIzZ/IxNB3/x6y/ZXl/hbc3MRobdluVySeUdi3nDxdkpuIrm5JS4WmQb5prdZsvzb3poL1ksnnF7fc2m7fFmTjcICU/b9TgbqTNy9eLxE0K/pdvd4OulEmgKeAIVHbW9ZeZu8W6HswmLxYslra/p3/yK7rrj5nLNN8+f00vi/OlTTn/+M9a7LW9evGJYb6DrafvANgqtWOxsRt04bEpEB84ZttsNF8MF26sdv/30K/pP3/Crv/6Sn/7Dl/yDf/Kf4xenFDJqW6ZBjPTtjmoxw9WeTdezWp0RUhqtCWMSKQ4YF1Sm55xLiQmSokFra1ksl8y8o+17SIOiHiQShxY/i4pMJUfDxjKrjKaBIoQmQeQcoLYYSJHb60tN25DI6XKBDC3t7TUSehpvuTg/ZbdeE7oWW9VUtoKURsTyaATlOTW1Y/ZtPw9cWYxJhmQh5bSJFHVKJkCc2aMQMrGpkCZxBiVNPZi7YjDOsTfoCvpc5UdKduR9SDBWtSDtSSqLZCrpEWAy8mjP72XHdAmrvH6ulFF37EG0+hHs1M45UqnmaEXIGuWsuK89bDvev//Qcvj29m3Xv+/4w1awow8FGe7aat/Wt3tNuO/Up8m9TBmR2bbDgNgRZfqu7d2rSxx/yNHxmUK3NUhQVsvIbMHZesRZk1MVbug8UM1o7IIUK0IYWMxnbELIq8GGYehVyIeBs2bFWe3pk2NG4vLlc3a7HdZYPIZKhJkRfBwgdLqamwLDrqPbXWON5f33H+HrgUTLavWIqzdXfPPNC+azmfIIWIs3jto6ThYLZnWDdY6TxZyTWcKZDGkPgc3NmiscknquL2/Z3rbcXG55+fqKmBLnJyecnJwwryzzSie+sYYYAlpyMuLQ1X1vehquqOMrvLlBwgZne2yAqzdvmM2X2Lrjm88/5/b1Ky7mM2Lb0u9afvSjD4kh8er1lt3nX7Proq6oOzsShijaA6z0pLRjtWj4R3/vQ07PnzAky+W65/HiNcPzf4mpzhXS5hzWe6xxmGGHH3bYGJAw4J3BeUPMRo0zSm4TjRKEzps6B6WK46fsvzaT+rTbDTOjg1WSGgwpVVhRAW6lkEbqyNP/p9OtCLbDyamGTiCFHmvgdLXk4vwU5xxhGGjqmqFvmS1WLGYztutLzd/0XoNfEhRmxt4BNjYz++Zb6aqKBaccCtNcNUmqvEwxqEZ1khWLtar0yy4BkhJH7oMKqhTiQYBhryhKoMWgEWOTAz5kYe6cohgq7zRKbPMqzxhkMEeSaAqPTeO7fkjkHr/xh9s+QjwePdHZ4/nl/TEJOJiSvfm2lq9u8tjIhsHINJx1pi3/G1Q4GvSex3nRR48ydkWOXtc7tHRfUELKf4bCyiHsfdnp72MfhJyXN13TN4cH5O1F8O9XFSZhkuIwlGc/7tp020SB6Ir9/n5y9OXHPXmcgmRrx2T7rETA94kO+78n0XCjSmwfkJD9t81GxXRhQ0pfRkj2ceDo6PnGiMckH346kMeH2Rt1h286Q73LwWb/0UZIo5F73+0f2x9W85U6qFoFIWKdMAw7Xr34ij5+xcnFUx49ecJtF4hYmmpGUzc0sznOVVir6EojHdvr17x68QVV7bAyEEVrJpVxvnc2mPx2yIui8yDvGcfrRGbn8acw6ohJkRghpJ7tTstabjcbbF593G02OOOVjd8aKgyDtwx5FVhdL5dveR/Ad/+7Pd4iGboth7KhIC6KnJkuV1mrq5kYLdOXYk4jtHac5ykG3Wa0vF7K8iYRVWqkoEEIgwYsEvRtx/rmhqvY4xcJW1VUs4b5fEFd1YS+J0ku3Zwg9h3z5ZzGWZxEPIHHpxc8vjijWiyZ3dyyGxJ4rxxKaHlSFyO1i3gbqdKOD56dQ9hBjCxPLvj860tuWpjPL6iaE5ZNIlWO+azm5uY265ZI6CNps0H8DtKAMQnvHSerU6g6lqsz6tkJtmqYz8+4MDVJLtluWs1cFjAxst3cshoG7RuCNxFvIpXsqMwNtbnCxxuIHeISxlQY61hUPX/3Tz/k+uvXbM4rXLrl+fUNxg4Yq7ZOiJCSAfyYq2+wECMSAJuw1lC5RDIRUsBFwQ4aeLn88nP++eWaP/07f496tkCcHwFvNiVsSrkqnS741K5CcrBHJFLVHhCME+rGU1VmEnyIin4QGaH6KelC5TAEpSJoGpxzOCvK02EzYqbEGQ6Efg4CZFkvGdKuKjQShwFnhJOTFWdnZ8QYCWlQboOhp/EzZnXNrttRN4061UTsuPSSDu4zlQJTy8SYohNz8M8WXq9sXWvdziwbdKtkXZdG+3Git80RvD2n0RRC0/EaCaJYJGYi/2KHFDsmBxiE0knJcm9vxE1fp7NO06y9xWXUs/KKSU7b3aOh9rbFPfr8ntQC+9BijbzlOvc0A4frFO/U3p3Ed3+X/blFE7wTZfQ7mhffDWfw0K0km+ZlrMrYz9Ljd23vHmSYGJ95GO5vmI3XMqCNJIzxmYxF2NcBjITYc/XmNTvbI65mtlrSNO8zdFvm8zneGYYQsbic429YeHi8dLx/BvWTJe9feIbdFRI0/99EFbAiA7vtDTZ0nJ0scSLMvVAtK87fu+CDn/6Am/U3vL79mmezGc2iZr5Y0DRzam8YWuWE2GzWVFVNDJH17S2LswtC1+aJG+j6jugTfRupa4sVz+3VluffvCFEh7OeftdT2UDCMSQhBiH1MDtt1YlOAWOVF8BKhwyX7G4/I7kN680VpxcneJlx+dVLPvpoiU2R53/zG775/DP+3s9/xmoxxywaTlanNE3FYl7T1A7bDSQ0GGJz5LTyFkOkrhLOtVycr/kv/ssfMVusSMnSt4b1beLqq/8fQ2uomznO17imppmv2G13LLbf4H1FYxLSNARjSSFq1Y8UccpYibFqvCg3gRLbRDSPzVpAIn0XWKyWeGuovSPEQAgDYejxsxmOQCJgxGUyqEyUeGSW7csxwghji7oa09SePjvaq5MTTk5OefLkCZ99+YYhzej6QF01LGczjFgsEUQDHFOyfMN+5Z7MrSCoMB3lbXE0YsoQzvscV8lGDftcMwEZUSFuD4MTwaUsyMlBjqm4NPvfzQR1K6ClvJwZA0HOgLUJh9XysAe56qWDaSK4j92r6c+9kailN83ozN9tZiLky6WnbzWLVzncrXW9D1fg728pp8dM75j5RI6qpJSXY+7gI9/S5PAK3yWHYiS0vLftDYY9sVQ2ctLdIxXXM2FkmDi4U+IlkZgd8rxtNAD2fRnL5h0rusMI1mTX4bs6Gs6HxzG9hqg8gAJf2z+c0Y0ySafYB4sYgy/lukr9c/TBSrdSUYLTMTsZY9NF13s+vX6JjH4pY2ZcTSmnZaMnB8Pk4CL7gIUGo/421Psf27/LNoSB5WLJj3/yE16+fEnfdQz9wKyuqELCtBv+8p/9f1ivN3kl23N+8YSLx8+43fUYXzOrKi7OGirZ8d/87/5zjK3o22v6OEC9wtQz8LXeUGR0cvKGo9m5D56N8nKclzlDNusKRd9pYDnFRIqadrqYzyEMGng/WTKfz+iHBETCoNB0K6r/SlG/MhcPJ3oxho/dgHJMQQOq0Cm6ucjlUsbToEGRgnyQlEgZ6ioYRFxOl9jfV1A0QyDmQLmM6U+RSHIBfEII+hwG/ZkEYlBivaAO8ayqGLxjGCIYgwdS17OsKlaN56vYM3OG959ccLKs6XLwJUnMDpGlIlChxJ4zC8H0VHZg1iTOz+aAEBP85otvuN5Geiu8ubolVT2pu6WfN7z45hs++NATY2ToE6tlNaYReGeIMXB7s6aWgd2uJxLZ7BJXV6/o2kDsI4gDMVRG4eiaImByAECDJY1pacwVnktsusHRst1egzPM5ye8eX3Fy998zXunj0jxhpMTz89++j6LFw2tqTFRy3WWMtxafUARNN5GKhc5P1nyyScf8v57Sz78YEHfgpENqybxg/dOiKmmlxq7WDLbvcJeV9jFGck3iiqJCTMECEIcAlXtmc0bRbJIwFlLlJw2YcFVWr1LF+ssJqf1GqOr8Dc3N5yceV0si0q82PcdXb/jZHWe4fmCz4iG4lCNTrMUZ11yUG+vX1IMOAO198yaGVU14+TsgpOzC+bLOdc3V/ShAjHMZzPms4qdgCHgjM6xaWKp3FFU07k22bpXN1nnyB4dejRTRWwmT7V7Z1HUih7DnHkqSw4yFIBASeU1opBx5yQjGPaBwjKP9fyc8mInrp5MgwXFVjUaDDJgcZgRsWzGeQXHMmfaiq95ZNulEmA8NuzuC1I8cOnp4e9i6hlDqWp3KCL33DD3tWMcNtk3+q7t7bGQ4+t958hJ/ipxMiqLbZkDi+/0krS9c5DBpmIUmv0Az8rO5IEZY+ZtEIW7CmaczCIJokbyYuwZ+h3BDohNkJ4q83+KSq6Ycl5OUvIZiVuWi46f/WzFx997irFzXlwmtm8CTgI6TQUrg1aI6DtkMSd2Ox6dLtl1HbvNhpevL+m6NavFnPnc8lcvv+JmfYPzjpQGjexXM05PTjlZnWifrWM+m3GyzHCgyuDqCqRHUiANA9vrKzbX1/TbNVc3A03tmF2swAWMccQh0fWJetnggfX1FTx/gV2c41yFc1uIG+huMU1H5RLeWbyxnJycZOJAy6JumDlPZSxNo5/OOEPXt2y3twoXSwEwpGDAeZzTlAcZWs5OKprG4OzXVI0hyRsMc5zMmSXPqdmRTEuNQ4ZADOCHOW4XWMUO41akjaOqTrB2hhWH9Q07iXgSzqmgMRmFYNAgQ4lSIonKW2bLBd4Zuq5XTgZncA5SNNnIGdTpHwVtCS8UI/7QQVFXXBWNpMjQdwx9x+3NmhQTZxcnRDFUTYP3NfPFkjdX18Qh0LYtyVXEvqMsCRwaTlNzykxkWVYNGf1qshYw1uydxYkhCEbJO3OwopSOFCnr2tleSDbnviW0JJ4qicKhsEcHTEkg1VATkZw6YcZvYU2OfNt44CiP0cgiIw9kX1kaUcdposcmclgFTpHv97cjZ9bsX0yB0h0dMb7jiW/8sDiTougmRkBRfuUax17lfRd58PrmnQ67rx0owYPfDHd+jLnL99xE9ka6GgbFMNkfvyfblKPvc7ffuv9o5WRiCUwSI97+fCVWkpXk3VerkyIVOGtxKDBg9vmKd6Pi5UkKWqfM8H0sw5bfGd2P8UqYpAEwkTEDorBdq0N28CKyoVbKZ7EPRuR5Wu5p2AcP02QxqDh+hXvnj+0Puy2XS6wxVJVnGHquLi+Jw0AcFMFXVR7bd8yu3+Bi4vL1a+YffZ/19fv8+S8/5WbbYq3h5KTmT376Pf6zf/JPeP3qJV++2fBnn35FffaEH/z8H/C9n/zsoJhECThOZfTU3dD5NDEWi9AW1RUpKIoONEU1DIEYLO12S9912KRpjtv1mhSErg/sdpHoaqQyeRVYYenZ1T/qBexpdt2BjC3HjYTNZfZKUXc2E0CTEQpxD7dOe12TJI5YunLNURoUnobC1pedPxCSBFImMUw5fQRJtG1LLw4jDldrtbGhazUQ4x3DkHSx2kLsOmbAaeWYm8jFoqJbX3H56gXu5JwYey1hnWWeTQNWgsoya1nO4P33zxDZ4WpIRG7XG15f3hKMJzmYLZY8e3SB6RaA8PjiEYvFHBHLzgZiUF4BQ8RZS4rCzfUGH9esTk4Zupabyx3bXaTvBmpjsKIVAjyCt45FM6dtd8QUqW3CSQf9a9rdF1h5A2bL2dmS6zevWK5OqeePeP3Na/7yX/05J//wP2AxqzFA06z4+tVrGguVgdR3xBjGLxNCQGyk9pGPP1zyD//Re/zwh+csF2BMR+o9u3Xi+x/WPJovqasZtmpws5rZ+l+yvv4l9uwj5o8/oVqdZ/t9i5WOyniWs5pUOdoQdeU782J5i6JcTcQaddqtASEpcTwJjFD5CusMadDAg3NlUTRppTZJuaICKAn3fqyPto1BFyyM2mLZ1EBCRxxaJES2txsuneHJxQnOOuq6JrkK6RzrmzWNiblflqHfqr07omVVb5X0nLfbIvZod0H+FQTj4awZbcPiG4Kmf1jRLMM852y2MZPZa1I1uY1WvhDyIlixJ4riNcW8He+tae6o31T4rsb+5vTj3ENrJHMrq69oZG/blJPvBSaK2VfBERRlM3br8P3dsf++TT0fBC8mF7nXbtzbLYfICnMHwXtwe3Nny7u348s+GIuZoGQERrl97/t8i/1+ZNelyWLSdwlZvHt1CZMtL5MVg4aj8sDQ6dvHmEnm7OhsldVGIwqFslFwJ8YAAQAASURBVALemjEKPUbm0ZItJc9NnyUxhB0xGS7eq/k//p/+S2YLwfoln3+x5Z/9j19wvd5iMFTO0ojDNp5uUGc77G5xC0sVdvzyX/0rfvNX/5Kmifzv/+l/xcw3rBYLthutQuAkErqePnW8fvUKiTpBr9e31Islm9uNMtSK5keFdmA3tJgZOBu4OHHMqqd8+umXeJv48PEKb3p8EWDSYQAvievn3/DNqw3bpDVjT6qWv/M9w3ndYqqg7zrD/+pmpo6jNdS+wjuLIRBCS0iRhY80jUFSIIZAConkU0YNiBIU0hN3O8Jmix1aFt4SWkFLglSQ5gxdYOiF16/ecLqas91c0SxmtLvnVL5h6AI2dIRuSzJzEjX4htPH75N8jUktjRU6p2PFZgNoj2TJsC0Rhq7FVR5lw42ZRdjRVE6DFAbIUWkd0vtKEOTRVvLlBD3eFYczJYauo+06dl2LqypuN1tEhLquabst1dDR9Z3mp3Y9UltC11FKak2F977to3cHgceseIoDlUv96vwuMPEihfIqvWTSVGsUNZHNonyMzhspFMw5FcmYPSHn/n9Bi3iZTBYpuZRlZvbNMFWXU1OKgjCH/92Bn40lewQM4V45W4CKe8Vw3MwEbp+F03iozvfj0j17x3OfKDNVng9JyTs5YnZK8ZP2972XsObhtIx9xPZtyuCwh+Ome6/3lks8cN49V59c68gkz+fbpBwGxQAAMr/KW7s3njymiT+k6Mv1DuMlOg6PFFMJoGnknz0kVSYrNWYyNgoiLvdU0uTr52PSZPWgpOWV8ncxxKwMs4oymeS0vKCjlQdnDRQeoFwZR4+dwtnLtlHtjU88fX7u+euP7Q+rffzBEz79m7/h01/9gtubW0SUlHm18Pzkx9+HfsP2xQterVsk9Nx2b5inM2ZDjV1/ze7lG0IKLN5/zEX9EbvXv+WbT7/A+BX9yy/5q1/8gtOzc370ox8hB/nR+7XN+zJxD5jOixwTJWcuRIsShaapNS01RLpdj01gYkboiSBRiZZr55DGMpiK4CydJD0mRSWZYx+4FdRuNWYPHz/kWSmrlukgGDcuCcRIjOosWjcRSigCK6aosOx0RJeWdbr2JDsmJRk3cyiNc10KbF4DfikM3Ly5ZOOEaAbEGmbbGY8ePaGqarWjTHaIxBKHHpd6Hi8XfP/pnDN/xrrdkfo1c3NG4wyN03zxKgWqqETSKQXEeZ5+cMFq1XB9fcPzF18T7BwM+KrCC4gJdLHn9etr4uYaZwxf/vYrRCwhwXY3sKhWQK66gVBXNY0XQneJTdCtN7x6/pKhV93ka8+igoW3pKCBsM3NLc3iIjvtCSsBM2xob15CfIX1AbuqiX0PQbDBQBfptx1D1+EqJecDw/bmDbY5x4lQGZWNXd8jXa/jSHqenHj+8d9/yo9/ZJgvn2NkQKQmpgZJcLoS5tbS1AljNgS5IW4vMV3F7vpTutcf8eTjn2J9TbV5wVJumWMxXUJCzWy2wBqr1SIS+JyaW9IOxrFnFMVpjVagWCxnWGeQfkAIeAveCslZvFUbVceQG4PQk2Gnfo5J43qLiLqOlsQgA7vtLZvdLZvdlllTcbtZ411FXXluth0hVHRtRz13iGi0e+i7nFa4R/ukiW6Be2yxpDNR81EO2zSsPZ426rc07h21lGH0K4plVoru7p97bxGZgwvnu01WJEY3Owc0KL5cdvrU9p1Ks8nDmUipfLG3EM3eJrR5rh/ZduVbjzo4HdpIyU7v8t1g/eM7uLPhAe8cc/d7Te39h9rfhonwkGF4YK/K/tv83jfaJ+2aSRWad2nvXnVLIrqyqiQr1uQVVzGa25S5GEwSrFNnz6F5SE7ydtF/tXfqqOfSATEmYhLarmMxVwhiGAaMQD90RGpC6PjkB5/QdrcIcz786CmLv3iDyEskRsKQGAiEOGjE20UeX6w4mcHF8oS56Xl99ZxnH33C0ycf4O2cD97/iA+ePWVod/SxJw0D89mM1cmC1XKOr2qc95yfLukGqw5wDFRJmDczGg9Nkzh9tOLJ9RIGx+7yEomGx6dLUtD3NgTlHljfXONPb3i5vuZFZ7DzMzabNY8XgZ89/gA/j3gnDElXFUTUYY6zSMrEi6RE6Hu224EoYH1kSDuubi4ZYsjfZcq9HlnUjqWz3HzxAsuOGx8JRhBbYdyGQa7YbCOzesnX36wJj2d8/eWGi8c1L19ec3Z2xma74eTUst5d00dhsTpjO0R+fnquMKgUSSHkKLPmyInJ7L8SNEKfVy/6vmW2mFM5S28hpIjJSAiRSCm94wxK6lkG+5jHMImojVJOI9giA6Hv1OBxlmi0hrJYwdqIpA7vDdZ7YhhIKeFHhaxG0hT9vZ+jd53R43l+IPTziQdQqOk1s3NUhH1xqxPkaHC5cgkyTIJyk4sJamjqBfRqJjvehhyNzpFhMxHARbgbo8SWhbhv2pvy8CZD3vVShxlkBsAeJ7JYVZ5ixvz2fW9lH0IQcxhoyF2wYyQ2UzXe6+wWQ1uVtplui8JhJKi0hwMKd5uACZPfy693IjJHv95Va+8kj8dxfd/2qTLbA+7S0b3tpG9W8tNOAgFiNABRfr+3TV/Rgb6SvQFi7qrvspI/jnfR+apIgYmTPo1cJKBA8oSc51scCxDR+uBaJhYNlpn8BmTfvSSFGKvcI8M6jdFc2Dx/CqxcR3kaMzm0AxYjXoelcGf8HM/z0UybGKj2u+neP7Z/D5s3kWF7y+vNNX3f0dQ1tbXU0nAxr7i+XbOohUXtWHdaEi+lntonfvDRE2xqGcLAf/KP/h42BN68eM1nv/4brjcDH3/wfba7DtPeYLot1WwOpvCTlxW5hGQOmTu4OgHYQ+QNuqJvJGW0mlMdXFV44zhZNMxnFZwsMSkQY+Lxowtm8wW7dsANiXWHTiBjNC1QNOimaWv7NK291FdjUykD9wgih8GLzgFNCcyIPok5wKgBgBgLCXVBN5XqTIArgQ3HyPEyWTE1KHGxiFadsmmPu/IZuqwqS5AQCV1HqgY6UYLHZITTM13tnvkZs9qzazvIufwSN5wuFvz4kwo+ecJXL6+YnQttuCaFDYSg+f3SY4iY2FLVlnlVQRrYrtf42vPs2VPEC3/+r/8lr19vqebnVAvHcrVgaQ14tR+7PnJ+8ZghCIvFgG3mxKbJgSO1lQyQQqIyIC6xrBLrtsMZy5OTxzQmMLORDqFtWwZ3S0PCSiT1O5CWOvY0RjCVw9VgUuJkscBntHBlna6+CmMKq7MVM+8ZMkeWKz6qaGUtmwRH5MnqlBNf4ToIg8W4BZgFxi4JQ8963fP65YbGV8TQEVMkJUMYLPgbqC7Zff053lZ461g5Ty2BMOg48jQEidTOa2opwryuVBegfgY2qiNvkxIEG9is1yxPLJW3DCEoV5yf0TRNHnfZRjBRJ5rdq4LRikl7vRVHH1uDEyH0iBGGFBjigAH6voU8npvZCuscQ0i0/QC1VjOzKWWbxY4LNDq/cp+OFIimD3HUs30b7blprOJeYr48k60ZL1P85zGxSUpaUzEa4t3LmHtMq8ntTEp5kacsIDNBHSbtqDU5eJOYPvJBr43q9Hs1qrG6IGD2Fuv4hLnYyCitCtp+vOx0oenwnY6v+cg2+m5pkHcvMH2u39vnL9c5uMZBmOie/jx0aN43iSqVlJmD65t0Z/HonpHxYPsOSIbiGhhSriFu2a9cGQumchOuL4NYdYBKVr21UFvPrKrwxtL1kmGqkolaEiEEnHMUhtO+U2jY+vKWL3/5Ff/sf/gf+Or5micf/QnffLMhUeFcoraROoFYA7VjuZzx4x9/Dxd2nK6WvFnVPH/xOT/66Z/w9Hs/UqlhbtntNlyc/4hZXdNt18QYaGaeJD2bzY7r60sGE3n+estPhpaZRBojNE7J9Ug97eaWtLtlxpxqaNm1gd2b13TdGlt5tt3Aze2OevWImXOsXz/ny+c31OfvsV6vaZ54pDuntp7aWQK6ok+MhH6HJSFpYNfu2HU7kkms5gtC0hxEYwYSnRoZZV5mBW9l4GK14GLWIOsdQmIoK+gu4puEIWADuJljVs+obI2zNUYqrGlAavpux81tYBBHHwNuEK5vt3RhwFQNM6s5hDH0IAFjMueEDFqXWQLGJJwT5t7nko6WyrnsFEeGoWexOsVbw5CdAmOL+bWfCHufr7hZmQyCTKCY03asdzTzGa6piRI5Xa1IQ0eMPWDw1uMQKgTSoH2VuM9PK7d8QCgciqfsZOngH7s66fLhuZkozmSoWBF3Ll/lgCOhOPvH8iIVJuyywWIyE7MtDp/ZQ+cU1VL6N41CF0WXcr6rBZPJv0wJOuylzx68n9uopItZWkSQmRyVJZccnHlvk/HhD9Ni7hyXj9mHPOLo/GYf9/doe4N6z/7IQbxhj26RyUeevFum371YAeVdvuXOB7szVPMgWqBmvhy8H4s5uqw5/kP2Xf62YPvdptEJS1Hs9uCdjIz52XAw+RQdd3l1c/J+UnEwgJjMOHKQXKKs9DOGbPwYYhyUJK48ktEHchpByIsouVMimZtEx3WK2jcrOscMKOIOMyEK1f6UGuIPvaTi9kmGmB6mE/0xzPCH3Pr+lpPTBkmJIXref/oeHzx5AkNHe3MJKXB5e8vNtmOzi3zzesPLm0959OKKqmk4OT1h1sy5udkynwm77SVtB9vtQPfbr6gSfPGLP+fx+QWf/PinGF/jqiqXfMsuu7Eka3DeIc5npgNd1TNj0rPmXCe0XCYxEodAigkXIzIM9G0gtGtur99AHEgBXj9/TjNf0IdIEIP4JWIMriwSwVidojg+Glgs8yr/s0q6aNFAscXgJGFD1BRXiTrvTVRGexFSTBibXZqD1D1VOS67OAW2XXi/xizgmEgExAgxhUyyrNUknIBNRp3fYHUhjKK7c4UrIjG2iFi6LjJfLGm7LsemEzFtee+9ij/90cdU9cA3l4/YpUf84tMN1ZXye3XDgO13NJWjkojP/Fqf/vVf86/+1f/MyXnD//n/+n9hcXrKvPa0N7fMZmdqy4WB6BOp74n9wItvviFFoe0DXR+RZotXhoiRmLNtW9p1S7/bMV/NeP/RjFf9jm6zYWVPSMMOIw4TNXVxOZ8hw8BXn/0aqWYQd3xwGlnNYiYAz4ogClXjsCRm3uEQRRobcCbhjCJOBym58rkygDH0IcEQeXQ246Kac/v1huHyiii9IvLdAuyM220gGsfrNx2VF/o2QHQYa7i+vubk1NAONxheEENkPl/h5gt+enaOWEPIBODFSZSYSGGg71vqqqHwwUmKOCIx9JQKU+pLa/6/t5DCQIiGGAbq+TLrMV2gECOHjnL5OUFpOplsS8r75StP3dRUTYPzNcbCarXiq5drag/WVzirFdk8BhlCrrKWxmC5pfAfPEwg6LBE0gN7Dzs9JVg9ehpGj//AqSwafTIX2dtWd25z4JeLBigl2+hJLXIJR+knSe1jY0WDDEHnekEePdSsPbSVDAaxRqvWiWiKxeSQcmQs5ue+3MV9sQPuDdwcH2Tu2fbWVuzlo0se+C1/C216IWHyYYqRp2N1byMeFCLPpx3Z4/fHscaXd8eefMf27kGGnNNnrJbv03w6JeNRxWQzwXYajUuMpTaRWiKVJEwM1JVnNZtz3UI7DJCiOoWiDk4MA3Vd4bzPq/mQoqUyS6wsuL0Ufv3XL0nue2y3iRgMEiJGBkwY2G22SOzZbrfcXF5yPne020vOVp7/9J/8Y15+/Tltu2bWrKgqz/vvv8frl8/p2x23N1fMm4Z22+JOz6h8xXJe896Tc/ogLHzApw0zevr2liQbZq4n9S3t5hrrIjG09G3HMPR4X1E3NclY/K6nqT2zusKZxDdffE7/1StiiMx2Mza3T5F4ggQNKGilA4sxEYemUHhf4L1KZBOGXlmSQ9BodB7ISRJikubqeUdde6rKUWWnWpEVGpFtwxZxFSfLUypvePr4jOV8jnz4jKppqJsKX1VUdUUUQzcEktQ0s5oYZ4S2x/tIt9kRB/2OQ99pqcsCn4wBCT0mBUwcmC+WpNAztD0xKKdGIhDDQL/Z4JcB8fsV0ClU1DCBfRuFhUuGaJok2BgY2pbt+pa+a3HG4qKmt8zqhm7bsVtvkZiYNxXeRZw3pBQgJayUXLzJHDN35zSUSVw2FFxd2blfaX/I7RgdsanTP91XTjzozH6V16SIOnZFDpTgX8qX3XuBasjtAbCmPJdCJPK5mRIov+TR2Y3TuGUW3Gb6dzqST3K4vyiUg6e32bE7OtSWfJnjt3Yk1URTl3QwTJEPh8br7+XylXQf9hH3Q6V4+NR6vyOtZFAFkA6Pe/CWcBBMGqnTCvHHOOD2MEQjmrxyHBkrt54q4YNLTA4XDg0tQdMS9ikWxezXd5ByYMFgSFrXNfdKS8JldykHoNL+mklIUYNrNqdWW6dGbHmDEVHETC55ReYaMfj8AHl8GZen2X61J0melzbbGCQldjVgTDaXxzmaMyTsPqVj+vwPtWI0jEGJtxz7x/aH1V598QVXL19inWW73fF0tYB+ye3rV1QkThczOD2FXjB2y+Mnz9h2O7bdQHd7AwaWixV/9de/ousDIp6YDEMQ5ssVppkRL9/wL1Lgt5/+ksXqlGaxYLGYU1UV3lfMFic0iyX1YoGfz3F1jXGeOObWlhQxlatjibkYsCJ4U9PFga6LJAmQBrWlonId1d7ivSckS49jXlVsSrBWIjaTjhgjYAsRZKmLgSID83xTrWFwKWHDjsr0uGK3Zf2Skpab9DmAMpa5zU1lXgmWl2CsZLm5D+pl6aKOSorEJDjrSEaJph1qcySJunIt6hTXVue9M9DtdhivKNzZYomvPCaT2vVDj5tVPPn4Y7brVzxqznizrRnMmi4ocaAMHbHrCMmymjuePTrl8fmS07nls697Pvnkezx+8pSA5+l7T3nyeEc3DAQDcdfRmg1xu2FWeUQGFvMKZz1NY6nOHtMvFsQQCFHJAb21NJVT27GCx8uKNLfc9MLJzJHqBmcsdoBdDISuYxY6rl+94OXtht3NS8L3TvnZT04xfsB4tafadkvTLDKfjhBCDzIQ+oARizNCu1njmprKWg16+ArnvAbErPD+00c8vThH+hv6odUUaQvWg3GJpTXYypOWSiTe+ZphgCEarN/hZo2WnU+a6hAlILEnpb3DVDeeftsRuh7EEAeHxAApZMLShMUQYlRy0xhxBhbLBVjYdi0mDhqsSEER0miqqpL+hiNY/jRyPkXg2XF/jANdt+H2+pKhUztfkdgtTVOxvr1haVeqb7xRJIjTlBbJ5Kolq4Ac3HkoOK0zfE+Seu8x01OLHoRsDxw9l+yPl8k8M3tDdlSvh/bNkUEM2f9TfZwk6Tw6SkccLwgZwJH2eZb7Xfe2Mr9HTz+/L42T5Oc6ioMUPV7sYVOQG6IpxMbsUzGnr2V82uNoRLbt7777PXLy0Ny766lPicDfbisc30Ue2FfCr+WIYkhn2wjZBxSK3al1xo/sx/L+J4GcB77HIVLmu9k8754ukRK28nhXUUrrDWlAYizd1X95CVFIEAXbbfH9GpFI6HYYk+sgyz7vOoWBfrfFeo9xgYXMstKMRAxJHJ//9jmWyFdfXyPMqeoVZrNRgVc5amOorEFmc9rtQIqJr377NfJowccfPuKDDx4zP5kzVBabenTsqmH+7MP3MXHg+nLByWpBk53r0A/0IXK77TBph+vesP7tJbvL15zNHbO6YlYZfNMwXyyY2Rl1XbFYWOarBULEOrDBUFWGtluz2VyT0oAhEUJP5YyS1xAYuh1VbRDR6Cd1hfdGywE5ZYouuYQmE6eYpJFDxNC1gSS5TKJRpABWcM6z2+xYX7/GO6EbOrAWX2vJrdnKs2garPd03mWjH4x1CvWKiV1Q2LLEiLPCzAuPTxc46akN7NY3WF9jDWx3axahp8nf1ySh3W2JQ8vm9ppd1yLW57JIyp1f6iVfv3mNOXkfv3iKmDgO/zsOo+GgCsR+0iiBqEGFmpLAQb/rOT9f0nXduH2za7Fzx8zYcS6OxlNxzNLdWWfGDuyl9YHgEkCmjvnkGgUmZiZqxR5O2SI8TDq8rvqqaV8+Kgm6SlOEjM3yu0T/J+Q8BkwqPS+Gaha8k2iwPr7RcWX2K/V7MWRLfCC/DHuoNMbeF/dLkRp757Acm5XNMU+CHAKx9ukwe7XH4abJ1j147mFQ3NHtJv8dk5mN5Zin+qigXEZvO58r0+j9/gUdBJjJ3+strZDojmiYlGVpITvL98j0OPu3UtIgJg+mHGnH5SQZlYskyeNRc0M1G0ugOC1ZYWXXQmV1SiPJaeE4UPTQ5N0VrWozP08xSgQKRNIat0fNoPJMxpQdhX47l9NpUhnPGWJtzd5RMfufIiWwsCeq0m+WRgLJHFmjrIIZW1IwygcYX9/DLT/eu4Uk/tj+kNpcEo+amvVmy0IS25cvuQHCbkvXdbzpWzCOvheef/OKm/WOetZwenJCTAPGGJaLE5CGXRtIYghBaNsOX9fKw1A5PrhY8d7ZAuMtTW2I7ZqwCSTnWZBoKovrBGcj1i2JTmVpBlpn516Ikkgp4h1Us4qTZkHjLI3MeXLhWMwa5GSBlUQ/JIYozGYLuj4wDILB0cZI7DuuXrzAnyZM1eCaGld71RGASVqBIqWIzyuEkjRpwknCpw43vMGGDcRe7ZCyUoymQla1H+dhl6LyP2S5KxJGA7loDiYGcEE9ORgRjs4aLV0pESMBmwKV0XuSg4rGkBdYKpx3xDAgYqiqmrZrmS+W7DZbUrJstz3r645f/9Ut/+1/+z/y+csrfvaP/kOwC4xbY13E+gqMIabIrFnw/rMnnC49jy5qXrz6DWenc5rljLhTx+v6+g2PHn/E6dyznDU8WjbYVc2ssYgJzOczYmoJvbDbtqQ6gXWIMcQk6qQ2hnpRKaF4ZWg8aheSGKJyaHRDZLfb0LUBd3LCznV8/duvWF+94Ptnn9Bwqu82VxwTCWpjkgihIwwdIbQM1mI1SoAxgjMJGMBoSkJCMtG1cmwof5ojBkMImqLZ9husG1gsVxgDy9rhKq8pD7aiH4Rq7gnG4CrL0Has5jNmzQxbz/GpJ/SCoWH96hWdwBAEbyt2XYs1kb7bMXQt9YmOQVKk27SYGBg2t2xjD9bQ9QNRlEg9xIiEges3r6lWz3DNCldW3ykIzTse8uFPI6Q4EIceiR1CQHJpcusds1lD325ZLJWIddv2LGeKzB5Sr0gcyfqOw8DaMXfBXpdroOKhdscnngRKzMRmOLBRASOKGhrn3eQeiWwL3He/0Vben1sWvw5iNJMUXp2PKeveqbN731PkP23pid3bgWa/yJY7sbfHgWmFrZICVHabBDg7eecHRmSxlsf+GxhLe94NMugzyOG6VomD3HmUyVkPBov0YDte4wCpakpfp0iVI4rJSQChbDi0USbf/qCfhx2+a9eY/fuYHvfWB9m3d68ugegKuuT1GwGTc7XIq7/l4+7HmeDigA8dSQYNJnQtm/UtIkvSMGCrimHXsb1dUzUVdd1ACswqRy8hk3cZrNM8sRggDKLMwFbzt8MwsBs6gFxHGbp2YPAQQkQrGEDVeI1iB1213u22vH79ksdnJ3SbG3abG/70T37I++8/xXnH1fUNr19fcX11rUiFYcPuSusvny2f4HxeoQ2R2s3wVl/nrt2xa3fE1ANC2/X0fYupHRDp+w4MWEl4Y7Am4B00tcfZRGMrqsrhvOX0dDV+UC2H01BXnnld4Y0SOHlXYfEMoaULBtvMcLUHb5V5OQnGOuqqxljBJUOfBoU1SUBcz0kI1M7StVsA2m6DrSu+efGSTRfA1ixmC86Xc2oPjXdUFi5OF/izBbIbqOcNUQTTLPHe5cmgBkHtHN5aFvMZs5NTYoJt3+NFIGnJO2c1VcR7sDZijB0F8Z5V31K44GQyB8qII0atVBK0HJJFGZh3mxb3+BzE0PcDWeopKZwlc14oMoA0nUD3RTGLzzylxipYhn1AgPFvOZqkMln5RR0v9kJkXMllCnfSd1BKCY2OnpURWDCtNSxGspE1lYI5r2HSn1KyT//W30cBZNIEtooiayiRj3xTstNp9++sPJkZneVMqJcVxVS23UU4TLrLfcQ6DwtqM/7bC99jXTbGLCb7x7jwKL/yw0g6vJeZJG6O34DJtuzD3iO0y9eUo3377pR7y+gLi9jiE4+OtnZIxvup3tZvIakE2iY1IiIUhaplMnOKAhrITEMkRTWIUpJJCdYpX0ZWbCkHAURGA96YvDqZDGNpVoUTaP5uEkQUkqnHGpzVkVSg1rl3WGtw1mG9V3Z10VQLo0pgonxLWhFjzqXAWB8dFGpZViVGgIcUwuE89w807aFQeZv+PB5/+5n8jlr3j+3f27Z+9Yonz97j1ddfMQwDV0PPLAW26zUvvvmG3W5HxFHVC1ZnT/nVv/g3PH76hPPHj3n14opXr1/zJz/5U9abQB8EZz2LxQJrd7ja45sa4y0vv/qc29cveO/Z+3RDjxWoKs/J+SOeX73mermims2pT054/MkPqS480Sp03WbD2pHL22Zkowk9yWkqQL/d0LcVEiM3t2uQRB8iz1+9YrFYMfSRFGF2ckFoe/oY+H//P/7v1GfvMT97xI9+9lN+8qc/w9pM4hx6pF/T375mtazx1rDZdNT1nMpaFnPDh488MzcgQ4vYWZ4XaZy7BRKuqYIRY2pEFJqvoVdFPmoKkxmD1yWgGqJqoyL+ijy1YrCxx8ZBc95jwhiHc6rrmqrCuJyGEVPWOIm+bVkuTjG2o1QvQCrWt4G/+Isr/vqLl8yfbFhcVMobFiLSD6S2Y6gt27bl5cuXMMx48mTJ/+Y//Y/ZpRZJPcY1nJ2f89GHH3B1uWY3GL7+fMum7vDsOF3NeP36Nc5dcnW5IaYZjz7+Mbu24+Z2w3azY+h70hKG1LFr1zR+wfr2mn7o2G63vLm+xohQ+4bNpmWz3VCfVHgTWM49vrbKTxUHTBx0casCSSHzPkR9VyJUlUrUqvLMm4Y4ACmQ4kC326pTH3q6YaDvBpoEKSY2t2uG4Yq+XZMk4auGqplxejLj7GSpBeLCAMZhgTYJt+tb2hCJxmKNJYaeoUvUlcW7GuMivqqwjadP4E2W50mRhbVzBJOwaPqBNUBQXi+bIiYFKlMTRXBGq2qpvhFqZxjyu7Ax5HRvtzfY2NuV+0DXxAoTraKn6GvlGtPFe8PV9RsW85q2W6tujZm/bqFkrDHFHP/K6O9sY0Gxgw5tg70ZJOqgf1srakimNciOQxZl5pQ/8yJGqfaC2gFptPqYXKn8JpPO5bSF8sKK7WzRAP7k9GJPZLPxwBa6ozmz3WOKDClOdkE4HTz2xP3N+t0gmUZQL2RtNl+iLjgU27QEEfSe96CJS+rBQ7o9CQcvW35XZKNARlqO32iCrpnaKiXIMBKem+P3J+VxjjrzwPia/HFoEpU3W1Cxh2GGY0TaQ+3dgwxWFUOSNA6OEk1GNGcpYYgxr7CTfaf8wpSboRDsFSWpnUwxsNtu2G6F+XzB6WpFVTcQE86qY3O6WvHRhx9xslrx4qpTmJtRJ8lZg3VGSUcA7zzOV5oT5V0Wfp7V6Qmp9rimxuAY+oAkqKuaYE0ucxOAgJRyizNNeVidnLJeb7i53tLuWrq2p0oJ6xPrF1dcvb7hZHnKdtuxXm+1jnZl8ZXLpXc8i9WKxWJBXXli0hzJaAxhSKTQK1Lcgq+cwtVTIsZIVRmQwDDs6LqdOj9Jq3GI5HxDa9h2HdfrQLWEuXVU3mY0QAKUr8FhdUUlWaxVZl3vXRYyUHkly2kyuU4M0A9CNBHrAsmo8R9TpB8Gtps1q8V5DgYpDNp5hy8rHflf5T3WWmrvqeuKtu+xxozIDCTRzGpcvaSaVQwkjAQldCuDMDHJ5cxZ6UXuoPwfMfYKCzXqSoXYKzGP0wt4b4ihA+MJMRCjyfBuJanUagr7yfNwBr05EJL7OPghYF4KPfH+QD2vBOdEMoO+GbcVd2UShtBTi3NrjDpTVleE96RcJS0h390cOoqFnHVP7FN2KaKngNrHWK/dv+uSnXvwLqSENEpgI4vlwgWBAZnk/I3PnpXNRMne3+5IyXGLPXyliJkWX5zCHI9F635DudYIoEt7oW3NPV/9XkjlfktxZgU5NFTyPaZEanfFvSpuBznAoQiEiFCIOUfjOmUjJZb+WkUeJF1pTFEDP5rWoIEDY00GHiSiCCGGMc2hdMZOVgHI/bDWqhwQ5Tspz2ANGizwThEQJaA4js0M50sgVvOjnVWjsoxXm4wGan3eZkqZK4NWfEj77BmTh4rGfhTJNTHL9p+rKGANhOx9fzNe5/jb6d79tzn8LkdN7m41051/bH/QbT6v+eSTj/n6t59zc3ODd4bzsxXeJLrtitVqhW/mDNHQ9YnFasnJ6SmPHz3mzZtLLi+vuLld8+byivWmZTZfcn17w2ZzizFCM58RU6Dtdzy6uODnP/sRn37+gq5Vm+a9Zxf88pd/jfcV89UpTz76Hufvv0fDOZWo9LZJbSIrBkkDPu6opKPyiXkVCbHDENi1wroTdqkGI/REtslTmwZXg5NE1ThWFi7fXPPf/3f/X26CYX56wT/9r/8bfv7jZ/jK4UjE7pbN5Ve0r75ifjbDNRXxZgvzU+rFEusc25uvCe0NRIWyjwa8QIqRMAwZvSSUShQmow61HLqiEDQQaLE4rLcZ2ZkRCoXfpRiYgq6qxx4vA9K1xHanOkJXwDBOA/EpBIyxxBQIXY/1muNvAUm6cPX8+RuM9FzfdiAVRhqsqbKt4ojOErAY64kJLl9fcjo752T1hIsnS+zC67dxliSRtt2xnNVczCzvPV7xD/70YxZNonLQdj1/8+kXVO6SEGrmixk3Xcuf/U//nFdXN3SXr+nmc8IwMHQ7ZF4Th57QD7TdjiEMNFWNcQbjYDarODlbUFeWV5srJSfMpRqdNeDAO+X7Or84xTmPcYblfM6js3OauqFpZhgHNlnqqqEPyr01REsbDJs2EgaBKAyDLsqYpDZ3CAMxRCqfNA22+F8pqS4LcHO744svv6aNgnEVj84vqKzLQeuEJULq8XaO87lEp3d4X7PZtTgMVV0hfsZ8XmNNRtNYYd5UWCvMFzXn56e0Q4frBDL3ji3pd80Js8oTSdgSqpO93XYYdj7U2Rof05QMn7lGJCZiSrRdx3JmERm0vH2KxKClSdU+BYvyTIzXzHrT5n/a7LgclAry8TuplpLmWAIY9+koDdAl0AWvzPeS8kKWnnjHe8WMPn5GU436Odt0LttVdp8OO71vsVJxatfc1cP7ZkfmSDOxNeXOOeO5hqn1q3fKyPrxvZAXRZCMDJ4ECXLApGzIVvBkMfG+m+7t1zI+3s31vq//mgZXAgpjkMEoGlhJ2tWOtIcw7nta4Xub9u7Q1hYpKFe5L3aTbah8zyNUNXBQ+ett7Z2DDNN6xvqgeQpKwqi3kwMH2chDDf/oLJ33BCcYp+y1Cr9Lusq+pzXKUFxFJtT1DA94Ii4NNM4wryyVF5wJOBOQ1JNihscnvWbseyQ7td5ZjBMlpKEnSWSICnfywBChD9B1A9455rMKYyO6lmyoak+SlOsdX/OrX3/Bm5uO3W5LiIkolmgsydTE5EhG4WC+WRKSxyWPwdM0DuMGwtDnMjYaIRbjSGIUliQaiDAMOhxiItlE33fUzQwkEUM/Cq+u//+z9+dPkiRZfif20cvMjzgy8qjqrr4xBwYDLIAFVyjkUoTHP85fdkkKdinYAVZmgJ4Z9FFdR1Yecbu7menx+MNTNTePzKquJinCaUpbS1dGeJibqampvvP7vm9kmiY2ayGmyP4wMEyFwwDJFrpeCaE8GVMykpT91tWNmqVguoD1qnS8M3TBsQo9wQU26w04V3vk1nddlO095kjJlhgTv/v8C37UXeAvLlVIWn3HHl0nttoEpdZtJUmknIgp451Vws8aNDGYORuLlIpkmJd8DTA0Adg8rirfimZnc8nknKBosMsHh115urMe64XnFysmGQnWkeoYkYzD4kTXouNjG/jphqrkPYvMpzqti/FBZZyvn82tx9pzVJj9nHI9XqueqP9YZifMNKh3PdHWaPHR3V9GxOUoBEWq1qqZHDP7spV4Uub75/b9zFF5tHdgOZYGmKowimaM7Fz2YbQ0Y45lHCVve/JZlD912to16xyaWVGezv8HAvHDwsT5G3bmapBj+cLicnNwYlaWHxIxHekOnuDjlvd8shaWz9TWQVNCHwYZ9N+ZfHqGIi5KXqj7qGTIup+s0ex/QbSWt3ZMMTVAoI6+Kk8RyKVo/Wo5Pp8xRtm6raWQyakcUUjGEkLA5aydXpqBYTWAap3ToAal1usu0C6tNtIs7oE+m6nBC+fdscXkctJMDUK3ZWQ5Llh7fAtSkThmYRAd15c0y+M4x08I5+T05R7/vthCH3vV7Z+2D+aP/z8wMv50/P/+WG86jMlImTjfrri63LJdeWxZYcoVY8wMk7oAk2hteM4TlkLnHL0PbNcbXjy/Iqa3rFeOLng6t0ZZ5zuGUUAcfec4u1hzeXlO3G4wxvDjn/6Ur7/+ihIjvTe8evmc7abDSSQAcRggRhwZb0DGkTNzzZ/90PODF1e8vHrJN6/f8+aNxYnwy28GxmmDNY6cC4cgBNNrVwI3cvVyw6szz7NhzfbVv2LMEMKWv/yp5Wz6DSYZjGTK4RGZrjG8px8CvgSuvCWQcWVHGi3D7h0lDtXxqvrGGi2NsrYikqosEG2rbEXLMFSf6o7S/au19iajWVwpOJFaatt0ULUZRPAiuJKROFDigC0TRSYmcYyPVBI+tXWGcdAst/es1o9s+h5Jam+EvseKQxnVa9hdtEyskJlKYiqZMRlisiSr8jTlRLfq8JueEidst2W3e0SIrLxnHQrO7Hh24bg835BiIqbE4TDw/v0NufRs/Rm3h8S7w8TNwz1bkzBlRTAOk4Q0jGz6FYd+UiJ2Y+hWKw30jgMuG6g8Bbc3t6So6EzFiSjxtqnOSZ4mwrrHIExp0jIELFIKU0yYEigYHoeJ6eaB9/vMN3cj9/cTZT9xzsQ+Ja3FN9CHUMkW1ZEvOTOMAwKklDC2J6MJq5gsgiNnSxGnhJReu8rFceDrL78k+/e41ZbV5oxnL19gvdeuUxZKDUgYSkWEtky8+hQ5J1KayEm5SJw1c0mECx3YWgpSZ6aVBAmVh6qRGFdlbSTTOg8VUBRD0Q57Riyh67Guw7seHwrrVYez4JxRRAjaftXUYIYGD1qXFZU7LaChR6nnmKpPnhpJ3304GmPBkbz11N1vdkolzrRoRy4UUWxd24HLm87G3IKd4vhvi+fbes32/w9d0+NxHNXSxmzjbp8cDaa548y3zUWd0FKfrSUItQtJtc2dpXVeMwZsMYht9zBP3gPVuf8QZXIyKw1NKQtb8rve17cYCHOAQfRn19Db1dlviK4KnVnGRr7lNnUW5fQtKMKUOXgyhx5K8+vVJ1m+f6Uv0ETTjC7/nvUS35+TobUha8a0OQYZdFx1ehspZI26Z+uZXId0Bt85glPj0tL6FFfHzqpjnWPisN+z6de4EumksGWiLxMu7dn6yLM19EYj11YK1lr64PEF6DzDfiBYo/1wbdKMPYmcDozJEHxPLxmxjiEJ797f8eoysFmvtc+0V5i5C8pbsLnYEtZr7ncjd7s9sWSGXCijsI+F59EzmC15sNxnz020xLtEsJntOtF3gjEJkUQa7pF8oLPKkBoQglHlpwarQ7Lgag+rFJNm2k2NLNWtaZ2h7xQV0DnNZBrnKsxan/1s5bgIgc6pwPPBVXIWiJO2CSpkmCCXCGTiNGCM5bDb4fqinBImq9OiDBl03tEHJYL8r7/6HaU75+d/fYZBSx6cPWZeTVGiyVQ0dJOKsixjtGOJ5FyjjSgpjwl0UjAmKzh/GVk0M/AbRJELi72DSCanSC6qULugQSa86o0QLM8uV7x+v6dbnzGNlpQSkjPeexzKjbHMiZ+6JB8eS+FzdD8XgrDFFQCKLEiNjj5tU3AnksMsndwG76K+h3n7VehXa73ZAoGGmXjyZHym0iTLE0e7Rk2ftGNogr71kTa0tku1tq55kGgsQ5mSq5POMeracIeGFlldKJWn09oYg2fPbentLZ3B5Y+yOO/DoymOFr1uU90IPT9w+OtrmP1NwwdBDWnPOL+5xbye6Gb9RSsNjqUI0pz1GoFo7NEihpI0c+dgzhK0wHUprYZZ4Z/WahZLWzg6jClIWRKsGRoZkwBSamfsGjWfHfL6jq0xWo40l9FotxaD0a4/tYzHOIO3dn7H3qsz1UpqGuSyvXcNKDqs1ICQAWNrYYvIPFadMx3fcanIPE8nHr3IMbg0v6CnDacaoqadIyenz6+qFleWJcORPDV+To9jkEJ/bwbrn44/3qMBha9vrgnWcr7pFao/aZlkFkMqnpw8fb9i1Xes+qCGXNHM6MuXr7h/3HM47Ilx4IeffsJu3GOtEGyHCZactDsEWZiGkd3+AMaSY2IcBuI44kLHftxzWTK9RDoKD9/8httvvsTLxHYd8B4+2z7w2f/up7x88Yq+v+DvNxbjLF98dc/f/PqaKWlQz4gwHSzbfeLS7XmxPvCvPvspP/j5M7KP/Dv7UwwOmQyP1/fkN/8LNmbSMFDGyPlmw0omhusDstlw8fwFu3RNirDeXHJxucZ3HusmsmiNOqaojRKc8mCh+15KzepK0bp/U2ZIuG67hl7QtyINubGI0hqjJVHa2lD3vQW8EUzRTgNZtJTEi8PjFdkVI1IUadH3Ky4/+ZQkkWACV88u8aYQAiCRHGOlmBGVmcZgnANjcdYRgvIjGJsJnWW17jHGVmne+CIiq87TB0HY63e9JXSBvu/ZrrdkelarHj9EDuMjpJFCwYqjFEtJlpIMWRyxGGIy7HaJwkToPIcpavLmMBDHiTdv3xH9RskUyfhgEOc10CswTSM+9IhkhmlQsu6SiFPCuEywjmmaeDgUuiGyK47b0fAwCHkq4ApTJQ+XlCgkkIL1FmsNPnicd1ijJKPOdxjvsWbAGAeEqpdUx0iOlGR5++49X72941Ac4ntefvIJ/+bf/TvWm03lh0BbyRu1MVvGvpSi6DwRUorEGDGielF5PDS5kmOuOrKW2UjV4E3IF6mtzAUxSbP7OWJZgfPq26REmSasVISIFbAGFwKdi3TOYsj44JjGQo4jJQesC+QaHLOzzSIcXdP2e7O26spvZUYLOfWd7p2UWk54ak/Ox7H2Q3X+bKM13apWrfvum8xj0kBGSxS08t1jcuSobBc+JEe+iGYNnyTH5n+bTdDsnlMFe0QnLuex/o1TVLH6m4XGaNPQtsek2bLQ9uRRv/VoQYij9fH7bYCPmapqnmgwSkqzQfV5l8+swZEntvAHv7WLNj9M57aIaPfFRtBZ6hVnW6g+RynkrAS6CJhadpZTRShRcMbxPWMMf0CQYUEKsoz2aBuS6nhIUxDNqK3OjnMYpwyrwVl8JQPTXsoLI1B0EZSUSNNAyCNuGtmaHj/eIrs3nHPgx1cdZ3ZiZbR1jbO+CoyC97U5Ss44YLtZ8+rFBatgMHnCGq9srtUXK6I9dZ9vznChRvKMaBRbtK3mMI7sDgO9S6QCCcfdQevTSs6UcMvXX74hF+H6/Y7hMHD2qIQ3lxvH83PL+dbx4sUzLi47/urHL3jz5Vc8HNRBfLWBkEf2d7esSwddwPmCkUwoBm88Roz27PZe5zEExBosWtMYgiMEj+8t3ltWHn7+gxd8cunp855AQrybjSFMhw0erGiApyIkTK3/khwx0nOx6ZRcSLQEwpsyZ6xzEu7udxx+9Rt++Od/QddnyIpm0Qy9dpq4vb3WwMJ8lHkDmWpQZMnYrC0Ytd2U/mtRwru2jU/dfznWzQmKiMgRbwqb3mPtit57nDGUFFmvA8+vznjzfke/6pjGiMkjRqS2cMrqeH0A4V+KqhNfZR5Xq+ea9/+TDdgE/tzRgSpWZSnDdD0sBV5ztObgUnOWZ6dZCa9cHoklI36NuHB0wMx8Ad1vda8dxWKD8dd1z1EIzmR5LShg+ECwtEixa85gQzlJzQqYhWPf+BCbUK9jWV5TUU7tpT690/GzpVA//eTpvJsqu5bBgKMymBVQ/feD4o3qRR59VG151WZlVpNyVAgGjkgAdbnJsZYbGEVW2Io0oL7xQq5Eib52QpCaCFRehoJgjcXVIO38LE+hEcbO1kFB92AuBUorCzEzAmcmEKU+17ympC0ZDeSJZsAcQGUat1WWFJH5WTzQ+ru3chOhllAZU+u7K21d1WymGe7zdB+NryWa0iLzu2hvTBafyQkk8iOvEDiiQ9rtl9GjOu6nXUBO9ucHVzwepsXhvwNa+afjn/zxkx//hL4LPH/+nDgMeOvou46Liwvl8jGBwprdILy7ecCFFavVFhd6utUG3/fc3T8gwPbsjL736rg6oETONmtKzmzXgW69onPC1fmGzcoTc8GIIhg629F5obfCyokGGWRiuv2a97/6W1wZ2PeW9aZTUsDtGddffcXNwxd88y5xfTNxPxYeJkVMYr3KPNMRp5FsJl488/i1oT9z5JDog+7pvIscbh7Ih0dstqTHvaLV+jXOBIzLYDve3d5xPz1w8fKMrVc0K0ZRTg3JNsvHGlSk6pljV6DGw1CD5KZU2aPnlnnPHw3kphRnHw3tSJ6MpQse36kBnEXJJaXBKSXPgU4L5JyI+0e8vMCUgbWzdCbROWFlJ3o7ajKLhJV01G1F8JjaCjdBGaGMSB6Ik8c77Wwgle/AUNisAmdbh/cCNmOMq0jZTJwiE2Cmkfu7O8awwmCJuXB9d0CGicuHSEyWN/cT7x8TtweILtONA5fnGwyezhvONxu8ATNNeNPhpWidQlaby/oq33PtzlQJODXTnulCpwSdBLWlnXJhFNsTjWXEYKynVC4rTY6JIiSLonCzjZTS0Wr8kUKJEykmglE+L4thiFrKYaTM5PfjEHl//cD9JBTXcfs48NM/+wuuXr6ck21I1rVSqp2ZhcfHnQal6jtqvsXspNWfMxkXkq45KVijuM1SEQZSMjZHXEm4MmDTnuvbG8zqMzZXrzRolEacjASXuDwLdD6x6g2r4DlbweXGI0xsVmfkwVS0jlQbUdd94xOpq5dTJV6ti2oHtTr9U2uMb1NDzFbO0ttdJJDMktOJhthYBO+PX/qojPyYfdtsnXbMZcZmUVZsyuLcWTIsPlvetyYflrc9ue/HxrawVTkGGZotokDIZi+d+hNiGu/AE4W/sNdPbtP+Vv2T2VDn+A4Wl/jgq/PvJ/algYrKaaSa87/tC7U+5QNkrz2dvfniNdkLWorbykMsR1+9idhKwU+upW2tXMOKItEcaKKp+ibfN6Hy/0aQoTkRVQ3U2vc2z+rECoilkYoYU+fGGkJQeJRk3VgidSkYrVSyXiG0KY54mUD2eDnDy8j4cAdppMNha+9ZK9p+py2QcZyU3EM0IrnpO148u6ALFlth8baCL6yB4CySk7ZXFIEcFW5kDBlL8I6+D/R9ACCmQsEyRGFIljHB+8nz1U7Y73aYKXERHC/XlrNQuOzh+coTTMHsdyQRPuk7/vu/eKlGBYXOJ+zjex6/eITLLbb3sN0yJvDe0xWPLY7gO7z3SNGewEgm5xExBeNQKKGxpJIZ9ge26xV/9S9+weOb37F785o4jXjT2vUUrBIVELwj1LKJUANBfXB03vDy2QWr9YbDkClZW//knJlSZpgy+wRff/UN//YQWW8NuLpZjWAkE8eB6+v39CXjnWEVHCEY8lQh1gZy1udJOeGxrEtetPVpTuHCsVtE9zRYQd3wmjlwNnO+Maxyr72MrePh7hbnfsRqFTBkhd6J1nw7p8oSEoaMWXY8aEJg3sPmRGDPW/o7MunL8Tcamfm/5onjCxUUZGblYo3gqA5fnjBFS4hcmfBlwpc9ebjl/aHQ/+CviNbrmqAZawsCmflOS6ft6FSqE61nl7lzRKsXFOZuAPNTaIDBNAXavPfjXWbZb8pRUeojVpj7E2d/ZkQ++o0fHAs5/52HwIweESkzY7Jtjq1pxrB58r0nQhyO73jWU9VplTL/vYh2z8lZSZ6cVXRRitqaqyEOxFbeFRV9DVms82jNjJ4wRqG6LdCmMLZK0ojQKNMkV04G0Xso+kGUsCzn+roV6Kb2dttDGtlXAJpUng9TiRx1DLZxQCxYECQKuVr51qgxWkQUKFN5GZaMyKYGERtCoVU3GCu635pDoraFdpAQaLDGI5xk8Q4Wr2i5h+YXb47v7pgNWCr/5cKShno8Ks/vYPQ+WRd1aSi13J9CDH/Mx+WzMzDwr/71v+Cr3/0OcmZKA/vhkevb9/junOv7B97eR4rxfPMwEC6F17cD14fM/VTI/YaH+J5dEtyqw67OuPnqK7a9ckV1wRMfdjgcziamwx1TnBAcwSRcmRAET8DkERsn7fZgEjY/4NMdPg2UKEwxMD063uUbPv/mgf/y6/c8Th0DawaxRIFiHNjKTWWhk0h0D4Q/+yFm7THboHX9jaCRwPMf/Zj4AOVgcZ0wDoVbo9lnex7YTxM3j9eEzZrN1Sd0Z88Ifca5gOWANVU+1MycZA0SaCcjtRlpDpdkrMlk0kedl+VxhJjXICmGYoQohWQM4gO2s/hgofqkwRpCRfxZMUoQi/KHpcMBkw50jLzYbDD7t6zPej575jCp58XGomklHYfaj8ImOBwJ8kQIG/rOYEpCkrbxlqIcNKYUKI3Y2+F9E+yCtjOscrkI3lVsXO0YEYvw+v6RNIzwds+zc/j1l7cMu8j92LEvhrMNbNbw8vKMdQfbszWxTHzSF7LdsfUTfjowPuzoVgLWQyU+91blee8c1hS8E4I3dE5td+8MvXekkrVkwBSMUxSt9RYXnP7sFN1RplLlt9rSKU4YY4hxAElMo7AOjufnPWOG/VDonbYfDT7guo5I4HYw3EcHIXD7fsevPv+Sn/3i5xpUcMzdRRDl9JAivHv7pmap1RFyFnIuNYCgSyanSMoCGTbPIpSMq8EtZxJOEiYNuDQQ8oE+PXK4/ZqHX/+K8JP/MxdXr8g5InkiODjrLedna0Lv6LqOkjPr3nF5HrjbHfC215Jlqwm6hFS0jjqSp0GDthIWvy8CAbrejzpvqadOd8jiGt9ikz7VlabpftrabGd9l01rPvhvs5qOTrY52n6Luy8vscRwtO8eucE4sQ2XwYlve6Z2pWZfNDtvFhuLhzaLXzVBUJ5O5gla9/Qz1Ac2ZfHnxRgWpsqJtyAfn9Wj/asXn+kOjJkTLALIk050T7/ffp7t4+aPt3d+Mv1L7IjU0iNtlKBd+hRh2vxz7526A61E9f/bQQaTW79kTspc50hYMz7rjyIWEauGWhWq1qpwKFo9X4WXX8yS4L2nIBzGkZIOXJ7D+dkZ796+x1vD5eULHt4+sj9EijjEePbDI+sQCL4jJpBi8M7jneAc2q1gruGqWc2iXBJavoFGG0vBSqrjbItBOOweGYYDZ5uXGGsV+i8GMZ6MUFxPtiuEPRuX+Bc/OucvXq0484mztSf4juvbHV9/8RU3xnF5seFlgP7csVp35Gzohh1x2HF3c0vOCd93PA4T4fIZF+cvMOdr0pRJsZCmhCSHthVKOKcki1hLEUuaMgeXyKWw2vQcfCWIqqSbpRSkJIVuiTqJthRMzqQ4EnwgTRPBBWQauexXyBgpnbalcsFju0B/3iO+4/FhRypo3ZxtSlyqss2kaaR36nj4YFXRTsrW7JzXLhnBUUQZtq3RzWjrmpgXVttoUqE/6PrT/W5q8CFhSqT3hVXoMaGjM4447EGS1sh2sNl47u8LJie8rXVoJc+ohjksUGbKgXk7Lrf0UUDzJJooHzlfTuTVjPRuvzcXRdQIcqJ9nF1J+JLpyWp05kFbhpWBTvbIeM27N19h3Sf0n/0lZS69afd/MpYT7XSUFC262RRHK4HXv1WYPLpvlsSyTVjb9p4W0nsOb0iTE1T4VnUim8SuDvapoJbFHU6PoyL7PUdpZQEVXlnva1u2vF3DaVAnY2j8BTOcv42oMgm3dquFiuCQZi6rQZNjViZyMYizWKfBBteUam66XOpza2mW9aAkVM2Zr4I/F2JOlEJtFVnIczlCQwUxE1eWysWghFuzRqzjq0G5E9IlfWHHGkbmh5JcyPU91RyRckHML0Jb7ErRnvTU2mspcoRgNnIolPfBUusgYS7toA2pfSdXp6cFoeZAg/79wzrTD3bm8Q/IERK5OOc4M4tZOFlSuj6W5y+PE8U+I0X+dPwxH9f337DZrPn0xy/4/Le/ZBonUomUYuhXZ4zZ8+7+ntc3ExOeA1veHwz59R33d5G3j4W/+8037PaR0awxyfPNw8gue4L0mLBFysA0RbzXfd4Fj7GacHbWkpMG3zerFeerNZ0L2CoTLFmdIUmkDD70ZPHsB8vdI7y7H9ilTLSF7II6WAs+Fm9BTGLbD/ziL37Osx++QFY9OWonjFLABI+sPNdvdnzzxY40dTw+RNbbnuvra159+in7QwS34pPnG8L5J7j+nNBFrO1p2Djdd3WnVlkiFT0l1aA3ZKTEmt1fGPlPjX1MrTlrZWqaUEAsxXqSCMUEinGILYTO4MSwcgFj/dwFzVRknZZpGYJz2BJZucx5lwnjLSF0vFgXyrOejc/s8ojkQkkFUqazmp2WPIFJXF2dcXG51qx4jnVckT44eiOceUtv6nurqA3IGJPx3hE6JZjUuckIyi0QRXgowlQcZ5PFTYbr20eer9asLzuCSfQu4qeBEM9Y9z3mcMuKwj+/Mkx5IHaBKzfx+M3X9GuP6w3j46MSKKKlE70PrHyAnEnjHifVLhYhGMPjfsc4RkqeKKageABqQEHfm3WWIL6iY9XpDs6BNYTgqy8AvekJndq2MRVd0VHLVa11RLHcRcPdFLAEcoy8vTloiUXTKXWOtMRTA/CH/Q6H6oxVF/DWMBbtnNRg8s4ack4YW8t08oBNE0YmrE01aTPQyUjIO9LtV9z95m/h9ob+Z/8HtZ0lkUrEIqy7DrfZYvo1+90jj/fvcD98wXrjiUXX5b6WMjqjwTBTpKJ1ZbG2jwv+1NJZbgYlzy/NY18430s4ver9el37LfpI3bGFgy8z/4O2y5YngYGPHKXuyXqFqpa1HHIBL5a21z42iG853JPfm11pTuznp7p/MZ/VlpzNiUUkw8h8QeqQ6/9LtRFOx6qnfvhWWlv2RsDYuMpm1MFHSo+bvfHt83p0CNQWr5Z4/aI6+x//9nI2l/aKFKHkXKMoBrMYV5PLVN2QRcufSg1+Yoz6c9YSnNeGClbt+CVB5u87/oAWlotyicUCX85aqhu++RkWtL6pqHPrjMFbx6pbsTKeMGa6ENToVH+OMSbkMGJKYT/c88nZhv048I//5W/5t//NvyQVqzwKX77m0WyI4tkfRixOWyFZyxQz+8OBsgVTDkg+UHIHySHG0mr3U55IUfteW3qMNKKcY5TcWTg7O+dse2C9PcfvC8aNOB8oY2aMEzd39zw+PGD2j1xtMj+7CqzSNV4iLvQIHcFmpsMNgqe/WtMHw3D/nmDPiXFERoekUksWVChevnzF159/yW/PXrF9NfHl796zP2Ss7ei6FWJGQufpQ2DV9VxsAjePkThlchGwhpQi0zRgjaHvezyKHIjJ4LtAKhlnDDlGctCWV1KVobNCsLDqA0PnyMaSsjk6TMaQkyAZ9o8HRJQlOCfRSK1U6E8GceoOpVIIRQMQJSUy4G11UAys+45grBIzSdHIXfXgGgRd12AzOOoOLEAuSErkGCnjWB3FA+tyzqrvMdVwM3nCm0xwkKMKF9/Waym6RmaHX450JCfbU49Sx2JAIaIc2x09jVmaJ0796RbSiD4VwREk4dOEy3t8ngiSWDlDcIK1I9YkrJuQ8Ybb29+yv7/m2c/+HKltcJpD/9SRahqoZYiX8Qb3ZERSzaFm1rX+7K2WrZ2qf6vz0G7WntUcfcOZyGaem+pAzsJzMRhpd6dpDZ6IT2ZHeHakP4zyWmk0lurMSqvFmydmIbMWsH3a3LRghKijWkwrLjmOubX0LSIK0W0ZcKtlV6bW7zp7LOUy1SCaCTaqE12K7ouSW2tJDahpyZIcO0K02ZrhxW2oQsYoLKLIXJ7TyhA0st3mQ713d7waT3kztN61vYIyGzECNfMpx/MEJFtm8tNisM7iRTl4KsK2zmeFBBpby5MUZk3Rsgp9tHKcnwr5aMr9Qx4OYTZ72vg/hjhaPNnpx8fnnz+pe7Esv9WWYb39chzfT+X+6finfPRrR7/p6M/PGGNUPY/j/e2Or97c8+4hcf1oudtB7jx3k6XsEgMHHh8jb/eJ9d0e5xz3+4TbTUTjGEehWwUeImy6FXZzQdiuMX5NNkHZjkwBPD6skJKJExwOiWHMqj+NIeaOUVbkbJmmgs0O1294zI6DySS/pRhDQls9l1oqJZVBHgCrUc5V78k5EiM8Pj6yWm3JU8FZx3CAr9/u+e2XD3h/xW4vXHUr3jxA/ywwxI5Nv2LMgZsbTUCNo6MQaoLpOKcGg22tlGX+hFaPr7wFrXRCj6bqbRPEjaRM1LBXLq+MmELBM7fSrQhKZ5WAuvNKTou1lNb+ucrE0HdYaxkPe1YhE4wg8UAeMmfrLe/u96QMLgScDUAiZ22ZGGPSrjlG6HtDFwRjIlkCkKF2IChpwPcOIxOmCK7aVWpjZnbDAw+7B3IeeX5+ibUOHzoOUTnDlCXKMcTE/ft3fHpm+Ysfbji3ibPeIQLfvHnP7et3pPMt286z7hwvtoESBLMJuHzg7ssHChnfew4pcfXZTzjbvCQEmIaCFEupZYXWBNWmRpAkvH/3nru0Jk+a3bTGqN1uLSUrkbep3TlsEe1cYtROFAveWdU56hCwcZ79bk/vPYfDhLaC1GfJIkxJOMRCKRFvhGFMiGgSyjfonzG1U5GqQKnk6aoGFNmLKUglWHRGx1E89JsNXhIm3uPHPd4ecK7gjeBRm2t6+IZ3X/+Scfea87MNfQdIBMUR6r6ZBqJ1rFaJPOzIkyf4V6z6wN3dhHeqp3JWdItDW9hTOzl8zOlsmt185FPQ5Ejzr47762g3aQljVVDfAsQz1GDA4tqm2jSmIia/IwZAa1nZcm/NWqTZDq3kcFEe++HRvtme9+PjPJ4r8z1bGcnJiXOi2zCXWRhzPN8c7c4lnndOdnEsG17e+eNBH/2LafX2UFG67d0sLe7jz3+wfWCOwYzTUZ0eChhb2DfNDC71vbQIEEcLuSxHZgBnNUinHqKiy4vOl7PaRMEZ5rmaL/s9ju+PZFi4TssNoIkmNcJtbYGi0euKYqjnW+u0XMI71mdbNqnnzHR06zW+6/QOYui6FZ98+gOGxz04bUm4e3wkpYgUoVtt+PFPXnL/+XvKCGK1pdCQhJhH4qiCcLff0fsNz1aW6fEeb85wfoUxBlsEIxEnCgMP3uOtYe6VXAoar9U2kbvDjpgKIYMQGGPRyG6N0udcMLnQ5ZGfP9/w6aZg04T3iSITJEvfd/z8ZxdcXH3Cy+fPeXy84fatoTMJY5SDIieNYDunNTC9y7zY9Hzxn/8rD39/w6+/+Jof/eicbJWeUIqQpBDjgDMwDQN5KvShI7isQi0LEhM0uBZHB8EGhy+G0HtccDjvCKsO6x3rdaDrPX60Cmm2KOQxF0qZKH0kjpMGCpLwzTfv+fHPfspuf6DrC3b1jLVbQ42+JrGk4oAeI0pAaYxo6UyNUirfQCJIxJdJs6fZgZhaT6fPcNwyzFlq6nrLOTPFRCeGi4tzou9Y9R2f/fgznO/YbLeMhwN5iljjSVhW6zU+dJVLoizCBO0eT3fDQuiDCrXqJLKY4w/OXxDuNMcQDE7A5ay1n2Wil4jPIz4NOBlxJIIFb1x14DLORGR8YH/7Nfv7d6y3F6zOn7HHYikz0nvpOp1Ee5GFojHzZypAVCKLFWw+CiJjStvwWm7C0fg7kkO2mwpYmTNGx+dtMmQ5j+bJ/ByF5vwdWYpZOf57MtEfalXz5F9MORlHHfzCWaYate2euj5L0QCg0LLwDowhU9s+iVSyHO34YJ2qLOe0Ttg6x9wHxLvjtStiKpcCWbRrTdLWVzHFWt7RRlcWP3MaOKjvpYEAZ4BIfY6TzH99F5bFOz9ZCQtFWRf/kgTK2Kqc6+Mof0TFOdiqgMyxZZi1lYPCsLhXQ7bUAINBEWRz3kPzK41XmNZzu2azSlsT8tQgY4HesIt6SllWu9TXvkQ3nK6lRizWgo1txerSeLI2y3KzfYd19qfjn/zhBFwp5HFi0/Ws12eEfsUQM2/vduxLD6utdnHxge7iDH95RnYOiR1m1ZOdJebM/ThgpBDGNe/f3nL3sOPhMLBdefa7O55fwc0BHiZHThnvA0ks9497DTC6gHt3w518ieu3GJN583rkbrxgipkhFoZ9plutmYpjZy3dpQb2bc7s9wckDrVcIeteb+1uKweLiGCtxxSDNYEkCUwgR0iTKFrTerLNRLEkG0jGUaxHXMftw8T9P3zD5fnE69c3PDxGUqbyzCR0Z3XaRlcq1F4MpUBOmoByJtfSgSoTc9vDy0OFlkXLq5SUT9FioKTVpfIPBKPJgyiiYtqZGgAuGFx1OAr9akWOkZvrW7ZX2l7373/5K376k8/oV5fYYHj/foe73FCKkMbI48MdazFkGxBjlPjRJqxEEKu8AQo5wxuDs3qONVmz+a0e36jdeXl+zqevJobJs1r1PHt2yZ14HvIeSUUz4FPm8LjDHt7yLz/teVauWcnEKgewnq7sSPt7usues23P7ftvWIUrhvEw8/6kkog5cX55hrOBr379JY/TlvMfBP7+l19zu4so5E5wnccVoyTezvD86gV56nnMD0yTYGUioEkgq8zgFUVnKLlgp0yeJnKM4AwxTkgWpYaQiLF9LaUQcpl0TpKndWxwIWCLBxyOwP3dA+MYsTbU0kNNlPXrhAtaPmgka7DKaiZeu5MkHAkrihDqvMOHwGrlseWAxEKo9pVHNPCQ9uxuv+L69a+R8ZbtqiP1G5zsoUzkig4sNTC26nuctWw2a1bnF8SU8M4yPD6wuXw5r10fPMVZrJSZW+LbHeuqq584rM0SOLHZ5m3SbBWOpa3fFSig2VhHbqOGJG0Bh6ejOtoGx1KDE3uhOffNET3CWD+4/8zd104pp/NxtEiWNuvSmZbTkxfOtAYOql1gjmUXAmpzlMX1mplTHfrWh+SD0cyBjMU7KYuZmod1atPOCZ6TsX6Po9lubZbl+H6XQzreqRl8HAMf9VxnjzZjQ5YdgzZLi6xdwmG8loW1bnaIUEqidXOR5fv9PccfhGQ4LvXjg6pMzXWya9WuaN2/EVfhyTI/QYyR3eOOQ1JCRTHqDParFYdhhGK4OLvkx5/+iN27L+gPXxKsobNK4He+2fKLH/8V3bNH/uOvvuT19QM5w/20J48jxJFViTzsJlXA5hlx90gJHXZr8c5Ug1jrzG5v77iwBsO2dqGwFapryZIZ06gRWzEYVwMMU1IEQOgIweOcpwuei87z2bOOZ6uR8/MeE1w1uh2Pu0zoe37+Fz9i3a/prhNdSKy8BdkwjhNxUrINY4TtplfyR+PYl46vbwqvrzOrK+Egnmf9Rsdlk5I39UKaDoh4rPOVH0SQnJjGUTMj4wTGUHLWvvcV+mKLw3iNOmdRZR1zJKSJcThQjGGaBmK2SFJH0xaZ2XUF+Ob9e97c3BHjAbjDn13RbS/ICIeYidPE9f2OMiX64HWtGA0+lZQQifS9pQsDvb3FmcjKekpxlGJI2ZIk4GxXOQfs7OAXqNlQbYVZshBjZJwGHh8jpvOM43MsllXfMewfOez3qFsPBIcNboZxiuQP/NlTR2axu5qAW8glM5/fNn2tP5WF2KzSwonQ5UyIe1zes/aZlcl4CtZX3ghqbZSJdS9GZHrg4e1v2d99qX9fn0O31Sx2hbNLkxoic9R5ecxCduG21sqmhZqp4keaw9lA802pmFlQzw/XnPbSJKE9zuACOjErKXM6pafK9yhgnx5HZ+80rPMkdFH/LafX/ZaCMsNSTxhaq0drikLi2j0b4RXqYIsBay2dra1MCtra0Rj8jJBoWYIKGwZKUmKtlCO5IRxowYvSppPWorGxAh9l8Qd4maYxj4gDYA6uCAqBq1NgqoZdIAg1ADkbQcf/zvNpRTtU1D9YTOWVqB0trNEyqFo60b535GkQRLTszLRnqvtDjJCTdhyauVfkCKPOol1njrGDD021ObStBeCL0o7SFvz8PB8EDND3aK2jrfvjqq8fzVWji2NRlvKn44/3MKmQhwkZE5+8eI5gidMAQL/aMOU1X7x74G7SIO3DYcfDdOCTV68g6Podp4HQd1pWCBTj8ZtzkjPcR8u+ZGLukMnzm3ePPE4Gk2BtLSVreeg4HPDOc3Nzz9/97n/lMTmKND6pDbEIsRjwDlsCw5TIrmP7yZYzKaQ08vlvfkMpjYMJXcszIlC3Qtf1eLNiHc6xZsXKW0pZEfc7UjKamHFazuWcoes8SRJZQIxnioXH+wPv3l7zzZtrHodMNtVUr8ZuKVKJo1WfqROgdoixiuDT7gTUso6Fvpk3uqtGsYCpzOclYdHW00bUuZeS6LxBcLhuzXq1Iay2ZNQmSFlmBJkPnt535MMtJlsOjyPjeCBGoV+dsVoJb64feLH9hM73dGEgiYAPJFGZcHG2wtuRNO0Ivdd2k9Q2ww0ZgCipNJ06JqJBj5giD/f3vHv7jpQDW6vlvt1qCzIw7h7pL87ZZSGI8OrM8aMLy2raE3zBuIwYz6cvO37w2U95/upTxjiQYofvC2vjGIYJnQ3BmExHYn2+5fAA/+k//SP288Iv/+E3fPbCU8wKzKClGvlAySPOr3i2veBx7yjv7xjjiB0jUxqRdIazXondbe0WVwQXNJCTY8KIo8REa79dUsThsE5r2Y1XcmPnHd55utATuh5fPMV4OiscxszDfmK1cpAKD/uB3binuHP6zRWCME0TJg4cDiMp1TIJhFVwrPtAqN2P+tAp8SgDQganJUS2BRiuv+TNl/+IlwEnia5fMZhe25tnDXqnXBhqEMUNkVH2bLYb1pszJHg26zVxnMhZS5K1I4nyhNgW8K9241MCxBNtJvx+dTLr8KqXvju2cJRzcOIoatJJU8laKn5qPbWfZ0LHNrjm2LdxcJQtc0DgVFE+NfCOV/99A58dfbUVTngt698NLQhiZvT9KQ/B6YCOFBRHG60yCc7q/Ol3ZhvXLD9YnFIRzbXuu07VH2gXtLLRxSHlWN774Vw1O506D0/4JaqP3t6rxrkaRnm2TPVKNXuUc9QkNSiBbrXHGsdJKd8vyvAHIRmemvJqGyp819b6DZE24Brtqm9CitYjFSlMMfI4Gnb7A6kIF4eBVd8TgmccB96/fcPae+7v7rksA+v+JT/78Y+gJPI48uu//3ve7A3T4YAyBqOK1CQ+eXXBD857zu0DL55lnCv0/YpSPMM+Mjlh5bZ0SfDG8erqJZ9++gmbsxXeDcQk7PcDxXomCtMh04cVl+dCHA6kcU+JI8Nuh+0zw5hYrfYcdg88I7FdeZw94HxBTFLCJreB8cDuceTtzQ192JNiJEqhd16VuIFsDUhWEsoAzmdszISwwTmLuDWvryPX9/DTP7vk7GzL+PiOn//5Dzl79lP+n//lf0BMQsSqEkbbN6U84aTgrdYgxtrOp+SCuNon+bBHjApr5wJTjHin7SBD3WjOGopTqFzwnoDBdQG6wiEVrh92WJOx1mrEF0PK8LAfKfHAr377Fc/PV3QhaCYjC2mccGSMHDjre15eFYx/i5hvwDgN7AyJcbAYOcP1L7H+AmyvK7KAq3BKKQIxktNYmY4TKY2k6YFpuCelHSFkri43OFdIQyTmyDdv3/Di6krXa5EaGFrs0HK68pfS/yTe2QIO7ailHrYx98+EkgVDJuRIl0a6NBDyjmBG1vhaW+hm/8kYV++oHA1MO+7e/Jbh4WskD9j+nNydU/ya0vLls/CUWVB+Hxnens9Wz/8Ysz7Oia1ZXv1Ovf7TwK9pDnWNIMhRFrS7zTbkk4GZxQ/foo/mLy3ilx95jsW534PE7+R4Elmnkm82QS6Sq18pnNQQLtiZjNWym5wjEitnCApbLrX8QTC6V7JidJxzipAqqeopi3VqwBXUYMcu0TKLUdbxmMambj+cjwYdbe9M+TWUL0VqiZU1aoTPlRxGgwVNaTurhTPWGGQRRHDm+CVrLMadtpM0NVhQyhH1AabOgyBl0ixRrb3WbjFlDkK0zER7Dn22Y/ijfXZcs7qKnXPzhliaTssswzGA0mDdVfW2ICZHqKZ8UD9lTsbxp+OP91j1a7q+x2A4HAYedgcuLhzGBA6jIH3HYczsDomL9QXjlBEyMWrLxVwMzvekXJiiyohtNuxjwWWD7wSHZXcoDGXPL3/9FSWOrGxhbYW7u0fevnlHigMpQ//sjFgMh2JJ+DkTlSUzZu3WEpxjNIVCxhqjCMbiwbha12uUxLeinjzM9ex5yowPket3A9Zb4mCY4sT9bWSYPIWCZGFKmYQQSyZV2aXowYBIh4gnFw+mU7d6LoeCFDOH/YA1mS4Ibr3CSIWwG1Se1Vp/xGCNVOKzpWGv9mZr9VtKno3khsZSZKTRDj3W41xHtkq2aQ1gLS5Xvi+Eq6vnPD+7JN6sIV+TcyYYx6rf8ItPf4i/HLFf3JCsJU1R2wobBwLjcGC0kXV/ycpZpt2BzfoZNlhIKCRGDEi1s8ShhWkeikOMBo1W6zXdqqNjw2Z7Rk+C0IM47UhU0R4rH/j02Zbz1SMvn6+wNiG2I2bLw27i8sULnr18zt39NTk9o/OGzvfsdjsMWg7rLWw2PcXD+SZwPva82RV2U8/DYLjfG569eIbpIkVGnn96TnrwfP04kLLXdS4ZL1lZGSRrgjEljNFyFES7lXXOErxFrMWHoAGGLBjjlSPDGiUbNVTkXqztx3XdaBDLkIH7w8i7+x2b6Cklsxsi+ylzPhayKBH63W6iDANffX3Ndu04P+vofWEdDOvO4Z0lWK/cJqZUsGYGK2p/poHdzRvefflbyuGRfqUlGd44MA5XdXmRQp4i03DA5oQpMOz3KHICQui5uLxke7apOg5izOwe92wutoC+T1g4rx+xn+YTPnCQT/9wLCGuPld5iuv7rkPf12wvtqz/ie3TvNYWnF+WlDbbstXoHwffSmjnB5j3c/MbPxzNt4+7KfYFvkJYjkRnZLZpTPVXzSJ4cDK8k8ueWqV1ThqadXHt5e9GZME9sfj20jRYICb+0CCDUgfOVsfxvt/2LE+DIAt7XaTMyeNWdt66A7ckUCOknAtGiiHnVvqr9hr179qy4WQlfOfxvYMMbvEQMg9KYcTeauZfoShqRGum2lV4G7Q4l7GGYRwZplrDXDKH4UBMkW7dk6bM7f0t3kLcP3LeC69efcKnlz/g5u1b+mD53evXfP5uzyNrpqR1btO0x5c9//JnP+Gvf/6SNbdcdnuwwv3DSH58JAyWoQiv7Ib1JmNS4mzT84NPXxHcBAi3dwcedpFsHZjAf/3NVzw+Tpz1W+g8LzeON2lEhgNZLGks5ClptrIGPKYp8f5mj+ksl9sr7NkVu/dv+OL9NW/3v2PdrXj5/JzpEPGrLQDRGGKdaYOlRMNlFzAu4XwhychYIO7gd5/f8cOXHd1PA+9e/47eb/nZn/8F/fn/zPT2EVtcbUmZsB58sJQxY4LWhlOVe6n/63xQokzr6LwnOMsqBHrvWfUr/Sx4KHbO9heJxGyYSmJIiQnLlA2d13q+LI2myjIUQ06GNzcDWRxiRiwTViKXa0ffCTBQ8oARw/h4zzTs6ULHdnPF4XHHcJsY8hndpaG7WFNsYLJurov2WTApQdL2K55MlozzlsP+FtIBYyI2ePxKeTmcCTXbaRUqWI2VRjB1XPDlW6KR5sPfqjNjUUMIwGaF7zkEmxNOIp6Jrgx05YAnYl3CWXC2dreoJQaykFRWMuQDj7dfcfvNb9j0UIwSXuHPKKZHxFSI+TG48PHRLjZz+/tJZFYdc2sanKoJeHsUtjM3Q6vwWirOowHY4qWtFOPbZOOJXWmffD7/JIuvHRXk9znK3AXiu4VjE7pPB9lIPqUoFFfFmmbl1DVtNXmWUgoxSX1/GdFqiHpWDQJQeSJMdYhFnQYRPXcuIWrBHWQmd1Qxbzkp7akEi94eORhm3EmpYzWA0a7yTWk1wiycamlrW81rbUVZSx5ayYStAQbTHPBqGBrTSiDqZ6WRIZqKSKCWngglK+iulMYAXhbIF32+Up38FsQ+WcAnhlgjhNJndXUu1DYyOG+fsHWcGgZLu8A6fQ+lpBq5b+3GTBULBruI4Mvivpjvr3j/dPzTPGzwhK4jFe1YYL3HdR0Zz8OQuH24pRTVk3HKWJz2ERflJ7LGKZmpdVjfA4JxygtgjNacGwwurPDdCtyKdeg57z1rU1j1G1p5IKKkXdM4MeRCdpacC9Y5YhwxzikhLIXDcNDSB2PwzlHixOp8i+s8FGE47DnsHsFkoklkSfP+2t1FPv+vN6Q0cYgwjJkpOqZoGaLBhMI+J/qUGXMhVmZ8cq7lDio/fAtQsgj8Gg0iTtOIM5lVCKz7wDRlvNUSR5mJyfKss+ysORY2M+j5yxdW214qYtZq9y1jOQwHvvn6a8pFZL2dwGi5xGa7Zb1acRgnrt9f8+riOW69Id29YXu2ZiuJcTyQHx/I2SipdoV1eRF6hKu159NXF1y4e9ZeO5YZs+JwKJTpwJkXCIYyJa7Oz/n00xds+lEDVUMmSmTMkfuHgSyBzdkzSraYAvEwcbe/Z78bGSfhMCamMfJwP+JfBsRMJCZKmQirLcasuL8eeXx3x+2oJWe7IeMuNtCvKWOcA9eFrMS7RpQ00geGxwThOTcP93z+xSOf/fgZ3dmau/0X/Pk//wmXe88v/69/R8xXFCBVnp+IEsWVlDFZwGVSUsSmjRzRM0Z5KwrUAILyabTgrgaVMyUnYpqIUTuTSEkU0b4exgiv399wdXVe+YkgZS2v1tIFYcgJi+EwTHzy8iWbPtAZR+8N3rTY91HfGlsDUxKRNHC4f8/16y/Y316TDrecv7ygq4gM5X5wWpaRI2XaU6Y9XjKGQhwjMh1weaLDsnIdSFF0n3UUSYDMPkLLMi/SN7MJNTu07Ydmq8j8n3o0PVOdPxH9+btsmw/ICJe2VDNvm+1rT3QxtdTnGGb4PfblrJ+lGU587M8ffvcjNvUiCvMhKuFoNyp6cZHYaKipj1xZ5t+PwRGzkDdWTr9wShEgx4uczEF7X7pHjh3w5njM9z4sVKJxmQMl9Q0vnuD0OIaBoJXAtNI47UiW1SYs0vofzPtwnpP5VenVyvyzbeGH2onv42P42PEHIBma0dpqUJrBW7DGg5FZMRqjrOultB7I7dF1F8VcBZLRbGbOkd3jI/1qTXE6GVOc2G5XXGw8l5fndP7AZh3oveH51TmP0jPcRco0YWqmz8rEeZ/ZmEdWZs+q17FPEcaUcPnAJFrfFi8O7O7vmPaPvH79BZfbwGbjNVJuinapMIX7mxHrHD/+8Q9IVfn/+le/xTtHsgqx9z4QukCo/UTFBcStMesNZfOcFM5J/QF//ort+TPIiWI7+ssr2J6RJENvsauMtY487EGy7uu6oHOcOIwj3my4vT3weL+nDB02Ze7u7vn6zW/Y7RMpWVwRilf+BGeE4C2HnLSFpasOgXc08kRnLJ0PlZxOMx4WhXpbAyUnJaMTSymZRCalRIqlZlQiGUsWdaURUfKZFskvMBXDkA3Xj4kse5wMdGai92suz3v6vid0iZIjb776mt3dDdv1GT/+SY8zSgaXhj1h2uHLAW88ufQq9kSwJVHSIybv2HaCKSgzuF1TpoRkJcy0ridNieIizmk/aucCwYdK5MKxVlsWwlGYyan0OALHTrbcMeirfbRFcBIJJdFJxMYDPcpmrazMucLtj9lWbT/ZuA5EuRFKgbTncP+am69/zfvXv2Pzkx+AdSTxhG5LFlf36ixBfs9xlJCtjk2PXLkU4ISvQAxILf1Ysv4s77WMFIiZ93ireXty4pOfjkqkzHGLamw2AgBZArsaDNjx+x5WasBmDjJ8mzL+SMS5usMzYWIuWflTEM18C9UxbQFWajlAdZ5NRS9oQUFFADQF35SBgSKklOfsxJwJkHyMildDoDnTppVGNAVbCXwajWfrM29qbMhaA8Zq+1ojizPVoG7XUVBCDTAZA5Wzxra6zYaQaUZiUQeh1DISKe3ZSnXO6/y0R5q5Fxr7fFPyxw3UiL2avXMk+NR3r8SThhZ/aOe2Y75eKfNzaHlG/Xluz8p83WMgqSFNZu+GZhKeBKDq36TCYuub+Pja+tPxT/6Y0khves1wBocVzd6PKTFl4fEwshsTMXvW/YppGnDOk6aJHFMtedS1HnPC24YqVL6nlCblCEAYYmSIif1wIAVL7ip3FZCnRJ4iXcnsHu54v8vQbTkcRrbnW+5vbrh4dsnDwwOrbsXdzS3OB3IpnG23TMOB9WaNRYlbY9ISB0rB2UyRNLcDLNlwd5sYx0yxKw7JEQuUoi0wLZCNZUoJMYZYMr5oq14tA6hJpSoXNI6pjomYht4SIGGKwRtBnNHOUyWqU3lS73y65QQhW6k8TlSUlZlJcY1o6QpFNONsjfJLec9kLTFGlXUWhsNA16/wPnB7d88337xhlUdWOXFxfsaLVUdJkev33/D3v7vh1293fPrP/oqcIuSJnokfPDvjf/OXn3Jmt1xdaleQu5sJs9uRzIF++wmSJw63d5z3PZ+8vMLnO0QSb9/ckETYT5l31w/c3+7p+p4SFfGWUmSYCofDUNulO2LM3Bxu2O+3TOvE+/GBEuB87TH+jHfDNcPdI93NxA8/fcX9IRPONbCwE4vHE2XCFyHEzNoGfJXN4xgZkiXGwrubA7/91Vd4u+HtN9/QhxXOP+PLLz8nnfWK6CxazqJy3mCN05aRXolyUolampsnpjyCWG3PKpBSoRR18HNOGK85UessNli8t7WduFTCZg/G4rpOOdewtRJRx05ROU2ZeH624tl6w08+veDVszW9U0YHv2iFLIjqPqs63ZKRPLC/f8fN6y+5e/+Wb373O/JwxyfP/gKCkkQ7o5R4SMaVjMtR+bOsENNETsLD/Q02RxiTyoxhT9cXnLOKgHHUjh1Fn618pCBBmPfARxPfcrR8Fh8yO/zyxOtfXv0De6cFE56erXMrdmk/HltXz4552+fIbH8sD4tTu6Hxey3uZz44m+oaLmzNjz2FeeLQL/42m51tCuZoxJFhwSy+aZanLD81y2f8+EBmgkiBY6/JxXWqjdFkWCs//tjb+7bDCMqBZ9r3j4jP41MtjsICuVrfTAtUozwiuRKJn7DGzYkdMyNW2zWM1f2oqLTlraSWT3y/Z/neQYZWR0wd3txj3VROhqICo70kwZJTouTEwhtBxCj8u9ZoWasv87DfYyq0qkjBOkNwAWeL1gDbQucF54XNOuDcxOP9LakEmjlJ0WxvsIKXgqSMW3WamRCLlEIfPHmaODw+8nB7QxwHbeeYLXESnFeBWXJVoijjuzWFYCy9B+8MzlnGkokpsj8ceNg/cm4PFDKu6wjbHjYrShHu7u4YxojzHb5bY9KEcZ7VWY9Yy3gYsF6fo9tsyIAtCllrKzZnzVb0LjDFpIRAMdL7Nb/9+hv+11++5u76gORuzmZLzTgEb5laPXlRckRTCo0uNMWJNE04Z4njiDWmlk1MpKg1dFIVvK1swV3nWNlA3/VYNxF8qOtEsyklJfaPj+weH4lRCXOSCAlhKoLNBWu15VMXOrbbwGrd4b0jpcLt3QP9akPME91mzbntCBuHCyMlv8MawdtzRAwlT3hzIE+vWafP+fnzwv6x8O5wIJmOnEbGw14j6lOkM44hKULE5IhNkd62sot8FMgNIzXDtBpw+kNHuTl+LWNrACsGVwq9jHTlQCgHSAdWDnrjNYPdnPyjPFYh4czCjy+YMjI+vuf2zed8/du/J+3uCe4HDOIwbo3vL5iKxbg6XpGaiWUeq3kqD82S3meONjS3virWuuPN8pk1iNOusfzuqewrVCKDEyVXXbUPFM3TYMNsaLYJajdobIB1rk9geU+udgxHNOHfgqELJbJwXo/K4Pjs+sYbwWNzoqXCzqqxs4haK7M2GGeVoVcKiPYvaa+8yHEsUp+3QdDEyAfjb3PjjKnGnSE4i7Uti65cAsaYk7FoFPyoQBXtYGltIxus0XBa7tb+pp2KzGzEiDTHpNQYXFFSzAqPLizLQvT9lDl40J7GMkOdbXX8i5m5IHTF673njibSuH9O369Z/F/XzVEJ6+1LRZdUfdPggUZro2cDzRyv0lAgx5rSU5W+LLFoY5PaUvSDLfCn44/qUG6UiHVe0Sy5YMSQixBTRrB4HxCrobkudHhnKx9ArvuxrkFRDpBc8hx8bHIrV4h5k2vWaAByHAd2Dw/EcSBvEyVF4nBg3EdkyhwOI94Ih90Dq94z7Xe4UpgOe1wIjNOElczhoBnhKU6UXLTEIdduKaa2K6t7tpUWYLTsoYhyNtX+DdVZkGqHZFrAT+OVFR25gJPPktoYRbsZmWu04zBweLzHBaccQ5Jo2e6jwNcrzEgmqtMpMpcg6iHzmUbKkeyt7mcXlCNGyfoU1Sg5Me129OsN1lgOhwOrznJ2tmVztmLbQZwmUghsz9aMX97wsNszTBPkQufgYmV4dmbYGMM6GGIsDGOENJIM7Hc7ijEcHu7I+z33779h22X82jE87tlNCvd/9/aG4Ht+/oufUsbEm9sD6/1IyNrZwVptxW6MxbqgAHDX0a0vsNsV4ewZh9jhN+c8e3aGxIRYx+WrT3CbjuI9fiOsz8/Z5IRLI27cKf/WEJESmOLEYTex7if2055p6jBJ8OL43a++4t3jax5uD1g3YYvDtWBxqbZFK62zSm7o9Be8D3ivJZ/ON9kLpRi0raUFpyizkrS1sbbI87O8xqjtLVXn5VRt0KylPl4SLg+4NPDf/OKHXKwdFxunJbtVr9H0F6axdFfdkyAN7G/fcvPNlzy8f8Pr3/2G11/8lk9fbnEehFzJL4MGvKV2zwD63iPTRIwj3q148/q1dhyhBsWKluoasZAjtiS8Kbhq+x4Zjdtx1Bwn7FFtT3xAKi6L71RbqqJMjtdb/PuRloPHM5b6rTEaLQmdhFZ6OAf0m23M0Xb54DC22pzC92EJnJMCH/3bRz4zTz+XI6h2aQNV22chNuZ/lm3TT8o+zOLUer1Z18+lwFqe8lTfLy8hUJNTS7/h9x+NloZmc9XOY0K17U8MjXZOvU/dL+3vpZLillyq7368T7OLMIK1Fm+t2kV1clUs29nYUR+nok/z73+n8AcEGXJJR6OrPmCjjDgSQFhlmoWa3SqqqNHe7+2NpZxBwvzijIEimWHYc+bO6GqfW2MEXKHrDecbR+dWiPXcjInDeOD+/o7cXTBl7fCQcwZT8Law8g7KgKWj7xwZw5QF5w2hc6zWHevNSuuX24sqWQWBFVado7hVhZrUGkArxxaTqBFhLEw5koGYE2McETzGdVgDedxzeIxMuwO372+Y9omrsw1m7dThT8K0H9XhbpM7jYQq5KRoO0loGU5AMgbNbppi2d3uefvVe8pE2zkzlMwa6DrH6G2FM6LwvlriMtdgo+dqT181HIwxWK+R6hA8BgtJswUNfoYBb7yKoFSrdRzcvrvmm6/e8u79Nbv7W4y1nG0CmFwzKglLwuaCxKLtH8WCWC6frRG54uUnLyE4CAWJ2q7T+QcSWWvPMtic6bmnk2/I7/6Bi/vfsPGW304RiRsSmSJ145TM4/09zvfs93v89gwD5HHElYirdao6v2Y2cI6lAk2FMK8ZFp+3raGZlUQomZVkOgYCE85V2NyxF8xCtzQAe41eVqfYSsKUgXi45uH6S17/5h+5e/+WH3/yXL/uAqHfYtxK62QlV24cOR2ioTpVnIy1IQ1OFEaTpm2h0/b60+OpIlt+1u5RZv+2qaK5jr8JYznO4AdsyW2KmqNLQ1PVERUQpe/8YGxtOEfbtcqq+XXVe9QODlQHuyCzwG4Z6iKQpcylAU1tSnsqA85pNwUzO/ZFOyYYg0IJavBPCqZWB1SxXztSHOk5Z9uCasfVgGyDIztn8d4tlpAaIm3dChpDVD4HLa2Ys/yLyT95r6JyWIMGp69Ux6MDyrnV9y0d+RZYqKaCMbOSBI4kSPWFKxlZK6MxC7SCKlJ7glrQgbTikIZe0MtWJIP+Z56LlmkwiwvMQTddULTVpOcc4aFt/prxVupzN8NiMaSj4bF8b386/miPx5tbZExcvXjJygWsF/b3e+6vHzjsBg6jZR8NQyqUx0dSSmRruKmt/FKOHMZB7SApxKgyp5UbhVqf7q2l6wJttQ7jwAZHikn5juwK547cJ64iCIzVUkVjBZGsRNZG8MHivVEnDrWDpjjOXWpEjMK9RUgFslTEorWUbAihww2GXJTA1RpDMdrauQ+enAK9s3TW0Ne/tZbFrftOrsavGuGNdE3UObMWZ2o9fMl4q0F2bdus9f3HgmaDluWVWR7MMrs04jGZN58Uh5SMkciRZLxgjcLVxXpyFsRYijUc9gdwjr7rCT7QBUvfdYSQcZ2Wn3TFsVqviDHyu99+QbGOZ6sOVzLeFoJiN5EshNDRZZhEkahxHEgYhv296ousJIGl73CmwwFOLKSCXzmMKeAEUxEmMWVynnCm4G0rLTOMMWF9YLX20AWkFIb9npQyq00gxkQxhfOLM4pE9vsD3jm2F+dse8Pj268YDyPBbgAhRZhyIY0D2RemkjFOAzq2WL754oZffx3JeQ3FknJFXVb9RdZyHtIE4shFavZTdZVzDrEF57WjiLWWWJWmrWgxckJyokRNSqaUSKmQi9X8hJE6zxOSor5TKWw7y4Uf2OQbQkk8e66oHcsxyFWNsWOG1yiBvTWZkvfs7t9w++4rDtfXvP3dF7x//RWrHjZnARfAegPOYnzAOUOqPBRxmhjGEV8SYrRL2XAYGQ8TKWlbe4xhPOxxsoYSkTIpzLx1l6j5l+OxVFanOhmOzq9II/GeFdnJb8eA+NEyMCe/H4+lpfZRZ70qfuUnaEHSFjD8yHee/C5z+cZpFr/ZncfAw0cSYE/GqUjbpyd9+KU5psET2+ZbbiDC0f6dzzHVUW9fPdq3R4Ym/fJH3Ww5nY35Tf0hxsFsn7Yv2fnZjDTUcXu/ttpehdSCQ/P7VoSCrb+21SKlzFeXKseLJJJRZLuxc3pnLkXV/zawQeOk+P3H9w4ylBTnly3zjB2d3tkalkbsZSAHTFMUooauMY5UMrlkNbJLqTW6makURufZrtdKsGMyxWT6VeH5c8vQex4OulyVkV2ZyONUKElJDqVErI0ENyFlxBSN0gcfqpOQ8N6xvTjj2dUlxqLcBXhl4C3az3jVn0O/YowTYgz393es+o44jRTREgQrCivGag0mxdbsB6Sh0AWHC5bz7Za4M9h8TR4j4VL5D1abDRhDnARjHTlGHq6vYZroOmFz3lUUh8w1zkqQrq2RlKRF67id9SAaSc1AMQobtE6ZexUGler+MUeiTqjQQqtOknd4r+0srXMKZfO2trQWJBdyyUzTxBgteSradjslTJzwzjDuJn717pqb23umKVJyxFk485cYm5AYMTKx6TIbl7BphGiIh0RE2Kxhc/aSbntO2HwC0yMu7TFlQFImdJZxesRGy8rt6HnDeP9L7M3nvLIjD3bF2nec9YHdELVEYkyk5Hjx6ke8+uGP+fV//EeerV+yDp7eCes+aM15PhKcNqENDS5kWG762dU01FkXfCmEEgllYCWR3oE1WRE5RsA4zdqYsgwr1J1Vd5hTgaHKfiId3rO/+ZLrr3/F7ZvfsQqWq6tnFfnTk7stxXQaFMmabWvM1jOaSzjWy85CFQ3WzIKWeX3UR2yz8FHvyVRn7TQA8aGyMcfZrPwDtEqKeTyzAD7Rr0dXrym2I/3MUcKVb4mSS7tRcyWbcdP+ahZzIe0jnTfJUoWp0ewZDTZcn13AtNrkJhPnB2vRAamGrgVT218itc1lvU4NM7T2jqb+oYEZGtrLW6dZ0lq+ovG9XGtaGyeClkGYSr7ojMHb4zye6uSWFSnzKBQBpcHanMqMaigLV3wmdGOBCDmqq6Pzbo44HAPQoM5Vl2tdY4bSSjLq83IsFWpBsaPZZLRrDlQlyLFkgnaRijxZREdaYGqeB1kwKle+B2PKcRmUJbiznneCFzwNNLR/v8NO+tPxR3KUmEnjpDwBqbC72/P2/TVff33N4/2Ox+jZJU1Y4Gp2SQqMdXVZiHkiTrkie0rdEoKtgcGcCtZZgnfs9jvOuoBMia7r2e8GSiogif1+R3T3HPYHdZK9AaNQ5oLM9lMjcYSj7NZsquokJdKTSrSq9AelZeBqNybVdRkptcMWUlGo6tSZUugs9N4SLMSajRUUqZGkBmFNqYL66Pg0pJRBa51dvaYLls45tP1gUjtSnwLtUNZkcP1Uqpwtjf8F3bNZa/qltUMUEMlYlDg2U5Tjpto9GWG333NxfqXtha2SFfbrwsVlYDo44t5pYDnDze0t4j3r51eEKWIRgoHOGkxJdJ2hBEuZtKxku9mAOyMEi6TWOlQwqJPusXgRTC5s+hVTinPHryXxrKEwjQdSSkySOAxCSZ44asIvj4+kwfNw88j19cC6D2zPXhHTBCXjnefZyys2G0favaYMX+LspEiClWdKSTuGFS1jiCWTyx7BgUTiVJiyEqCblJnSiObZSxW1ginadQGR2k3JajnwdqWBAYE8Rb1+FHISsMI4jlgsOWW9f4qU+rPqiEZILHgjOMm4nFl1jsvNilcXGz49s2zkce6EdJxntM1yo7YyR5vOkkEGHm5fc/v2K+LjHV//5nPefv0Vzy7WdH1gtbb4DrCG4hxiO5zzusdKIU0jpWRevnxOjB03tyPPLy6Yhgm/PufVpz9hs/0vvL8d2GzPyMnTdR2rfl3RgLWZ/FMKhY85obP9svToyuL0J7bPHGRb2pTH35c3+fB2zalsEYR6jRNkZPvi8rqn358/l48/Eub4XFD5lsr8h/pvlWdqFMy28dJQ/RgX1zyCJ39bIjzMR+ZgTnTNDvXiWWZbhtn2OX77+0QOjnMlcjpL3zLc05hItQ0bL5U5mX8zF7O05N08KjmWdCsKysy+uOZcj3aVkcZDofLblHI0yly1O+d1IIgRRX5/j+MPIH48mvJ28ZBzZEN0sKU6otoz2GkduxwXfGsr1rJlzcEo1Rgcp2mGTtkSyXHCMOHsyDhe8/ad5/5hA6bDGq9ENFnZ2qkCJARH8A/ADusEMR2NmGmaBkpJ2kEAUQbphwc6IqUDZ1esgZQHDofCFCesgbuba8r5lmmMGjGOkaFkppiQ/Z6U0lwrZhHSMHCYBrrtGeNk2N1FJUnCsH944Pl5YHd7TxHh7eu39H2PAYbDji5YwnZdyxPUUTCVkT7nPNd+5qx1jQ190Gp2GqwmZ9g9jjzcD6QoSAHnjLL5ZiVcUlhkZprW+OCZxgHnHMM4YownTRHnV1r6IlYzBl4VQOc8HQ6XMuPNNcP7Fc8//YTH22u++PJrHncDXb9ms+oJTrgMBtc5Hh7v6f3Ei4uOs9UIZWDcJ3q084XrOkxYUXyHv/gB0/VvmOIeskNyhzdr3OQIfmDlXjPd/yP58BV9iPRXG1ze8KntmW4t7/ePWFu4ff+OLB2Xr17w4pNfE6e/xZZEH+DqfM0nL57Tecc0E0s1h0QzRqeCpwnBZtxptwhnMn2e6MtAJxO9KwRNQzcxcKwLR4W5saYG4OqV56h7wZaRMt2xv/2Sx+vfsbv+Cs/AZrVhe7YB58B24FZkXIWMVsOy1bh+mxwQqb2JnyqD5pg9OZ0PP2t/mcWBOV5v+cUZVrm4xWKG53tin+ia+a8y/3e+4wnm6yMPOSta/fcDGJ5tiIijYb6M2OrrauyBtfN05Z1RPh7NLrQMPoIa7DXTgLVao6x+wTyGItTPlVDs2Fy0ZverF60BzCozrZk7Osz7O5dj+UBjVTfUbhTVoK+O/Lc7v6fEavO7FOpzVpJGKTUKUtE9NQretNnT688OC8yBKiNmDpDOAZVaA6hM4+g7rcalIjKWQYaGgmlL7Yg8OK4P3atHQ8PMz3hSHjNfs+1xakeL07+fmCLl9K/Lf4+q90+Bhj/2Y92veHZ5iQVijMRp4vLykovzxPZBmCbPNCRtJRkc0ziQUsQ6lREtqNt1AWucdizwjsk7rFFSV+c9VnSvxilyPwyYYc+GyP39HSlO5LjXvdYNTOOBmJREOaZEL4WUlBdpmiKd90wpImiLcGstKSbNSCGklMDUFs2mcaDUvYjFyLGXunUWW1tltvi5yiVACl1NWLRAZxENOGQxGsgwx2CGq0avQv6VJLCYjEPIccQH5dKRnBBJYFI14i1SjrJ1YWnqZ0WDixqwhSJuJtTTjhZ6X1fLPHLOpJwwzkCxakPlxGEYcC8sxmWMi6zXwuWl4+Ac14+VgBlHybnGjg0lJ4yMWDsQ7ABMWAKWoA51HOn7Dr86pw8dh4eD6lpjKQVSShjjQSBNif3jnv3ZHmMs+2FimDKtb4Z3btafznlAW3ZKBpuh73umZPE2kJOr43B0Yc1ms+H8+XPCOhH33zCN7+hWEb/VcuDRdGCjlgnnR1YxYPIaaxTxmuOkwVYbyCjfyBQ1iGNKqYSfELzDEebMqrEWY0V1T04YXM1AV8FtDWJbxyQNukmCzns6b+m9xRtDsI5sNRglKRL3B7YvnvPZ83N+8OKSi01H75X0/eiEihI6GqqdpQ6TnYMiCUrk4eYN77/+imn3wOd//1/5+te/5sXVllfPz8jlgb6zqo+MxVqvASqpOlk0WfTy5XP++l//Ne+/uoX0mm7dsd/f4ELPxfNP2Ww2vHn3DucU0bFeb9luz5R/JJd5VT9hJP7gmO2DRj5O1V3N43+CrANOSwaXGqlt/Lqfnt67Beql2jfU4OhSz5fFq5z1YiUn/H0Od0M+L40HgyY3jmULmpmfj6N/X4VWS+ot7PGFbm626BwQ+XB2vnWcp2iL4/WPeIUPgwpij/NrFuM4tQMW6IfvOMyTnwwc35GUOi/l+CJq5s3M6/9oF6lvbWbbXqfNqPw31diuMm35zCevaGHZ2Ln0pX3y4VN+2/G9gwzWlEp8Z2bGeV3jRYnDjE5Em3NxmpPypsLkWLzEmk00prVvK+1jcs7EKWqf8iQKkRLBkkjpwOOj5+bekYrHGDf3DlWzN2F9ousmLA9gD5RsyGxIyZKSIZVUF6Aq35gSu/2elRNs8aw6j/eJbCL7IVFiwnWWUKM5KQtDFPIhkoJC1KV2EaDCorUdXCFNiX6lCiFHZYlOaQfRkl9ecni4w/c98TCw3Wzo+g7IGlSwjcWz4Cg4Ms60rJsKndYSsbHzSnV6SynkDCnCl19cc/PVDc87R/AdfdchTDOxnjMa+FH5b7BVsQUb8NbjjGYbrXW6aL1TvgxvWVlHcA4rcPvuG77K9/z8skcebxkfbskFogVWjpIinUlcdIFcBi5D4Wrr8H6PMSNpiqzWF1AMw25kfBy48D9ga58h9MRoMdM5kl9Q0pouQPDvifvfQLphvQ74VYc1K0yyXInlze3A1kRs1zGlHd3mDAkdYdPjvCqfEDyh82y35yBOeyHPBCrMAbTTQ6CWsEDE5UxHpHeZlUS8iTgn+h4pFT6vjpt1zMnuJvRNc8TataVgiZTpgf3d1zzefM3u9jUm7ei90PUWGxwRg9gAbkWp6IUmII5Rx4UgsKeBB8tHdNzSOc9HcXMiTszyh6fO2ZPL1cjvMef/4WGqI7/821OVcPq9p8L++9SGVbFp5eQR5zE0xdQc/GCOOlzM3BtYTO0VbaoSF6uKfNbZCwdUmjN9zEM4Z5jrI01zjPVGtta2GtOy87XsAIGK/GrtL9tDGC2HrvaAQktNcdhKj9xAZo3tWJbjm3+ylVNAZXGrKCmVa0Jgkb2Xj5AdzbO4uHYN4BRFe2mf9orAclq329onNcJJMS1gx8KQMbPTMCdpWIScngY5yrEEos0rqLNxHN2yBEo/aTtnWeLxoVmw2AsiJ3//U4jh/z8Oaw1dHzQAbLTO3PqO1aqj6zxnqzVTSMQsWt0nEeuE4AMxqz2x6gK+63hMe+XpMIY4TmSrqKS+6xnHiSyFs/MzdtfXeKME2K9eXrFeebKzXD0/p7u6JLy7031kHBo0tARrazci3TuN9NWLJzhPFwLeaWDBWafM/kUqD5CpMXIhSyKXyDhNxByIMhLFaUlF0v1YSlanM6fadaVmritCIuVCNpYMsyA1mFrfWwOqYtASCJDahUBQiC4lY0xCOb6klpQZ/R1pwraWVVRftSwDiBmPlhZILvMu9c5poAF0vEUJIkvlqxqHA6kS8+Y8YO1IigceHy3vrg1FtlgblDvCqIEvjFh3oF/t8TyozrWdkiEmGHOmoCW1w2Hi+vqO83XPo4u8uNqw2a61jKAFimImjhFjArt95uZhYj/6Cg4QLQWufC/WOJzVoMfweKC3a8aDBrv7sCZNE8F0XGxecPnJFWaVGfZfk8drwsph11da2uEvGMvI2bbaGSmTk6OkgiQtuSk1ICw1ciapaEvGbLQax1bixKIcVoJmR62x2s3HOlzt9OGdQzWAZkeN7XBmxBjlTyimTq9VWyiLrnIpGSHhxPOs9/zi02d89vKcbd+pbWVOZTjo91v5L0Zj49p5JIGM7B6uef/6S9Ju5D//zT/wm7//JT/97JKXLy9Z9YUpG3ynXBIaMLEVKRMhRUyMmJhY+46LzZpH+w3PtgnXw3i4J4vFhw3PLs9Yd28JwWBHQ0wRG9xMLExdo7WN1Hcci6SXzN86cdSPh6mApu9XK3/M6FTdt7TrWnnqfPnqs81DN4vBLP/whxyLTjEntQTHQlSKlnTNyPmlEdBm5iOPuwhBLEb/4VQ3O2GBb2UhWU7u8+HvBsqxgfl3l0N8t1uuNk9DZC7WxPxsy3ddZeIcgNLPWuJEg0RmDhS19+pMs66qZVWTUUh9Ex99AJkXXAtMHxNn3+/4/sSPaTpGlk3LIukTHEEa+pkp2ipHROb50qhzywTWB7IV0dCsWmOgGOKgJQslZ41eZ3MMKBTDbjcxjBAFcBaPYRwNuVicW+HshnHvMKWjGI9d9wxT4eb2gSTa57lB3b3TSD4FQtcBhnHKWK9ojFKKOuerFcYFsJlYPPtDxLteW1OhPeKl1iTmFIlxQjB0fc96vWYYHdvtAes6Oqs1il3XY32gIGwuz9lsN3TrQBoP5HIA11dCtYw2X9aIrDeucdioIsitTsZWZIiQs5CL4+GhsNvB866j80raYE1dYJWTIax7VusVXdex7nqcD6z7Qhc6YhCcUfKhee2XQimRaVI4o3GecYy8e/NIGiPTMGj0yzlySVhTIE8EEmuX2ZjI87XnbGXou45CJmdR5mFxmJgY7u9Yn99DOWCkw5WX5OkZyAVhU+jCDdP+H5Hhls4FfOhwwWvZyRDZROHV1YrV6hLp14xdR9evKCVjrdAHjzGZbAzXD3e8e3fDs5cbLTMpZUE8qD+IPZG4WElApssDIY2sbWFlBWca27Y6o7YJa6Pty2yjsqsBJKjRRNuMqYKycA/sHt+wf3jL48035GGnnUKcow8B450KONdj/YZcDGIXELP5WAiOJ5HWjxP2fJekNCfnfB+10lAH5fSrT65aI9lyOsYjUdPHhLM9cZM/OlJjjtHzOpaZf+HJ2a0yAoByhBm2muMm0g1W13MNDEmtBF0qgaPSUgPQykyjyRHSfwxCtI1lagBRnfOGSpK5bOGpQaFjKtXpFxRxUaAcM/NFFF3W2kWeZj5KVecVHl2hZaVYhXkLNLb4j75xw0fqJGX558oBc+SU0HZ3RyKhhsyoE6dGq2mBp4Z8ay/i40paI/azil2+1foulqDRVr5zHK4IWCvMtJJLdbZ8JoGnz7ecjI/zlvzp+GM6Hh8fuLq6wgbNWiOllg9q22PNRAvWm9rFwGCsZ7VeYcaRaRoxInQh0JBJFq1H987OaAZjlL9l3fWkrsfX9o7nZys264Dt1zy73FBWHhcsNjnEOWx2uOrAB+vwIWCcOnWaLKgBgGpXTeM0I55ECkXszI+iAQiLcw4fuuZH61Flp0XJwFyVTarT9BkK2m89A0m0g4WSFLrZgTEGUlKOI+WC0mboq67DO2YiVmLCeLUtRb9aRVoLRhzln7FKuOaMUIzBiRCMKBqjdbOhISkqUVqTBaUGPovaaSlHxFtSHPAmkfMjw+C5f/CMcYs0FFtDOIoiFp2L5GlPyYUpeXb7yN1OSFa5BHJOjFPi4eHAfj8RzcjFdkXqlFMjl4L3hn/25/+MnBN39wOPu8jbtzsGvyEVw+Ew4O4eGGLGM2KkJ5fCuH9gLJFs1qTUMewO7MfEZrvi6vI5Vy8+wa0Tu92X5OmaEATvzrAh4EKPt4F+iHz6csXqH+9AJlI2xJiYpoEYV2B91Wt1TRRLTsoPYYrMCOUSEzlFxKBl0NYSrEeyInqLZMgRrPKBaFWpI08JZzN5mrSkOkGOkZwyY8qMaWLl4Uevrvi3f/kL/vt/85f8+NUl3qI2lrRgFvO6sFaDaJVLvupcqQmXyGF3y7tvvqSMkX/4z/+V//n/8b/w8x+94Mef/YCzrWDdgWLAOacLsNam5xj59a//gbIesH7Ntj9jlAO76xtIBy7OLN1mxeUPfopzz5Ey8PKTT/n8889xVnAOHnb3DOPARU61lLB1xHpq9Dw9GuT925zUhcZbJgK+5Xj6F60EaNdu9stMy6x2rC0fXMR8TBF/qwu9sB6q+bUALmj91snZy4QbWrrK04TYdyENT5/zxD23H1HvRaBy77VHaE2vde8v73Q0Qoy6sIt7yDFK8b0PM/93Lg2x0NpWtjEvn0aKaOTAlIU9a2jBGa0MWO4M/amhYk+uVSdntkoXz1qOEQhmPJnRJNwfcvwBnAx5hsEljVnXrJubIzACmgmrmbaUIcVR4WbSKmPbS6sLqCq7GnPQOEPR6LmVSnpTBGs8K99xvulYhUx+2FNyVN6AaqAjnmHwDPGCcniGI4EPBNZkGRmmiWEaGceolRWiRI7eaT/cKU5kibiYcF1gzI5ccs3ia5/fXCxZLIcp0U0TqcA0Ruw0UWytUxTtyZyAlDNT1K4TXR9Yb7YwTap8qSyuznAYdojNuN4TfE8IHd4nhscKi5YadCmFnBPTODBNnlY341wASUgxFGOYkvD63SN5f2BDRxFVIiYLcRoVomSdZmOMEKdICJFxHOiBw2FPKXDYj4QOpmGkGKMRXa9KdIqZFKOWipTIIT7yj7/8L+xTVmXkPII6FpSEk0wnmbXJrENgHSyr9YrDGBmGkbVYOmvpNtoH3JkHhuvfMtx60vACwxldZ+i6W+L4ORLv8d4q4qJz2KBp3c4YttnwZ3/xGWM5Yz+NDNZC2im0cxqZhsRuGFk7yzRFru/vWV9+ingV6h+I6RYHQ2srvSQskU060BHZ1GBXNkVheguRqMkmNbJ0o8rs/Dbx2wwYbVd4YDq8Z3h4x/37r4mHR1oHAxc8oe/VqKylEriVCoG5K8apxJ3FmD39zCzv/eSQJ+f9Ye7T8dtSFs7bdxyK/JKToTdCHp2+p4LNVsPzQ4XXxmsWBDYFWdQJl+UNmHkEhNqW15xcbUYxPNGOx37XLWhw6uC2uSi51g8ao0EB2j3MMdrTrprny9fPTsmFZkVU4YmaKW2KJtfZyrNRLsWQRCHM0h5k8T4yR03Z5l5mSc23vPi6fpd8DPMrN2BkrvFrrVDbUyj5nGavnq48t7jhByZVheudfqMGHmYl+eFCOAnOP/399JFOHlZfy8nufHKBhYxoc/2HWRd/Ov4JHiUro3yTz6v1iuevXnH5bsK596x8z7vre4rzrPsN9/keZx2bzRYRiPG2OvtGyahTnsVMydqi1lmF1k8xMU6Ru7t7xtu3+Jfn2goWQ8yJYRpxG8F3PXJIc2/znIVpisR0RH2mmHDWa5uyEEDAWY9z1TkuCkumkjJqx6Ka9MnqKJaiHXBa55uZR8HYucvBEratgUxFchXRjHsjzddzl9kv7aAh1pIwZLHYIqTDpC2mS8JjQDK5WvuCIKbM13CzkdmcoYpMEH8UnFJJ8ioSYM58Fqn1/jVJVvWH1Ox8LoIRh0UTX1OM1QZSEk19ZoPBI6zIZUVOPTlnVtszzFDY7W9I1ld9YbDe06/XrNZneALW9UxJuZZiUa6ImEassUy5MEblGSN4xApDLDweJuXWca524hG8sxQXuLi6ZHeAfr3n/OoFP/v5z3n1kx/jz4X9/mtyvCN4CLbDug5CgNUKobDeOj596fnRlec/53tKflZRC3XNGoepPECIkIuQovKnuWpnWCN4rzXhKTf+j4zBqX1YS5/1X0cpkKNogmQusy41GGfQFs6FnBK9CH/905/xf/rf/rf8m7/6Z1xtXU3g1PfQ6sVRp8wAOLWtWlpCjOpoIyPD/o63r78iDROff/47/v2///eUPPGjH7zi2bM1Uu4REtaZWsqhQTusZZxG/of/8X/EnP2MP/vLf8kPf/gjOveCF888n3zyCcN4TzLwg5//Nc6tybGwWm2I4wR+qOunkGNScnrykXtpqWs/LpFOfluCIOCJDv2Iyv59tlvT+XPZbLVH5hKXxTnzNb81wPCdyvWDj2YNWr5Ncza75omTP99vcd7JNByLUI+f1fPzU+cbWsBVmE1BTb/Mom55/9Oj4rc4WjiwCA8szmxufEtHyJNz6/WslpZql6J6tGptzDEIoJGEigWpiKPFWpFiPjpXc+W3UNdgtWln23cx8haAEuZAR7v9H2LpfP/uEjHqxY2prOAo94IpNfRSMzml5vVKwZWszMIok7gzVskprFP24hq2mcGr7cEdet2avRfrETqs37Ldrtn0e+JwQ54GhQVagzMZsYG//917fvLqgofXt1xd9Pzwsyu6bst6bVj1awraDkiwOBPYrs/44Q9/hJMJ54U3b9+wP0SevbIMCYYxcnt3z/6w5+Llp9wdCvsYySZUpa/GSMwTkPACfehYX6yJ1tD1HXd3e8Y0ISZjPZAV1nX77g0vfvAJP/zpZ8o67aDb9JgE687i075GjC0ijpmqrRIkGWcpScA69MJJiY1KZszwxZt73puJf/mjjdYw2qwBIePUJK6GVM5CipmUcs0qqtLwpSAUje42eHOxOGsITktLtFbckvHsk+M//N0/cvXyJf7sAnEWirJXm6zkPuSIkVFr6cSAdaw25zzuBspkwGaSDOSyoRw+5eF1TxpWmGwJXaTv70jjbyG+ow+CtR3WecQZTOjAKaokdIbN5Svc5mfcPbxmjAPvfv2fuBkL79/dcZgyW2vo1xv29/fsp4lYIl787Kw3yCBoIMaUQiDhTaJDSyRWdsI7Vfw0xm3AumqkLerXnat9w4soxt1Wo6+SNCIJW0bi8J7x7jW3X/+Wh+tvWHlbiVILxhpC32OMx/qAuJ5svbIxm+ZY6pacYwpNIiyyS9QxNX/2A0X3XT7m8pq/7wRBs0vfdtrTmz71As1TMVwvLXlGwn/b0Kx1OJcrmRY16rt0B49lWyeCmEXkvL2aUs82FRY61yI2QkP9eQ4tyPH6x3EZ8hykaMiGxXmt4LEpIFMH0e7dgkgWGmkoWa16fbZT2B91/LmNSzi5/hzkKMvvmMrrsQggsAxKoYtLqA7L0jFvC+pYjqDdONp+qpP5wTtT/ZFPPluOU44vYh5MVcZG73sShDqJER2NilPCJBbrv8xcD8uLnLzD5dL8YDF/lxn3p+OP6fj0B59qPba1BGcpUZCcMNKQfR3bswuysYixGOtxXpF01o2E0OleLJWl24Oxlr5bYQy1a1NtIStmhsU3yHkBDnEkxchGlHgwF8EYh3MemwXvO5wLOOfx3hN8oO97QggUkfq5/t25Qsy5hjYtXqxyqkhGaF0dMjkeMLJCkmZZrVhMKYpWSJGUJpC+lobWvuupgLMUM2Py1LjNqmOss+ScdONVGZwxjFmI+4Ethv3DjuFRSxWyUzBvyoVlplfQZ1J+KpULmYrWAOWqiCPk2mJQDDFmogipZAp2nmMqqqEYo5azdYhRJxp6vNvSdYnNGuLdSEkjwVVjXjIleW7v4e21Y7zNnG87us2KwkDKNW00JSXnTMqhhbVYG7h73LMbHulXa3KB+92Ov/3bv+Pi8grbX2L6gATDVBKx1DKC4LG2kidm5Rk4u7gkW0u/XnG/33F59Yyf/Pwv+fHPf0LYJg7jF+RySwgWZ3us9Yi1SF2rQsJv11xeCf/iFxv+b/9hIjkw1tH7nuA8UgRnCiar0z9NkZxKDegeSYFtcNoCNYNk9fb7vqcLQYNp1uK8JkVKBjrAd4RVQpzDBS23dd7jnZKNX11s+Xc//TP+L//7/45//osf0buMR1G4orA4mkaylc9HjNSgwpyPx5GxJRKnR67fvSGNia++esPf/If/yDSMXF2uubzqcG4ikygpVRlv1aa06q9M48QXX35DeLHln/9ry/NPL9iuLrl6+Zxus+Lw8I7d/p5uZcn5LSkd2D08MkV4fnYG+wPjGJUEXZQss9kiAE/Mj9NjkTVWPXQ0euQYXWd50uwImkWqQw2dWafZpvsXrrHM8OilYTWHdRajaHbIE3facOQCa8FGPq4dDRzt0KNZ89HzltdfOvOzjp6RGPPMLAoxzPG87zg0i8+CusnMdlHFB8zPfryyHKkCMMeZFPPhs5gnzzJfQU4+UYJa5aQxs214fL72H50/i9NJ13e34AgzdpmwW9hNi/mimDkAfLS0Fjbfoiz2pDyitFXz/eye7x1kMFk3sXdeHdYFa6OpUUghVzC4tkvSFor1vEW0ygePS14d/dYHuH7PWVv7vdeIuzU434Hz2HCJW20Z0sQuan3J2XZNQXh4uKY4wxc397x+nBgOiW7dMWTwMWK9w3sPhxHEYnEKXS+a4RcrONuRTccIiF2TyoALPd16iyBEOu4O99q+KnhEAtY5bdEzDQRjcFTHvIDzgX61YjUJqwjnxXF5sSUNFu8MnXesu55+u2UYB3JMdOEcTMF1DuJe58W06nmDoLwXq65j1XXsx4lhiNw/7CCbuS1ewrBPpbJPG4JVqKIBut5rBtRarPe4EOj7FSH0rDdb+m7NepMIoaeIxYSOvtbkpUpeaZ1XCB1CNg6zOucxOXZ5xLsz1vTYotmMWApBBMkRKZGcB4bDwDgY3GqN6zpKMhhR5ENJASNX5PEX5OkSUya6cKBf3ZDT50j8hs5DF3olsKwuofEBMaLRaC9I8IRnrzjvMty8Y3d/w83bHY8PA0UM+2HPatwwxshuOBDzhLP9XHOurbhUXNicCURWZqIn0ZlCMAlXezIv3KoaWa+ELL6WX5gKVTLMCnopNJCElQniA/HuLbfffMHr3/yKi+0G66RmozWr1K/6ej+PsStSduRlq6aKCDhGI59s5gWU6rui6EtFuFSCc2nD03NOTq73qEKtHL3c7zxOQHBSiTKrMdGuUGXvsUwPFs9+1JFSGqt4mZXf8mEMpibxm0Cvn9d31D5SIkeFBh+z28yILHe0eRRpND/q0nKoymIZ4Vk+xyzElxNdFqc1pdEs+mOJwXHe2j3ax2ZhQ5xEmpg1zgev+LS44FQlto8rEkMAW2YHfllK2Bi99ZNKijdfanHTeV0sjYIn5zwZy1xziyYmFhUxH3z1WPZiji90Pq+tyyZDlk+qdeSyGPPRyPvIIUvz40/HH+thrTDFUR1sSVpXLokimQYnbQTLutccWUQ5D1BOqcMwYK2WV5RSmKZJ21iall0Fama/wOzMHIaRwziCs1jxWtpQDCkJMSZyceweD3jvOAwDm7jlcDhoe8tpwhjDNI04a5RAG8M4RW092LoWaC0VmkHWZEHuAptVIEanCLlkERtqy8EEKJdKyQljjHI0VHSlIlltLTFrQtgqsa6RmZ9ARMgUsJZkPORIl7KWcqUEVtt1O6vtDnNJOk5bBXDOSIZc4QwKolIyNLWORMkfKwIj5UIyzJwYoKUqTUYWUQ4czY9FcskYG/B+y3aTOdskpOyQknj5/JLdYaCItt/+4u0931y/YLoTfOiZksMYR9cFTFjhrccaR+c71qsVfR/ou4797o7bxwPnbkUpQhRDwvD29p7tszV3uz2HNDG5jqlEUskMhwNBBD8cyBOU1JElQ/AcHnfkYvjspz/lR7/4CeEscTh8Rc53eGsIpqe1Uk25YDxgnTJd9MLmMvHZJ44fvlrz1fsDJucZTWytonriVKBYJMlRl9f5k9KI3rM2Bymldj2zBNcRfEcBurCa9WkRQYwicLWTUanoZUVCXG43/B//uz/n3/7r/5af/OCK3impNgUl7qyBbWMahxiUGmBoBIRaSqElxjntub1+z2438vbtLX/zN3/Hw8OB9WrFj3/0nC5ELIZcMzOmFEW0mID4jgiMw8B+KIQhsZ8mbu/fMU2eix/+BLN9xioEbAiYuONws2P3cODN1695uDtwfvHIGBOHMTNOijA0ubagr3Jg/v9HIw2nOmUOlpmqkT6iJst31aYuztOy92Y3SjUNjvZNyyVQTotTQXRflsaqcHRipXmjxcyO9VKHHr3tRfKo+bywCH7AU5NNH73e0TDb6R8+phy/PLeg+XZbt/ndKt/NYgZkjukc51pOvjcnRqrRqQGGakMtTbk2rKMFewzILP57PFeOk9/me/H3aqHUqazBNcPpldyHz3tqhy+LYlTO5rIsuZYPflZbW0umvu/x/YkfkdryombWq6IBVcwN2qwWt6XkghQ3w9RaSYQPHZfPnlE2Pck4rNdzvQ9as+s9wTs6K6zXnhf9uiIiOjrf0/nAatWx7QMvn53x4mLF4+6Rs5W2HLq6OmPIEdevUTogi+86vFdUAQ87RPstAULOEzc377k83+A7j3OB1coTc1ZCo6KOgQ0d+5TZx0IxnlIUYocktN1SwTlD5y1xGhinCelWTHgg4K1jOuzZ2R0SJ+Rsy+ZsCw72+0fiFCklcfv2nQYqtj2utg2dk951UTUjh6IONaYwHPbkslYiTUDEkArEnCilaEnDMFX5ZLVdkrPYlLAl48ce5z3DMGJwDIcBEcv+MOAS+rsxahBgtR/wlBlzYcgTdCu0wZFjNB5vHB6LldqiqBJ8KlGQEIJXQjjncM6xOTvDdiukbOj8JxR5TpIVIhEfRvr+EclvKNM7Oi+E0BOHyP3Ne6ZSuLh6wWq7QZynYHChMMUdnUS6zTmrMZFjoA9CsOBdx8PDDut7XDNKUiG7PDvhqhmV6zlIYl1GVjbSOSEY7bdsTNuURdeJRWtGsXNXgLIojbDWVkIj1IG1Rp22nCEfGB6uebx+wxf/+A9M+z3ubKPoIFEBbI2lC70CXsUDPbk4LdXJVVhUREOBbyFoWQrS0+Pkk2NI/PQPT772IQpCFoEM1Q56xtLB/cjR4LWtqHIh4E/jHUclOgc8hLnFT5PvM0nqyfCljqqy5c51uzVAZNr8HAWs1PNOiR0X0VyzXC8GKzypo1voi/m6HH38spzqFlhQh2Z+l/OcmifXPY7nqLTbuXL6+XL4swb/cA1YscdMwLcQOs0x+BouVyPy1IE/vWkbjzz523Gc3zaep0dZfN824+FbltR8XjXkloiH0wDZR0yQ5TgbnPJbxmc/Yu/96fjjO95eX3N+dsZme6Zdrqyj7zzBOSRnjIc0TYhxuH5VS6yOZQGaLRSGYWSKsRIuFsZxwBqD9wERYRwjoeu0XNN7sve17FAJqcfDSL+OhL5gjKfrHNkF+lUmBM/ZZs2qD6R1r+TFleTQV3LJ4CrhXlAZkkvlChIDoghRjNHse06KlgC1KUSItSU4IpXIWp8vpqkGLKBx1UAN0tZkiLNWHRSJusccsxFOQ7CK1NbZfibStbK0cUoth7CVODmr3SaagDLOIpVvxpAhZ7Uvsto+LgQ2mzOy2TKhPE8+BC3XsgPWCsEK3hQ6l+mL3svaDucizhasJC62PZtnZ+Q04FyBYJlK4W4csUA0FuN6tmcBf3PLYZw0Y4+AZHKa2O8e8W6l+h6HFKsBa+NxvscorTe7aQKnSR9X1PnOpeByJnjDqrMYyUzjgJSOzfo5rz75Ec8++znhvLAfvySnW1bOKY+W8zzc3rDf3zHlyMXLF6w25xjvKVaf9/x8xb/9y8+4+b9/TpcruWdOuM4xZHWOhX4uGTwGYHWeSxbKlBVwV+28/e4RrFBMwXjHOO2hGIoYpqhrYToMuH5FyUl5sDCsz87465c/4/LTP+PZ1TOCrR3jjJYUq1o02hnOWt1rmGrkqM4zphXwJCQN7O9vebzf8cVX1/xP/9PfcPvuPRsn/OjHL7l8FhAZKDUZZo2lmJp8rD0wjTgOh4nD5Li7G/hP/+nvOTy85+J8xbMf/gXrqx9ivdB3I9MwcLi/5eb9PfuHA+Mh8vbNO0zXgwm1K5SWADXWA51S9Zm+pQ7h5Jiz6rOzeKp1BHN0vj9yHB3TapfJUXHNMfj2Q+N6eqLbjDG14rNl2WWhxgvNOjBi5kuc3KT+KPLhIxfT5IqZYx/tqs12+PCpOXKosfjSwtY4aUH95JjPFjm9RgsIt+rWZrSfzEU7vQUVjpZ1M52aiToHD8yCo2u+oszBJrMIRB8jLvWiS9SGyGx4tBaTM+6h/eeJeWU5XqNdqHWGEZSedTGo/xd7//UsWZadd4K/rY5w96tvRKTOLF0Q5LCb7CY54NO02dhYP/XT/I390GM9nOF0NwVIACQbmgWgAAIlU4e80tURW83D2sfdb2RUIflGmNUpi6x7/bo4fsRea33rW9/32mfLe2bym2knv2D7+uMSIRYFcmRBT2mn0UApFhVAhJgDAkSk4s8ss1qTerkfPZmadjYDrQghcHJ+Tk4Ja2BROU6qxHffOufbb7+D9gPdSrNeDlS65r3LlpjOWY9HOOe4vhl56+QRxlpOFi0ndU3OkU13z+ALXTcl4jjK2EdRFBbyXsIZCc6VNkLSCwOkgCpqxDmWWWcnibQoOYsaupjRCKUQnSS4W42uW3Izoxu2GNOilOP2bs3NbUfrNE8uH6FdjaksGIWta5KPLG9usDrTGI1SCa2yFB7lGGutSDkJfZ4IKTFftMwXLWqtiFnogdNcZc4JUoQ4zS/KLFwuvvW5INPee7z3jMOAsxV+GLG2Io5exkt2FioUuzxJJqauYvCekIOI71CO73SzZHZeyDnXKAV1bXEVaCvJhrEzfDoj5jPgkmBmJEac3TBvNhCfEsaXWA3OtXTbjqc//ynr2ytChvrXfg3SGUrJyISqM357TRxeQnUCWEJS+KBRydC4Wmw5oyzuMQjNcyetDwhkkqh0olGB2oxUxeta7v+0UzOeqE37rqrcuCnnnS2lFI1pZ58q1CyFIkDqGbc3jNsbXnzxKdv7Ozn3ZEH7CwVdhPMsMQLWkG1xlmDiRuRd8NDq9SVxWgD3VfduScx78cI3afkdBoMHlPyp7f+Vj9mDCxJwXt+Lsh2ir+U4TvSthwFoVx3LMWW/YO7W9h39oHQ5dhefwkyfsVeFKm97UEyXRfRNM/5aKTD72dxMQfdf0znQ0zHZgT37ojip/XvL4+xYDGl6vwdWUPm1Y5seBKeDcHaANh+cG9gFrDcF5sNHpnAmu5N2j715279OT4nK7o32gXG/J4fv9/pe/IK9+mX8xgMmw/Tch7ugHpwTYKeq/+BdhRr01fc/AMkeHqPDOceDXSlPyxO69avt7+z25fNrvvnNIxZVBcoSQyb6gNWKunIMKYv2DpKcGTSFwS8uWUrhmlrsC+OIKUKLyhQBSKNLrZ13rjDAnsmphMnpjKV2VRlLCAQvYIPO4vKVcyquUqLwn1IgRmEb5CQaC7iq6GOVf6VE0ZM4cRGgC1Fm7VEaYxwRDWEChCebSNlLXRT3odwmJcdISRPLuGXOIsw4pVlalWmJLDT7EMGoROXq4raTwAey8uTE/vNy6ZTmkginIpxcdAImxl5ImjAEUkj7WKVFn8nYmtpWVHON0QYfPT4Ekh9prOK0jnzz0YxHzdvYlIRlqlpOFpa3LzzaOqKy6LFisVgQR8Px3KKNpffQD5kQMkZnkVMqwoikAIxFNC+gSViFCE7nLMBykmLdOCfFeigeNxnJzxCwyBGoVGY2MyidaaoZZnFGffIhi0cfUh0ZhvELgr/FWU1lWlLUvPzyBT//mx8CnpQj31CG09MLjG7BOLLVHF/O+Y3vPeGnP7+iUh6LxLqUI8YaYQ8YETmdapYp7KUJVDfFnUwXRqfOxBRIMpwsQFNpi6uyTjrjhEVbXChOzp9w8eQjZqePMc0CpUVgdbf+FqFlObdlnZ2SrUl8XqniROelYbO5ZbW849X1iv/4h3/Bj3/2CY+PHZeP5nzj/WPCcFeaoI0c711VKyNSPo2oqsX3iZBrlkPk00+fUoU13/rWY0gehdiF5RRJo8d3A91qg0rSUBqDx1qH0jKiFFMs7iKlXMuU2fjDvPMXb/ss6LXq8fAZv0CYT3KsEjcP4tshaxMmJsj+7aVSOjgNB+jAocvTlMPs87b8xl2cCuI3pJm7xkrmoOefYa/ptz8Cu1eVa4O8z00nmmueQI9flk9M73gIMqjD86EO9mbKF9XDS3D65OkGPoQQJsotepejTow2VY6HfBOpVR84gh7ksLuDNJ1DlYWwo1QRYyzX0eG+7naq7NdX0q9c4lG5pg7S+emYKdT+K6hfxn1+8/ZfJPyYU8LntPfMLEEjZ1UUU8ue5AwxorBMNjipqNH6EEToqDKYZiZiSDHQtDNiDFgSjkjLwIePLvnOe2eE9S33N5nlckt71LBo4NGJ4cgLS4I64uqaum1RWdOoip45nz37Eutqjo7PmDlZsGdtTVVZ0FKEGa1oKkdtbdGOEPpzYy04xWgctXaiYmwdpogfWQvWQNZiK6SMK0X4NOZhWBzPSfdbctZEbVhverpt5HTeEEOi32w4OmlwVY2rW+KYuelfkXNgHW45vmykoNd5d/KDL+wRW7yGdcRamB+18LInTYtWLrZRWm58W0RstJoez2XeT2MqR13JTFxVVVSVo6krGlcR6oi1jlBlQkqkJErZxliMtYAq/s8i9qnLKKUkTGofD4CUglAtQ0CRqGoZrPED9P0JmLfJ9oikHOhArXvaZoVOzwnDc5yOONuy3Wz58uc/o7t9RuMixlWE7o7sB6iaMgdo0GmL3zxHKQkiMUS2m46uW0OZK0wlmOeoSaEkCUoWT4unQbQbnI44me6Uwq3c/EbJeRBq9ZS45cJmKHT/snjnLGCBVpB1oXKmhM4jfrjD9zfcvfqcbnkLeS9ANrkCyKxVcTPImqxqgq7LfOl+cmxXzGX12pjA6wv04eKzX/xeZ0KJSv+bA9ebFm9Z3va0K7Ur3B8WwA9fsy/QSt7AQQiTnxS7BXaXfzyItXmXCJWcVI5ZWdzVtAYfFMLq4X/kvngYdd+wFT2EaeVN0p0Q1GiCJ0romRLi8h103r9rLsnmHqjZwQ9fOVKqPL772PKgBKkJxTqMi+WY5wlkmMLtPm4dbvtvuhvk2b3nm7fyDXPenyU1XSevDw78bWHpq39/U5dj98lfCZQHf9sFefb5x/SGDz6nvPkvov3tDvL+TKTXR10ePE1/BYD41fZ3b3v1que9j2qUackYxmEgz6JoAcSAypHsPRmNrRoZAy3OSyL0raibFoVoKWitSRpCaQxoq3HK7MYlndXkmAijJxb9quCF1Wi0KiB2wcJKMaUy5FhEsbMwA5IS1hzaoBD6vmhfiRK/QUuOEwNVhtYZyWXQbDY998uNxJMUCVntKfHlvtF6cn2YWE4H7NVdwR8PEln5f10szMX2XCwqxcFWS7PEe1QBTlSOTHCpStNb7YsGJdW3jAAUN6+UMjFpUojSFCqL2+BHblf3dNbg5sfMjo7IOWGUZb6Yo2LFkU08WsD/5ZuXvHtygRpHljcRUy2Y1ZYnZw3zWU0/BhZ6zvHxgpzmWKupbcWLZcSpLe++58RmXDuc1cWdQMCFo7mjrQ1WJ3wKpDBC3aDR4hIWISaPtVBpg06iXzUpyGstuk7WZJwVB4dtr7l8/Jj2/AOqk4YxPSP4W2mUmRmoGc+ffs5/+j//gHF7w/vvndHOLQ0bUn+HqS3a1CSjaBfwzjvH/Pp33+Ll847KaKwVIVHQNLOaNtSMao5PEPthOvu7htWe6CiAW+UqnKupXIXWFmuj6EIkuUayKvaWZLSzHF+8zeP3v8fRyWN0VZEVGFXGTqfCd/rvFED1FPumQFFiaE6o3DN0tyzvrri53fCHP/gb/uyvf4JPW7794du8994FJ/PA2gdIpjTdZDR7YtDEmIg5UrnM2AUCwpTuhy3Lu8CiekJbA8mTcyCmAR8HQvCsV2u6fhCAMEVUjujCds4pkbUumILE/H0xV+6bX7IdpjpvKtIfPpp3OYEw+fa/U6J8evCqqcAtOdPBlg6erzmM8QexkK9kD7I96FDtY/O0TDzYdC657kFRDeRU9n8X/Pf5m4owDXTsY/B+tO0X53EHuzi95uD1D7YpR6awIfM0hnUAe+xy3Okz1e53VYp/pUujYn8YSs60z8Efko8foA3yiCr9kV2DRJG1kSMwyRMcUjt2oMRBfvSmsmD3NdWDk7P7Xe2P+RvO8i/cvjbIsFOePPy+ZZFHIR5G5Y9Thy6RpINOQXdL0ptCEAGYLMI83nvRZpgOXE6o6LE5omIg+cDYe8bOk1kzdJ7se5QP5AQmjTgstdb0Q2ITPP2QubobqZsV3/hGoLEVKYvAjNEFSVNTN19myYzOqILy5yxB3lmDVSJuZIvQyTT+kYqAizFKVJ1VKp0KUXPfbtfEEIkhM4zC4Oi3ntEKutuv12zuDP4+szi9QGWH7wYymXW3wZ9anEY6B3lvQWeNETcJlSRgK0FJtTagDEo7TO3wSmPEvFoW+BAK7Uwo+yrKLJrQ94SZklIsQEBEvJwDmCDUzyyAUB4TbvB0g4hF5nLzWWsxan8NlCGbg2Qh4b04WIRckVEE7/DjKeTHRM7IWaOIVKZn3mww+RW+f05lEs7OWK+2fP7Tn2HiireeiEiTcRV9WOO7Fa5ZoJQtFNPM2N1iqsdEH0kxcH93zXo9MHRbxpCYLRYoLerUMQZSEItLo6A1npnONBoREaIwCYwGlQqgM1HrKGhiLsig3ikIJ/KO/SELDQL+5IzOnuTX+P6a1c1T1rcvUXkk54jRFq00YQpCSuFchTaOZBzZlJGgmGGafVW6UFsphe+bgtdUar6+urADJab//qIlevfKByviYT9dHSyPZT5fJUkemIALinaEJmuze1M18Wdf/+w8JbYS7hTq4dd6uDgxUQF3fIqH8Wm3v/rAa56iPSPJ/ESDPvyKilzkfneKzOXaPpwRTBPAkxGxW3XwPhNivkvG1a5DkDkEhl4L4/kAJOAgXh8wOF5PWNRu2c4H73T4Lvu/Tr2CcmTf8LyDw/DatfFwPvSXJ0p/27Z/mzekUSXPfNNrMlOg/y/4lN17fRVAOHxf+eWXfa/X3+9X29/FbbUxvHrlqaqR1dqjvLAFTU4cNTWjVywah3YtwRgRhyz3667LryhsR10aMmqnM6W1whgN2WKM3gPxO+RP2AR+UIwxyN+1EkvvJKMNMmMfd+tIYk+okqJb/h5jcaUKkdoZ2kWLGjtOqxm/9s1jTmYWUwqfvhswFQw5gjYEinhaAbAFHCirSpIVQizKC/BRloqJHRlzJGUZNdxZlucp2ZXG01hGRMVdgJ2d5VQ3Prj3YJer5SixLZWmBzmJTXASvYmUJC8dhoE+DKTK08RYXLrEoUBGchMtgfOF5XiWSX3PEDxqMIxdQOcBGbCINDZQmwjG4iN0feT63rNdd3zrmwOPLxqsq6htRlmNLsyX0YpAttWqWJgqrDZFs8FSuYqQFI2rqYr4YdBqP+pX1lWrFJVRKONw7SXzk7dxJy0+v2AcX2AVWLvA6AWffPycP/qdP+D62Rf85q+/wwcfPcHNPNW8w4drTJhhahnTUCZyfNryzQ8uCZtn5NyTmGO0hQRtO+ft03exvqGq71hdX+NTwNggM9wxiP2ogpASu06qylSVxboaPw5oLbn6OAYSgWEcqZsTTk8e8cGHv87i7BHayAitNnmn7aiULmzeKbofBICy2O+jVMLgiWEjed6m58/+8uf8n3/yQ276NRfnNY/fOuHkpMJlj1MKyvWQS/mcSyGotcYkjc7QrQfROKmrkg946hqsFZZxSp4Qe4bYE3Jg023ZdB2j99RFCDZDAVkm1bwsN62gkbC71vcB7LUMjf2THnb1p2h9GPlff422prCC9jmFnK0JPFC70UOlFbmIP+9IDXkf7/OUK00368Gu5ZLP7M7Kbl3b3cW/PI5OFCb2xTsZESFUIuZpdJKah4RG4cdIogJT745KVvkA7/zbg/LhsZ6YUg+P50HuVJLCPAGf8KCof8B3VBwwcvKuBn7wwbtjlPdJDPvz+vDz8/4SSTvUAGIBhZUk/5OYeT546esZ38Pje/jH/Zl++NmFjbQDT77e9vVBBjXtWtoBDuJJnHdMBV1mpFLKZKmisCQse5cJoYDHouWTdoJJKacycy5UIu8DY98TxkFEl7IUUX4c8OMI3osYUJIvbYyVhVFHYlKEbMhYchaK2uTZ6yor1wkZrQ1109A2M9EIUJmcAil4KYy0JubIMPZgFMqPMjtfOhST9kTMcddZVUqSCFPVIrajwTUVvkCvQg/Mu5NFhuQD2QfI4JylcYb56Sm2yeSxRy4WtaMRTmJJOWRIQo/UZFGarmpM1dAczQhK4WOSEm+66LIUUWHXPVCkcaQaR6q6JkYvYwQp7EGHnIrwld4tIDElok/EIMimtY5+EOGgmDIxZmLpCk/XjjEGPwah/2vHGGYQnxDTe2COkRm4gNMDi2aD5hl+eI7Viaqesd0E/vJP/4bu7kt+7fuXnD+p0XpEKU1cecZ+RZUuRQpKaara0G2WpG7FsM302y23t3dsu0jyg7ijZNG9iHEgp1FcLDLMrGZmPLUWZxRdFmNdEgB0sU7Vei+4UmikcqjV7udYxookUAqpTOeMyoEUO/xwy9WLz7h78Tkqj6AlqdPFAjRnDTmgUFRVg1IWjCOZWq5zWXF2idwkXrOftXp9QZBk9hBqmJgHO2aCmgLLm169Ly8frN8HtEUJRYLgKiUsF5s9jpHWitf4GDObPqLdgsiMjCFPqri7hZIHq+MUItVBZ/ohW2N6QWLyGlYpkSZK3WvfZrdYT+wf9sCCmk7sruuw/1Y7amWewIuD9y2JwI5+pzRZT++dicNAv91itKWaz8nalBiVBbF/QxWdyQcMPvWVv+1pkPvX7r5tnh5P5fb9qmDE5HCidu+471TsRRMfvuYrF0ZJOl63vHr9Ob+IofDgbQ5AnN3lVq7JRBljQz3AIQ5D4s56Wx88eJAnTASGqcHz4MvssaJi+PHLEyM5r+oNGeGvtr9r2/U28dNPr7i57+nuIpdzh9VwPK946+KYsAys+pGLx5e8vF/jrKaPQfKJQgM2SpgAIXiUUqKVFLzc1m7fYVNlziITQYNPEY8iqYpk54y5pkLsB0X/J+OsWDlaw46JabTCWFmTlBFQQk8OXSoBEaMUjc7M6sz33j/nH/36u5zMHcYo2mbGbLZgSJo4ygz8GDwhJWxpcBhEWqGtmwKYapG1QgqVVH5SWjLWXTxRYqE7jc0rBYQsiWcSMF8zAfvSkNAlvX5ocytNFiGMyf0oK7G4W1mt0DmQ8ZAzVhnheItYACmE0hSSY22UIsck+aQfCUGjsggR+rGj6wLRe5JPpJDRqeyXMQwhEBIM0aCLvlImE7MvjMZY6iuxnnTGYo2ClPDjSB0TKBkv7vuOmDVVjHgvGmBJSZNLETEGdKhhjDTKsjh6i/P3vkdzcYpXN4zDc6z2ODtDmTmffXLF7/727/PpTz5j0cLxo3Pmj46p55GkIkFFUhgwMUpzBY2t4fzM8eRRjVIbGTvNQBaRb6trcjYyQWMspqp3Y66HhVAuZetUM3kfUSYJ2JUyKYpuQ9SZdr7g8p1vcHL5PrPFOdoWi049xZsyGpjL+xVWw+sl3xQcFAmdB3LoWN5esd14/vrHL/jt//hnfHFzw+ys4tHbx7RtxppIlYXZHHOU0edIAcXYFWgympzp+oCPgRATGkMzr7CNLfbYCejxcU0/rth0a1bbNd0w4EPCpowrFf00sZRT2uUZD6pA9jnjgVfB7vH9sx7mHA/DTt4fJA7HBExBIlOpf/Y99+m/E9NxEunPJQfa5RBqgiR23lZMLM3D2DuJrUp984bcQeVfHE7Va/lcKcpNQthCuac2I7WLaOPJOfNquySrY2z7CK+cfK/D1PFNucqbP7qkjvm1VDEd5GT78/Kg2H4th9g/PuUF+ytXvZav7k/XFA/k72+a5Dz4sN0LdUrkGLl59YoQBo5PzpjPj0hFUHifQD3cux2MkvODPd8JaRzUALucWj04MF9r+/qaDCWihCBWMukgSwveo7RmPlvQDwMxRFzTUGlwOtGahMmBFHpEIFYu9hiLeKQSkEEpobzEJH7SgoYLJSvGiKsqBi+WdLaqGPteCnfnUMaKmI4xKF2RGIiAsWJnWLUyvpCVpWoqQECG+eKIxfExlU2k0NM2FVUz4+TygiFqrq5XJO9JPkLd4SYnjaLWq7WAIs4KWm0Aay22bhlTZjarsW6BWgUuH52jLhwmeqxTXDy5ZHEyw6dM3cyIERFDqisu3n2C9Td4v894U0wEHwrroDycUgFwpDsgbhEVrmpI+EIXkpveugqI0jXWBl3p3TydNaYwN6wAHU2D0RpXud0YRShFmzFCCbROEpDgA8kKSwGjBXRKiTGEEvgFnDJGE2IUX+rQMIbHKPseIZ+TtULhsSowb0eMesnYPcWogHMt3iv+5i9+zMd/82Pee+yorKKeL1DaMw4dmMwYtuQ4QnRkozEmk/yG1N3RrzPb9cB223O/6oh+RBuDLklYCiPEHqc0C2eZm4jVUWZglcIYARcm32gJQjIbMmmTCPD0sPDZsdQKmCaCviXqpJHkVyzvnvPpJz/GhYFZ5TAWnHMYa6AAOynJmKx1NQkDuiHgiEmRdGKiY2WKXdeOG18WLPIBNVwWjB3IsK+09uthZi8aMKk3A+Rc1mCFpIfqQHl4AjdKIEyJ9foWHUZM9mi/wYU7mhNLMzfUxhUU+hFgCLouPLDy+VmVfXu4Fu3W6F0h+eaqVqW9WKNJkvij3xBxSjV5SAHb17V7h4EHHzt1I15bwKckZaIZ56SASJrwuZTY3F7z8rNPaGcL3v7Gd6BqyvsLUyOFN3wZLe/1Jl2ABxTB1162px2WLlPeJw+HOz1dM7uaeqdahFwDO82O147b69vUVn3TNgEFfxvKcPiSvAtvAuiU75jLhXo4qpiRS2cCbzMwEfAOP/GQhq3ecDkIGUvtk5WcH4xoyhnf3y/TT/t08Ffb39Vt7S23ncKngO4cp63D2AZXeRFZJlNVNdoauVA0ohNgpitP7ayGRQBRwFJjrSTKWkYMzTTSZ5Q8t7Ki5q8M22xZjga/BZN61r2MMKgoTc8U0n5covzLhYGVo6zZMXhSrIR5mKQZk3xHWyea3LO+fobvH8PRjMpVNE3L2GlxdSj6WblYFKkcybF8+G4BmZqOwusShagkr1Npf5+SdzfhxHiMMVKrhJrJ+qxjRJtUtK/2I1cT8xck95nGNxQabUszK6liQBGxOqCCFxcMVFnyIjF4vB8FlJjWPqXIIcuYyljcEaLsc4gCUGsE/NUZKlNjlIyhWKMI0ZCzQxlhr1bOcHS0IKJQRlgITVVjFoq2qchpxFrF6ckJJ+fnhKRZrTekwrAIPqCUOD+knftHLLpXmuwDVX3E+VvfYfHWOyR1w9g9wzDiTIOuFnz8ySt+99/8EZ/+7CW+g7Yq12flcLOWIfb4LmL9iB5HdCW26Fl7jo81jx/VaDOgMrI/iLPCs+sveBkahnEgRcQ23GaMrYTdogzKqsKo1LiqxlYN1jZoXWErj8pSkDsNrjri7K0POX3yTWx9JACCLgxPVYqww5xDF1bHVPvsxvJgWnc1npw67m9esl5tef5yzb/8t3/CT5/ewqLh4p0zjk8NKXUYXWFRVEbTx4w2esdjYBppKLl0wrDpY8nhPEYZXGWo563k0zmiUkcMa/p+zf39Ha+urtj0YwHJ9oV2TsUqNYZyb+wjhoQZVdaGUvipRFFNRdo1+1dJnjVxDtPuSDyQUyhxTsKtQSdpGk6jqNO2//2r0Uvy8gLqqUQq4MGuF6UPO/5yP8p+FEe1SWCbg3SNN+Rre3GDXY2vCvv6+uUzqpywyaPHDSatadzIyZnj5OKEherpYiKFBmWPYdd4m77E314Uy8pUzsXUpNvlz4e58wSglFc9VJzc59QH77o/rNNB++r+TA21qV7bPz2/lry89jo52oSh5+nHP6Hvt3zjm99mUTUoa3cgg7Q2p70Tu/tchDt1Ptwlec5Ou3ban2n8frcbXx9o+Nogw2YjTILRp6mUKBaV4MeEqywxerrtgNaGFlFh3iyXmBQgj/hhg0YuvhAC2Y9CNVRIsavKzpfzprURtFxrxtGTlSEpjasturYyIzZ4TFWjjCXkjDYVKssYQCIzX7QcnS5oXeY0nTB6MLVBGbDOEUJiGAZqY0kxMGtqFmePqI/PCMnyYz7BjyOzWc1sVnFOJd/Z+3JkZB4ze0+qCj0wiPXQmDO1NsTY48eIqw21bcCDMpmqaqTY14q+Gxj6ga7bYnSi365ROpbFVk5rCDIPqjJCvbNFV8FaQgzEoHdURpRBF+GdhBT9KgfppCamIU9SlDELQZH1LoFPRQAxxSTot49kpQVcyoHgA2OfiiBoIgUR9YxkAYWSFL45x4LeyoWcYmLoEsZckvIlOR+R0aA8hoGmHqnMNf3mCyoVcVUD2fHzH/2cH/7xX7CwPYtZTQw90Xt0o1l3PSF5kt8QxjXGVKAUKQe08nTbV3Qby2odGENkvdkSkigfaxJEjyMw15GFicydpiqaCnoKPkmSB7nxktC3tNmvIRNyWhbLQzrR9PepSyNbIMQN2+6GTz75Ga9eXXE5b5g5I/ZT1lBZg9Jy4FJJ+Kyp5DxkS06WHD3Eodz4hryT6CkllwbBlmVkaaKqUgCR3eKy66q9tqJNNNeDhXJff05MBbloVFmMpnyg2675mx/8KWeLitytmelAWr5kaXreff+Cd7/5AafVjDt/TzYtGkvUFLNg+YRJ04WHe1WWyrIwp68GrcIpeQAbiPbYBDQcnJs0/fQwyO6X+/3je7NDxW6iMX9VWEhNlnHleKQimmtSpL+9Zf38C/TpObzzLhRHlKymAvj1LUsXBPXa+Tl4xsEL9+nX4XV4WE0/UGfYv+bgqerBA7uD9PA7vnlP3hRDyz7yMPj/om367Lw/1/td23NKDtQgdq+bNGlUYaXuXD4OAmk++O/D3w+Px/58HFIJJ6wlTwwtpV57n19tf5e3IRm2Xu7ttB1pXOBo1fP5q3t+/vQVz1aRNRVXz17gU2Ld9SSFCDDmSFVX1HXD6Mvs+uS1nkW7QWsRDfRpZBw9MSu240g/DtgUeX675Pl9x9XtHdU2UC8Cd2NgyAqrtegiKVVcpIRhmJKM+WVlpQEiPtZQ4o1VCktiUWmOG4Xf3PHq2RXj8B0ZgzCa2WzGmB1jskTjwGhiSOiUsdqSjME6aZDElPGhgMteQAwRO0ylkIr7cYec9/+fMynrciwSUKGpGEPGlgIvxMhk2Z2zFPwZJaKOCnKYmkxi42lNgzKBfrvBECCOjEPHpOGTtXSoffBYLWLVqKl2KABNSORoSCGWQtlQNxWJRD9usdaQlRWdCzTWGQiZlKCqK47Pj5kdGdAn9D7h6qLPpSgObBlyxDnD40eXLI5PiVR8+ukXDH6grhuMyhwft4Tn1wRVC/MiZrxP+K5nfrLg8r3vMH/8Ltn29JuXqLSlqmt0dcTnLzf8r//iP/Dxjz5jYRdUxnJ50ZLDgO83xHlDjJ6hHzBqhbEznBLRcXJitjCcnolTFRlIBrLFD567q5esWRC1QimNLYwVpYp1a/TizFBGbidHDBlHFk0CieMJbRvmF+9w/uQjTL0oa+g0TslOFHtXdE5r7AHlfD/CWMZPycBIt75nebdkiI7//V//IX/zsy/JzvLW22c8eXJEZZaEMOxEHuu6Jo7DziUVpjhamohGHNq220CVgpwTPxJ9jXNORqNzIvuONPT02477uzVXt0vWnaLgfbs6MZYRpqmZ+jAGZ1SWnEzintAedsXs9NOuOCyvSft3mFQJJqKi6MTIe1gtbJ4cw26qc2Js7FmjWrT11LRHwuAhZ1QWFqRWB3Fyl/fJLzlJy2lKikWiaseP2AH2b4yU6jA3kPOtYmJY3/Ozv/wzLuc1Lg6YsKW7fQ5hxQcfXXL2D3+di7bldvCswwqtW5ncPzhyb9oeKErsJK3Ug+aL3u306y8uo/YHAM/BnnOYd36FtaB4YP+Yv5LHpP1ev3Hn5ZxN51Aj12zoOu5fvYA4MiwfkS+eSJ2g1K7GnoQnJzZ71nmf3r+WSO2sOae8seiN/TKS6i/avjbI8MWzK0KMBO/JGdq2kUQ6ZbbbLU3TMI6e2WzG0fEJr65XbNcdNy9vGUcFlUIrQW2FBi43XdKgrVzwu6alEmrc7e0d4e0FVlv6bkNWFlc1KCs08SkYGeOIKUlXQMllHpMnRfDBM/QbalPRzmpy5yVr1MKYyMiIhtWaQMZVjratcZXDqYYYA+vthrrSaJWYNVY8hKeTRcZONoUGMDJfqCpLHAKbbqBqakIIxDii6pl0G5Qi5swwRrZdjx88aRwhC7CQCqo/lVITXT9F0UkYho7gNcZqtLMYZ0F5IJbORSSkgEmxFP0QxxGtFSFlIhkT5YLNCkzlsFVNt+lxpma92qC1Y+h66pmlH3pQVroNCnKbGX0Q9Wql5fMHjTh2ZIwS2qhGT8NfxJCZt3M++vbfZ3H6aySOIVsgYBhwbktTr+g3n2EYsdUM42Z88pMv+JP/+KdUquP8zDBrI+O4wo9H1LNjoWfpjModw/aGWTUDYyAnKme4urnjxUvD1a1lu+2JMaCtqCDr5Jm7hncuT7lY1Bw5cLrQ5jK7rnQmlQpDAmapWKc1egcs6Cm5mRaLg3gpgBKQAzF2DOM9T198xqdffsa67zmqa8R3XImwpzaQZAQlTUwIU0JJVOigUGkrgTkZtHYoZYjeS+B3Fowuo1saQmS7XnN/f8/pxSWz2Zwd5Ty/PmVf1pdy3e2t//L+MRLkUMZTDGmy8ywjG77vuXnxlPPqgn59xaPzM65vVzy/f8GMgXfeOaM+NuiYAS+uNEnv3n8XCn4Bbywxld1vKssB9E6sT2THXnve1LnffdXD3oJ8zz3xl53mgnzu9Kwp0KQSlUSBIsepw6Lk2qd04WKgW92xvX0JoaPvvo2tZmRl5PgfruJlZ/Lu14Ng/dUvW/bxzcfiIfIs697Dtzn81nx1ZCM9uDJ+SUH9VaDm9T8/3P83vFMJfIe7kEvisp8pzWStS7eoPJIzunR8s0D3ewApl8deAwbktp6+tdodgB3bYvdQuUoK5XWnc/za/v8KaPi7vRln8SnSj1IYdjmyCp67IXLvE3e9p1MABqwmgIxPGou1TiyZtUHkAab1VJGTIpKJWcbvhlHsLauqZgiJ9eDJIfCjL17x2e2G9TZQ5YGFGxlTkpEqq8s/hWsrtDMYZwojQvQgZK7doKwlKqB0iF3OzHVmYRJHtaKpNSp5hqHDjxY/9vRbjx8VniCXeUoYICVbmIuO7e1At9zy4upauvbKQArU1hJXG5QPqJjBKhQFhFfTjPe0YssaEFLm+m6J0RsqO1LX4jYw+pEQEz4EhjHirKOqKqwzdJstSWlSjtS2wlWZ9eaOV69uGH3ERGG9ogAj+ggAORbJuqKDoUp2nVJmvVqTjk4IIdH1I9o6XFPjq0RWPcqa3dioUWDQxDySSMQY6DdL0smcqtb4FISZngSCjiFhlMEnqG3F4uiYZnYMVoRFt90WYyqMhvnMEf1IdhUkRYowdANnjeK3/tk/5e1vfhNzBJvlM3LscM6iXcvtJvP//Vd/wh/8+c+hD6gTzbc+uuSj9y2WLePqhnR0ga0ctYvktCGMdyhjsHommh9WUc8t3SYwjsLyNKbGGSAMROXxk/4GiZhDAbgiMXlMMsQkDJChH0lJ7qU6J7pui9I1pj7l/O3vsLh4D10f7WNnSmRddJDkbO2jyITy7tbavLt+9AHAELoVN6+ucNWC/+1f/Ef+8E//mk2ILM4djx81tJVHBU/fe4ZxJDQOV1XYGGSctXw3sZlMpBiJfmBUNb0PtHVFp5w4mfjMuO1IoYfU4TcrVq9uWd1subvr6IeID0UEPpcOW07kMtqco4zaCrFH7b6nzPiDMhkIJXG0ZKxob5WkdOIIwR5QF0AhCzM3ZVL0bFYrVvd3pDDy0fsfEAZPSBlXN9IwU7JmJKXwyuwE/Xf6igomZof3I8PgaeczrLVMHXxhG4ysV0vub245Oz9jfnSCtnaXGEt9Kuf6zU2U/emVelhirYqBsN0wrq+pjy6YuYj1cL++Y1jd0J9YVD9i5xWVFrasT5GM5NAP8qbDz32dvrjLudRufZKzYqbV6oAtW96ofDe1Q3kOxzslV9mLTj/8rr+8VM+/4OfX30OAhpRB5UQYt2xuXlHpzLi5J0RPThXTWFYGUhHVnATRd43fvP/2wG7cNWuK3lAutY/6Sh74dbavDTJs+8Q4BKq6QmlNN4DW4umcsqX3irv7jtXWs+5EnXXoRpbbTOgVtDVV05Dzijy5UiBUHnEr0DtbRUXm7bfe4vt/79tYvSL7QNgVNKL9kEKUuf8ESlnpEGhD3dQMU4KvLW3b0M5aqkpjmwZVD9iqWEOYosA3kaqLcE0MAYYRnEVpRYgRbTRWyQyk+EYrtLGyGCuLiQMYoUc666hPjqkTJCq265E0RmZVzdG8ZUgRozS2ntF1HS+/fMGjx484enSJcU+EWp5E1DEj6swmK7EMtoI0xxAJITOrXVHydSg9oFREmwh4UhrIyQuCrC3GmvKzXFK6FLMpsStklTEY67DOyeiJsWK/ZYzQZZRCG4OzNU1tJdEvIxsTvUhBEbQqfht5EoqCR++8z+L8PaI5AbTMWamAtQOzec+4/RyVVjRti3ELbpcD/+63/4jYLfn+R+cczXpmC01SIylHtDEcHZ8Rxw39MDAMK6rUY6lRKlNXmjRuefZ0zYtry2a9QSVx5IBAnTPffvcdvv3eOxw1FkNxgDi8wXfIqCw/ymikJD+YzzoswrPof1AQZ6NNgYsg50jOAz6uuLl7wU8//imv7u4gZjwapeQ4a20wlWHvvY7obTgn904MQEDZXha8aCAMrDZbvvz8U9bLe2bHc956/30Wx6d03cBP/uZH3Fy9ZLvtefT4Md/61vc4u7gEI7OZYYeAl8J8GrUo9Z0IuErZLUTLBIwYLFFV9EpDNkLrzwnje/S4Zlwn/OaOrcmEfitBMAVSGjCMaBQpefLkiT0d8inleMNam6bjfhBw3/SsPWxQ3ldlDmDaAiZNgX5f0e+m1nRmsnSc0Fy5CmRIdB+f9u+bSaQSaFQ6AAYSmBjROWKSR8dAjiI4mr9yve2L8enym4LW6yHqAR6Q84O/H0JD6eCxXxwoJHiJfpDaPX96k18c9vKD//slb//1trwfqc77h9ihAApyUgcgw/47TgChdLwOzm2aruXDndizUKY5WRk6Urtu7OHXUuWXyUzktV3+1fZ3fLOVYTarwXtOj0/59rcu+eCDD9CzK9Txe/iffs7NduD0rXcIORJRjNsRYyq0FqvJ7aajampOjk/JwMXZBc5W9F1P27TMF3Ni6eiFDO18zjiOqJTZBE17/IhoN1R1zfHJJZuu5/bunhgzfTcwq1vWqy3WVazWa9J8Tjf0VFkxjCNZK/pYlPPDiAojutboNFKbzPd/81u89dERx49OqBrHmIww55wG29AFROQ5CFqnjUEZzWqz5fr+nk8/f8qXz5+jYqbSirnRHM9bsrbosUelKMwANXXpZMXMWe8KK6Vl1M4nCEn0E8J2QGtHEHkqxggJQ/SZMQfUGPBjBg3L5ZbKJdrGsN547jeBdRdxSlPVjUTnAxaeKjFaayNxLQeUhvfef48Pv/UhsbsGZCxR4pFCJkRKAWwkR7VOU9kK1h05B5qmYjafUVUW4xy4Spw4cmleOYc1jhALFTpKtzRnDRh8AG2kK26zMAWMdiU/kfHb3/pHv84//cf/gKPHMGy/IMUllXPYuqEbLf/md/6c3/39v+R2m5kpxfy44cOPHvHkrGdWLyFuSf6Iqllw1BrGsSPFFX6osHWLwqKcxTWO7WZDVom6cRwfL7DXnQAyKRILsmqJ0qDLImI5NfumMRSjK6xtis6BFMr14oLzt77L0eVH6Kop4G3R1dDCMtRqWq/30sNTPjmNykHerbsK0eGIccP1q6c41/AXf/kJ/+q3/4C7IWDmmrOzisaJUHyKI0MKDBESuoBxphTzE0UNGXPKwsiJwHbw+JQZrKLyhrEPrO/vGPsb6irTLW94+ulTPv/8mqubnugzJiuctlBy4xy9iLb6EbLBqoTMnqjCjBXx+coZnBV9kiF4fHbEVAO2BLbCYErSCJNKUxUBVU0YO3rfc/3qOdcvn9F3ayoNx6zYbnu2mxFTS020mB8xPzqGZgauJSqFSXJcDeKGs9ks6bZrnj/9gvX9Pcenp7z7wfucnp+jFdy8uuLTn/2Uu7trus2G49NzPvjwm7z3wQfYqgVjdjH6UKdFUrLM4ZgEJf5O+UzykTyO9JsVy1voGbHBM/gBVzmMq5CsNIIS55ZMAG131P6M2jERFVJo7zWY0kEmoHb/9o/to7+AlOWZ6vAlU6ZaREPLz6+zOfc5156Lye6xw9zhqz8dvtX++XIfpJzROaJToDVQW40zsubGAqLu7psikhsnGEXtmcx60t4o4AOaIlha7rcdEPfmfPyXbV8bZAjBAYaYLHVVFZcAzzBE6rpBa0vXw/2yY7nOtM2MnCyrLrG6XfLOYi4XhZLZrd2sXvHf1UoTUiQZOYzL1ZIvPvuEd88sTRE7CiESfSCrQAgwUZGFxiMHsG1qtM/SpU2Zfrthu1xSLVqxjRwGXD0rbhdiqelDAGrqpmHbd4QQaJxlzCJO6X0kBkEIdZKDHHIkDwNKW6xOOB9ITtJi73v8/R1rH8nZkaMlJwlYfdcxjoPQHTuP0YbTk1OqqpERgwRj3/H8Jz/nu994i7nT4sWNeGgbpWmMoXWVeNSPmtAnnCoWVWNG54QuC5F04SXAWmWIiChSIGG0xlpLRmNdJcKRzmGtpa4r+bszO/Q/koubQknMs0KVkYHdhZ9Lw9dYEaCKAa0ylbOcnV+wOHsMbkbOuTg2JKwaaGc9Ob4kx3uaymGrBZs+83/8y//AJ19+yfc+POL4YsbZwtE0iZGRYehoQ6BuGsY4YMyAT6M4j9QLdBFnbCpNHFZcv1jjgyg0N5XDac233nmX3/j2tzhZtKjsd8HuAX0OUQXfMRgOh0XhwF952srfcyp0TVvUkhPkkRTWbLc3fPn5J3z22RdsBo9ViqwMKmthtaRE5eye+go0TY11RkYeUiIRCCGxGjfcX9/z/NlzVps1N7ev8FE6Mi+vn/Pt7/8aH//4Z/zsZz9lHDo0huXdK5Y31zx6/DaPnrzN2eVjTC2jGDnnHWBkiqKvUwmnFM4ZcWFBujpWe3LseXnfYetTcmqxEYgj9DeYuMZg2Kyv6VpHrjJN3WJPGnKlwUDwAyH1RDUQtQRTXZLRrPbI6ps2edobnlAAAz3R93Is9PmHi/gEMqTd8+QvSj6+HOc9Qj2FlB0dt/x+uMkIki4JxISOFy2LGLFKsWhb5vM5BsghMWlcZ612FOv9fUVJlGEi1Bx+YjwYUXm4/pfgcvDbtPfTI2+q+eWr7oEzjQD5h0DFbh/S/lUP2RLqDT+9/qmHe/bVPTkMaGlKMpWIxZFjGb8Q3Y48ve8hsIDaxRe5JQ9GfabvlyXB3O+9dEIVoI15bb+FYeZHLwCqEzX4r6YKXxdJ+dX2X9t2fLTg6GhB6Dqc81w+uuTdDz5gua0YzJYfP7vjpg88efyEzvfcXN3guyBAu9LSBVYiRkgWl4HZbMEwjPTbHm0MddNizIqYAv0wyHWmNMZaFJp2tmC16ajqlstHj5ht+2JdrXDaULuaxeKItpmRUqSqa4ZuxFXioOWcpYqOylliCrjK0JrExanl/Dzx9ncf8Z3/7tsYlxhXiSGMrLcbNoMjZM0QMyFnxiAMBW0dfb/l5atrnr+65tXLK7KPLGYt712c0arEoqnoY+ZHJfaFJEBrRpT6dbHQ2KnKZNH5GsaEcQ3oxM3tFWRNXc8w1pGVom5qjHVstltW92tcVaOx3G8S3q9oW0i54n6dub4dmB1ZXFH0l+oml09MByNt08y14n51z5dffM5ZlWhd0f/KaaeVoErDQPY5SiFYO+qmQqnM0HcM/cDYJ8J2ZBwkT4wx4WOg2/QMw0Aza3e5X6sVo/d0w8jog+xPTIRR9COCEVq9s4a/9533+X/80/+GywtNGL9g6F7hKo1pWoJq+P0f/Ij//d/+MS/vOoy1nF8uOH9yhM9bTi+OqV3ENpCiJ0fReDBa4YMnMJBixDiHS47UiO2kVZa6AmcCbSWFix7UbvxcKYXOZRzCCGsGpWXkRWm0M9jK4uoa4zT17JTzd77FyaMPwNSlvpkE8IoLi8lFvySXrncRMkXL3H+aytQ9Q1ATyXlkc3dLCJHlfcf//L/+K15tNmQLi2PL8bFDMYgGVwz4HBlDIOZKzrWP0iTUXhpDiKaaVuIGprMhetBRkXNP1oGxM6zubtlef87dlz/lxz/8K37+08+4vhlZ9YoQ5HqvsobRk4de8v3tGjWsaE1DbTK20uIo4ixWI/mVyRgdGLYrPv7yU2x7gh8t3QiezDBuuV/dMwyDXNtJFX0WQeZ81zGOHdvunjD0IgqfEy9t4P72lm0/kJWh0ooP3nsfzi5pL9/BHT8imhqSIYye5fqOm1fPuHrxnGHYst7cM/YdV88btvfPePLkbZRW/PznP+PFs2eE6FE5s17esr695sUXn3J6+Zgnb7/L/ORE2Ec78W21ZxkehOpcmhummASo4ElhQw49pIbl6oZZZVFOcXZ2iZvPCAoqq8lR03cdtJ6Ubcnnyr2e1YM8YmqYTazrPbigZM1GPWh2yS6mHVyaoEwiH3Jo5doRvYbXs8JcADFeE7M93PYtlJ12xQPRzId5qzxP2NJijRqZNzVHtaOtKsiQgqzj0/+m7yBGCllAtgNw5XC/cuTgdQWOyfu8/L9k+9ogw6vbpRTygDEGrZQ4QAyjiCJpzTBCTJY4JPqxIyvDpg8stwP90JKS2c/85EyKCa01MQr1XhXwIeVA3Rzx6MlbxO0Leu8hifpuo2vSMBICBD8yjgPaOGL0VFUFMRAiDN1IToHGOlrnUAm65ZbVdkvTnpBjuQxTYrm8Z1YtqJwgCLmIpAkNsSJnwzB6+n4gunK6coYoHXsFEGPpWmp8zsS+ZxiFIlhVjkzE+4HaVoVdENl0G1zVyKyjFfue6CNhlNnAlOQ2iEqTrObi8RknJ8e899aMVnm2r3pCfcypO+V7H55gTi2/95dfcFsCXY6ZNIrlU0iQgseovKNC5pQJxR0DJWKD3XZD4yqGrkMrwzj0ZGXKuISRxV4pKu/p+lBokPLaqrKi+ZATYwooDI6IUYHvfOdbPH73mygzI+aM1Vlslxhpqi1GXTF2L6mMxtUtQzL8u9/9A37ww7/i0XlFdAlvIsePzrB6wMaeTbdmMY44W8tNoAxaZ1KZu9PaYAzM2obTeQPjC3Ry5JCojOWbH77Pb3z3O5zO2yJhmCWwAConYboU6xyho8otvqNBqambWRSRy408KfnvFpcpqOZEzp5hvOfq1VN+8rOfshkG/NRJjXK9pBTK+I9m8GNx98hQ3Ca0koQn5i3/6Qd/xau7LcNmYLla42MgZU/WMi+5Xq+5evGC++WSvuuQclrRhZ4XvuP+7gUvn3/MN7/1HT78xrdwTixerRVhJKMFaHA644zGWjBGvp/RHh2XXN+8wncJQ0uMhhA7VLgmbF9gUo9JJ+isUSphZobLJ4+YPV7gtfhqa2tIMTAyHlCMSym/c01QO1BRlWJzWkteR4and5CudKmOiw2bPH/v9DB90qSfvkN9p2ft3lrOeirzo2oalyogXlZgckbHSPCD3PPKELICbcvYRiLEkTF6rDU4awnjSDIDI0bCVLElKpiG0J3ZA/7TWMtDQOFhb14uv/QaxeGgY1AAmKwETMoxMYwDdVUx2bFOl7CzYimrsySQY4YQJAxqI3PiajpiuajmF+0SrQ3TXyMKUpaumJKOHcUqK1OEhHMutntKhE+1LnoWkdXqHj/2jH7g8cUFm80alKJpWybSbMyK4ANt2xRRVqG/lr3FjyND30GCZtZSV82esqqke5tzJASxDnTWHYZAUkqEFNhut7K2aE3dNiJMtjv46vDE/Gr7O7allFktVxAjVQqSyBtNMpqoLSEZQspo69B+lPsI6XoO4wCIsKEOSdhp3kMZGQxBmhAyTiH6Az4IWJZiAiusqZwj4zjIdahl7nscexIwDAOudqy7La6p2W4HyFryJ+PwQZiMw+AxSsMwMHMj77zV8E/+4Yd8+H7LO985QbuOnBVJWbLSKFfL0Jc2hBDoQ+B2eY9Vmk3f03c9N3d3rLuR9uiY9ggaq4nG0MVIDpF1PzKkREyKMU/z4BqVSnOixEcro8H4ADe3PfP5hpw3bLY9JI3WvjQzDLYaMdYwjAObzQZ0DxjW25GUFNtxwMfI/TqyXGe6KuLjWDSMBIyP0ROUrCWjKi4gKhHySGUNR8dHbK+/QLWGYeixVKSg8AF8GPEZlNLE4HFVQ9cNDN1ICEHiWsikMeL7Ld0oeSFZo7Bsu4HVeos9aWSf9FQsp+JY4BnGgb7vUbrBKBEdJQW+9/4j/sd/9g/4xrtH5PQ52+4FVZWxVU0yNT/88Zf889/+I37+4p5sHY+enPL47SNs47nv7hnygouzc7IeSCqTYo8yNUppUvIoAr7vMKZBaSMWnFXN5nZLGGueHJ9iPqhZq5HrP/+UEYNSUGdwKZLDyBAsOSWMzoxxBO3AqzJKkWj1EbOTxxydv0e2DaB2xIFpzdTT+plhh5arPatZImdRm1LF0pVMzp5+e8d6tSIEy//yz/8lP/3sOaOC2UnD+eWMutKoIi4fk7iq9YPHp0BFJKRRLBspzYQi7qm06KeELpBChwkNR0rhqoEYYHV/z8f/+T9zc3XN55+84P5uYNuDH0H0uyKVsyzahkfnc06Oj/jG+094/OiItpFGDUX01RrRTDAEVNwy9resrz/HhY6f/OA/c30f2QYZc/bRs+07UgpoXURQtUHvtFjEGS4XEIicGWOk69as1vesN2s0mRrPo19/h6PZwHb9JUPscfMzrq7u+PyTT1jdXzF0G4ZuS0xiOxpjpO8NfXfH86efEFNmvdkQY3Hjy4oURm59z3p9y5dffsrV1TO++/3fpJmfEJXChwjKUNUOYyf2y75ZM+m4VXlA+SW5v2Kmexw9fR5QOnF6ecyT995ivqiJFpIxIraaE3HsiVqTlC08halRMBXxUmjvmp/aFBt1GXNNMZd7fUAbEee1RngKkzlHLPtc5GX3DaDdFynFP1M9XmTScxHvVJOpe94Bn5Nw/66sT+xAt0nzbJ9jSU4reU0ZaRsSKkNlNCoG/OAZjGdUEJVCJWE8WBWJQ0eOSUBpLUNHkvtQAMBJm6/U4+y5RdM9m9JU8/zt29cGGfrBY5wV2kj2QqHLkLVlDFlaXUZm9xLQj140B1Ii5ExImRS1UDhyZAwjKSt0NGQjVk9Oa6JKpLjh6irx6uUJbF4xr6WTnlFo17DerNCmIYwigjiOA13fc3F5yf3dLVtv+OTjLxm6gRwC69slqakICaIXC0UpsAOjH6iMnFBtjIwFpCnxTYAlJQdKZi6jMWQlxaixDucqcZXQtlxEEHLGoLm8uCTrihQNIfUs2prLi3O6tTg5jF1PCpHl/T2zeUvTLui7DVVT89F3v8+sCqg4ktDYtuatd445nTectyOuu+b6588ZNpbRWxaLJzRjR9zcYM0cUiCMHsaxiCcZuRxzka7Por4cYiAiLh+pJAvBe2IKTD7YORbF3bJQay3neCcOl6XDZxSE0r0IKWPxzK3ng7ff49d/7Teo52ckZTDI6IlTkboacO4O37/AEKjqGUFV/P4f/Rm/+/t/SntSoWeOLgXu+wGqGldZdNR0d3eEoaeqKlISiyVjFLGg9jiNMtDMLE8eHXPUwCxptG15/4P3+I3vf4+LxQxNhBwLeq0oWYL8XEZgjD3QCshF/k/tlXOlhZpK4VkABl0Cos5SpDEQ/Ir16oof/+RHvLq9JShF1FrAnxRkFVMl+FhNX+xbyRFrJfhhYLtdc73a8td//Rd0wUAqVNQsYkvT+Ql+w3W3LR3dKIuLc8zbmuOjGaenx1xeXvD+kzmPjxXGZIxOWKOxulAZlSoq16oIUUY0nhxXdN0LNptrnHlMtw2EHHFscfEKHe9oTKRWmaPaMKsB16AqxctXrzi5mNPMFSaLXsRqeUt2rcwVG1MsWxMhpt09pbPe+cqnQokLMQAZY2wZxSoLoJKhDimoRQPdTJ3vAnIalFjpZr0fDVKSdKDK6SgjVTJnG9EGwjDSdT2ZRN201M7SkLChJ3QrbNPigbt1h3YN9WJBQmFUoO9HEWoLke1yybBVrHxmCBm0RlsnwcgoUbJ2TuibIRQ6rSnrYenwl2tGEriCEBSFexC1ex8DfhzJCZx1JZBkaufotx3L5R2zWcticYQC1usNxMjZccvZ6UxmPDeeu+3Iy5tbtNacHC84XswwWpTe0dKtkNGojDWWVCjHKWeGwUt3sCima2uxRosIcLHUyIDShso5iTFJCrVnX3xGt17inKEyls8++4wQA9YKO65yFe1shtaacHyCc45UXivd4zlDPxbVdMXQSbKYdnRNAUhSTozeY7Whsnu0QCMxw+dISGLhNYSAZ9wH36nF8IuUL3+1/Ve/bbue4D2LWSsxIUcBbLXEtqgM6GKTlgUgdZUlkBhjLAwggdbqqkZ5jTXSXa/rumgLWOqmQY3CCrTGUdUNlXMYI45OdS3ichTgayfoW4TftLMCbhlb1kEpypWS/9donFJYk/jw7SP+p//pv+db36hYHAdcm/D9LSo1qHzGajWw6TyDd4wpsvaRPgSevrzh+uWrMj4QqduWEKIUiAq2o+e+2zJutlijGUNgkxXeyBiljPPqkjzvWQySa2mUsqz7xPX9Fmc80JIy+MGX9SCRiCIepzMpFwHIrEjakjEMQ6YbRvohE6KVtUZ5KcCyUKX9EGQd0h7twRpXxtd6lqsl6+UJw6YjeFnDgurp+i3K1gC7cbbtesPF5RM+f/qSq/sNy9WStxbHDKsNvTOEJM2vHKTA7vsB72VdC8VJbRxHBj8yJAGe01SkaEUORZzPRh6dtfzf/69/n3/wvbexdsmme4W1Cls3YGo+e7bi//Ov/5S/+Mkzgqm4uDzm8TunVG0k4Vl3HS9uV7zz3vuStzNIMaic6HPFgCuszxQC2jm0FeHS59fPuHuxxfiKY/eIcfmcOm9pk4x1NClRxQxRdCNiiGSrioZHZBwGlI9YN8PNH3H+7nfRtcQ/rWTMeiqAhSEql/Y0GifNnj10HnNhMJQxG5l9FzB/eX9Lyo5/+7t/yh/94Ef0UWHmlrNHx8yOwFi/AypQmpwNo48iKF+JgLoiFyFVRMzbaInDxhLCSFt7Lk/mPDk6plpkKrVidX3PX//xD0neM3Yeh+V80aBtReUMTx495uLigrOTE46Pj2mqmlnTUDmDURmMgPyiFBIxeIgbxu6K7fI5hi2PLo74D7/3Oc+uRkYcMZd4r8SyVusoY30IA1aItqVbDbvmlJTuiFtfClQ20yjPSZNZ1IHbZ1esrq5oT9/i2edPef7llwxjRwxeSvQiPJ5yhBwYes84dsSU8CkSgnymMQqjjIyAjiOMG/znI1XdoEzDqhtZbbYo7Xj0+DFPnjzmaDGX8aKDQlqnAcMGG29o1ZLzeWJmR/RcM5s55osZg9+yvVvy+MO3JJeJCRUSY3/PqAaSrcjKETGFrCht+Nh7Zs5hjcIpzcxZlusN294zBgF9Z+2MseuBwHxeYxctbTtns+3ptkNZbw1DFL07Z6dG4n6tI0FMgZgCxsi6HMvIhYBYpqyGueRre+2zKbZM8gHCoBSWlTIlBiDtTj01PTcj4+DJlWLYbNi+uOE+rYmuQTctlTVUSoC1uLknDx3MxA1nOwz040jbzrGVQxmpd4MqzPQi5kqWe3wYR7q+w9mvBx98bZAhZQUTypPE9z6GWBANXez7NM5ahqFnGEeZpzuY7Z0Qe2MMORQEx5gi+iKIptEGjaOdLWjnx7StImzuISeh3LGf69PaYMoNpRXM2xYfE6H3rDZbjK04PTnZdcSMsdQWnHaymkUZr7i4OMfVjhA7iePa4H1AV4UoEwPei6NCVFLQKCUHPIZAAsw4kCpxWrDGSBKhlFCzivVTSpEYfSkWFPPjI1loV5puu8GPA8+fPqU2CsVA/cEl86ZiOnjztmHeOubzwOw4c7w4J/s5Mc/YDhWrOnHkEjENnBxX6OMK18O8kYQdW2HKvNDOekbJjOFsNqeqa2bzOXVV0bYtrnJUdY2ygjyGBDkkbFVhnKOpLA6NI0mwGrYyYpETJgYam/nwyRm/+Wvf5ejkArTQQZ1RWBWpbE9VbQjhCp1H6npO1hV//sOf8Du/90e0x5b5oiYTGYPi9q5j0yUW8xZtE+28pR821LM5CotzbbGYjKQ0YrIUNK5SXF4sOJppfNB88OH7fPd73+P4eIFRqQg8ysiNzplMLNaeEWOm8RA5hzvUcuqCTyBD4fSr0imftEZUme9TBGLa0nc3PP3iU37+yc8YciQUvQayaHBYLf10SeQE7ZSmr6KyFoUi+sTV1Ut+8rMraitJ7XbVYYwtWhCSkCZEmEtrQ902tG7O8XzBk0eXvPXWYx5dnLM4mtE2DU1VY62WzrmaAr0qCRD7sUWV0IyQ1nTrZ2y7JVW14Opl5ub+nqS2tO6W82pN2NwxM4FHZw0zc8T8SMCg2hm2Q8KMivFqzauPrxn8EfdLhW5PWFxeYqqawQc248Cy6wiAcw3OOWrnqGtHP4yE0bNerlAq0zRt6Q4GlLVY56jrRqjGKaEILBYNrtKEMMrIV+e5u1+TMVjrqJqG+WKB0YYQRQhsEpcNQeZ4nTO8fPGS+7slxmrOT084nbeczzRVHsjrW8LQEJVm+fwKnzNvvfsBPkty3a3X1CmRQ+DZF19w419yux7ZjCL2pF2FJ6Gt4fT0mMeXl6QY6Lseow3z2Yy6rlHFdWezWRdxTEVd1eQUqZ3B2IoYIt3YcXt/z3K5xCjN8eIIXSSoL87OWC9XLO9vWdc14bSjco7V/S3z1mEWgUZD57esb5bc3w+s7u85vzhj7lrmdiSnxDAGIoYQIStFP3hAse06AairCusqUor0fc9221FZx9HRgtVqKYEzJtr5nLppUQqcq4je02023N/c4f2ANpYf/+wz6aCkhFY9lbXMZqk4rma2XS+JS4xopZnP5wJWjB6/swDeYosVoVBxBURIGYZxLPdbtWORTKBzCAGfBMCJIWKM2YELIqa0t7T91fZ3b1M5QooyYZsiKUu8n1hLpEgaQxF0Tfi+Z+h7nLGQswC48wUhBMZ+gy9i2SlEodXPpAPux7HYKkrB5/1IjlHWL+/pxh43NJJzRLmuFApdZqY1Cp0VOiJdvAQqJShaDCpHTI6QRubzY77xrfdYnKyoFwFlAn705ABdF3n27IYXr9ZsfcvNesnTqyt047i9vePmfslkZXyqDH0/YFwp/uKI1prVciXuAij6rMvMr3S9d4l3mvINoOjZZG3IWPoh06eRkpuLcCPsBDQzmahkXRcWVDkfMTP0GR9k1DakDLt5aBkLNcqQtIhhGmtL/SjCdVo7hqC47wPH9YKceqwWhf2MNFaUBqfF4tAZw7ypIadin5k4Oj5hNjvCmYR1Gl3t1HyoK8f5ubBPc9piNLhKgGxnq9KpLEB6GMk4cs4sGs0//vvf5L//zQ84Xgz03ZdoPUhOaWdc3Xn+f//qT/n9P/oZY7CcXB7zzrsXLBagUk9ImS7Cly/v+MbmLc4vGkxpU6aQ0Vga1xbB6J6x39KYk5IrWZ68dc7jkwu0r7jdKL773hl/88lTTs9ntO0M03saPwiL11iUkdGRyplCq1bYes7R+ducvfUNzOycpKxoXZUxCU0GTdEdy0zTihPgMHVvUXtywzT8KOLmA+vVLSEmfvTTL/l3v/cnLDcjqrKcPzrl6LTBug6l4n40TguwHnIUq3eTi05Zuben55WGZsoQ08hv/r132MaW2dkZOPj8Z0vCeknXWU4WLadnc+Ynp1w8fsLRySntrGU+O6KuGoyxWGOZzLRRkcmMUr5nRhFJccO4fUW3fkUOGy5OFwzeEeLIMHYEMglxZjHWYpSV0Zq4F942JYc1Wu3c4lSM1JXl+HSBcZnlleb9995m+cXH3F69QquMJrC9vePm+pblqoPYkcZOGo/lno2I8Kk2U+NGmL4pR3wZM85Z7puUElqJRt7YR374Nz9gHA3dGNn2AzFrZu2MDz/4kO99/7scHYkIaAhB6sPYkasB69bkuGHWwtlZRYxSh7TtjG23wYcBFT0MgZc//ZRXL0b6cYa3DWp2hJsf05XGr64s6Mz6/p7z4xOOFy1LH9i2C15c3bAdPMMoY/FHs5ZKafpuzXDcYDlnbh3jZs3ydgnZEbLidrUiEbm8OELrTN1WoDJ91xN8YLPe0G07mqahqhrGMTCOAa0N1tZUdS1N+SAWwcbaksMGcoKuGzBGE5Lkfjlnqqaiqh1VXeGMZdMPhGEgL29Io0cFw+r2ni+e/ZiXPejZnMXpGbO2YWEzT45b6NaE1ZIUZc28u7rmfr3h0ZO3MHVN0x6RrWVI4ii42W7YbDuGPjAMowCz2466rvh/fo2Y+vU1GZKgh0aLjWHKkRglsTNFJELUezwhCHqrkPniOHk5F9qctQ4TDGmiPU3zLinLlL4yvLha8pOffMZbR3BSF4wzRsZhJMRIUlFuOGOlyNeaWVOzGTpms0oojlGsLEWxv8yaT3h6KoKT1ooVDUW0jiSCPyEQ9QgqkPOI03OsUiQSViussWCk0xCCl9ntLP9U6aYGH9h2MiPkvVCB+6Fj9CMpJZZ3N7TzBfOTY7qhx9/3bDdrcmUZujv8OycSeFJkfXPL7W1H/WhOfKwwraOtDUrVQINeaT6sG37jgyM+v93wj//xByxmFbrfou5vWN5do8PIvDEilZJFdCbEQPaJyvW07Qw/9ERX4YcebRxj6KndjPlRy+gTgxfBze22p+/hpDL8+vuPCKbm1X1gDAmTB2wKvH18zj/83vd4fPGozKkqOXY6YdRAVW1BXZPSmrpy2HrGX//oU/7Nb/8B6MTp+RHDKKMPPmVWm4Hb+y2PLmZobZjP56xWG0n2tcNoI9aWaST6DlPNULbCWM1s7mjaFh1P+Ma3fo3T00coO+U7pbAuQU0foOWi7ouIgiop/LVWBTzLBfgqCq2a0sUqw2Za76MmnmFccnv3kv/813/Fph8JlSiAC8iRaRqLswZjizgUhV5qNaOW2VYp6Hpc5Ui+53LWErLmut+S8PRe6Pimqpi1c87OTjk/OeXx5SWPzi44Pz/j+GhB3VSlc1Y64NPsOuV7HFDBpuArYc6T85bk7/DDHVYrRl/zsx+/4Or6JVlFLs82LN6tsUjSXttMajSWwBBHdBLPeD1m1i+v2X76Kfc3Ee8rUnXMvP+IsZmRXMPdcsnL5T3ZOeqqoa4qKms4PTnm1atXYuuahDA/VFtShm3fExALqdliThgCVkHrMrVecHzUsOGaysHT5y+4ufbMmlNiFPrhYnFMVTes11tu79egNcZWgLBLtIab21sUlhwDYbsmNJr6okVbIXe+vLpmPYystyOrzVpYGK5i0/Vsrr7k/TnE4Hl5fcfLztIFhXaOWTtnvVqz6rckDavlHcN2i0ZAXXKmqhxN3aCNZrPZsFwu0QqMccxmM7SCxaxFKc3yfslmK6KoMUVOj49J1rDcrEkpE7qN6MBs1ug4kueWrutxeeQb733E8cLhlCflgBrX2DBwZCNPjhrmOpI2t1TOMfYj/cZzv+6IxrLadPiYGb1Y3mnnUKhdETVRwZe3FeM47CiAVV1jrDBSaluRQmAcesLQi+BVLtFCIayerAhaaJr9ZkWIXlyLUkYlcNYym81wVbVTrI9JRJGEuWalS1xYPgpNDDKLrbVo6WijdiJL3ntiWd9TnEAGEUHLiIaPMGt+tf1d3NLQkYOnUgJYS1dHVMZ1jtgUqICZ0oxZ4TI4Mk6BJRPHkW6zZt62VEZLEp8kZ6iMETvJQpHOKIyhjE4ZpIEiOZJzTgAwNWno68I8VASfSUmTk2YSOJviqzAZCussZYia29stn/z8JR98VHPZtDiTyMmjqFHUDH3CB9gOkS+eP+fTp09pZq0UjpUAlTkW16scMThSYd2pgkCHNHXcJN/LJQ+aiqtpP8UxUcTFxhDpB8/MN8SkySqW3HKKs8IJFvYrwjbTQHFaylEeF6HFJOtIrMlJYbWjqSuCrcW5p1g2TwBASIkRxYubJZ8/Nbwz0xw1RX8hCYCojEalfdyVTrrkmpUTccmswOcsduoxS9yJ0vmTz4qE6AWQigHGDrot2RawJEUZIzRFq0wlvveNx/yzf/RdnlwaYnhJjEvJj9yMda/5nf/wF/y7f/8DNtuRo9M5774z5/zMolKHHwY2xWb0ahX4/Ok9bfuYpq5kzLGM+zjtGP1INh0MHbGeS25eO9rjFlUbTKjxxvPrH13yZ38Fv/U//kNOjmvCZsOzT56yvX7Bq2XNrNLUSmFzJqpENJZUneAefRuzeIuknXRcdYYyNqpKLjXlXnl3dtg3bg7mUrXSheoQUXmg7+8YhjX3dyv+/e/9MS9f3hECzE8rzi9nVFVE60jWEPdTr8KqVuBqK0wArQrwAdYYgmAkoilSWDdvPzll3Uf6cEu3GTirDfXshEdn5zy6vOD84oL58SlV26KsxVgnWmVZ7mkBEgrIpoW9IBqZCU0gx56xvyGGJVqP2NrinMUHYceOMeCzjF1lJe4m5EQcIymKEKTWAtRkJQWZUbrkBImz45Z33rng5Oxb3Hz5lF//rX/GH/4v/zNPnz/HzCra03PM9Q02BubOM+qBqHpiHNCuYnZyxHqzZbsdUNlCAemMEqZwVcuJ0jtNjUAik3Qg4gnjQD8ktoNYyGdlxAHhiw2eDUdHpyicMEV9wPo1H104jj88kvvRJJq5JXrJyRUeTUBHjx5Hxu6W5eef0b3sWa9gUA15foo6OmeNwaPRlTAe+67jrlpinWEMAds0rLcdWcm6mn2km81pnJYaIi8Y5w0v1ms2m44YMqPPvLq54269pG4s3/323+fVzccczY/wyXP15eesbnt0rjCqYjs4tsqijePubolgqEbYatYRQ8Bqw2w+w7ct/TgChpQyde3YbO7puy3WWOLgWKWEqyusNtzdLem7LfW44iyNVKbhbn3P7c0dg24Iw5r18gZNZqYzZ7/+HRoVubu54f52iakahmFgu15z527R1lE3Hau+53a7AWvpth1jadCkhOgYxsh28/UYm/8FIMMkhKHkwi7on1K6JFny1xjFmkm4xpRufyweuuyKLmM1phLRwJzjriscs+gHdEOkGyIb5TmfnUhHmUxVWfrRFxsVKSRiEmLVenVPMoqmXWCsJQ1aqMIhiXBhTIRUhNWKku+knE/OBB/lAiidKaUT2kSyGjE2l1l1SRRySigni19OEecczjp0VkQfYfQ0p8d0YyYHjdHQzuY0sxk5Bqw1NHXNbDajms/Z9lvCtmO2mEkHQp3RtiLsFEdPd3/P7bMb5vqU7q05w7Aox3qApAi+QoXEd9494vKR5X/4rY/QyqN85PnPNb//r/+GYTPw5PKE06OamCLW6V13bhw8YfSE4EnZY2swTeZkfoRWBh+EHRBjpt/2bDYDiZoPH80wVUMyhvuLU6LSuNkMHxIfvfsBH7z7IdY2BwCD6DBUrkObJSHeUTmNq+Y8e3nHv/2dP6QfN5w8PsY4x7jdkLwnpkhD5tXVPW8/WnC0kEDhRkfOiTCOWA0pjKhKk8JADB5rKtAa11ounrzDeniEay/Bzsnak1UkK7EdpVzT4rQRpLOv9krHO0AhJaH9FbRdTQh8GXPQWR8EEgHf/LhiHFb85V/9kKfPX5CsISsjQESS67NyRrQQaqGCap13BY5YNgkVPqVEGDtOj1uOkthens9rtK0Yc+J+vebk4pInbz3hyaNTLo6PWbRzKlcVzQVbpJynoD5RFQqYoGBvaTmJQAoVj9yTwj0vX3yMw+PsCfdXGz750adse0M705w2iu0mMWx6fAzF/syWDp5HzRTNbEYMAa3hnXef4Ljh7m4g25GwesXYtXB0hkoDTouWQdoGknesvae/v+P69g5lHLPZnGEYGOISn4TB4VNmDBF9t8KgqLWiNZE43hPznKvll/iUePl8Q985hk5cMbwP3N1u0Nay7Xq23SDCWFVdlLRFUDX4wHw+Zxg7tmNgljRdG4jGMETFx1++4OZ+U+b8EjFfUVeWYRiI6xWprslJPN3HXma9j+qKi6M5KXhWY0fIie3YswScNaTCgOoV3EZZj8die2s0OFvR9VuMUfil3Nub1UaSXCXrl+oVkZG4XeN94GZ9L4EuBhrTEgfNZnVLWxvur56yfDkKuNsF7m7XdCHTjSMvv0xFRX6kbWq6wdMNkfV2ZEThkyKV2KDQD5SOpdufCST6NVTOABkfIsNGihCtwE3jLzGRYiAlv7Pqi0UCLBYz8tjUKGD0fYlPiFOx0qzqSrRiplhUCjllrWhAIN3rEANWW5yeHtcFkIjSfXCOEDwKdteyMUKHl6JCYozM5v9q+7u4zRWoumKmFK0VxmMaR6qUWVjFkYMjk5jrwKg8C5vJFSwqhWsMqVbMbKQ1kV4NGDKNigzJUxOoibjsMcmToieNA8mPQs9VmugD3nu8HwneS0Ml723HYiqjjTkRstCVbRnzySkJsFDur5gyMRiubzw//tENKSwIY+bk1BB6Ganabj3dZuT6+o6Vb1mv19Ilcw5SpJ653T1YV46u24q7F7kom++tB1Vp5KQYi1OXiFDLVp5VwOycFSFm7ldr6loJ2K0pYJ0pDgzSDIpREZMiK40xMoqVk1hil2WQGAVkyNGhksIqi7O2iD+LMKHWCtGpnub8NUEpkq6IBJq6oa5AuwoYUaZiCJkhxOJEBn2/paoslUNm6q0W0BFxo8jGCK04lkLaCEgUojR1tBXASgpP0QkyGhmFyIn3nxzxP/yT7/PdD48g3xDCLc6BbRqiavjDP/0L/o/f/kOu79c0bctbby948qimrQPr+07YailTK+nIfvbFDefHc9560mJLU6TbbIlGiWV0BcaN+LGjaY/BNqhKig6/WRMinC8q/tvfeMT/7be+Q04rgm/58lzxb/7Fx/zVT695fHbE+xen1CRGB72rqOdvk04/INgWK8piKJXKsS9jEkY0evbCvWLXrXa5iUAPU3Egx80T/Jp+c4sfBv76Lz/m5bM7Zk2DqgIn5zXzNqKMOK2hZCROadFzSiR8CmSdsZUTbYAyImmNKToiUu8oLSLOxIgjUzctF4tj2g9mnBwfcXJ8TN3OcFWDMU40onQBv9mLTkg6tRcunhwzNJ44rvD9HWG8Z3X/guw7To6P5HPRWGuIWfbZainXZvOGi8tzXj19xmY1UDj0RRy5WNhnjbUKZ6BWmdx13A9butU9rz77hNGPzI+OMEZD2HJ+WhGSYfSOi+OKbAyr7RZd17z97e/yZ//pz9h8/pSwG10R7QSjBcjISv7JuKmGFHeaT9oYnEo0LlOjyFoTw0jU9zx9+TE8byDVOF1jI8xYc5xmbC8043ZNyhpXSdNku95gTKBtWpyV2qtfr3jv3UuOqjWf/fw5xid6b1ndw0Y1jDjQo4wn5cyy89IMUorKR5q6QWnF2K/pNz0NollWWchHDd1mzfWrlyjg6OSMMPZsVlcM6xUWR60G7q6ecrtKDCny6ssbxq2mcUfo5AlRM6SE0hU+ZGIUXSuttkXTKlNZi7tfY6rijKGktjya1fhxzf3dNW3bUtcz7pZrumEkhEjK4gSyiCtOGg/Kk4KnUpZ5Bfe+ox+2kkMpsZO9Wt3xs08+ZfCJ2Vyu4c2mI6n7AuZmNsPAst+KqGsW0NUazXw2Y9E0bLqe/mvmOV8bZNDKlBumKAUnCX45ZmIMxKz3c61FWRgUOQgNliwLzCSAlHMqtB4NSYvqrbFFvAKq2oqNoQFrLFUlC0Hd1AyDR5U54BjkfRpXM/oIWWMaoUXJLIl0zhpbZmByEeQglbm/XFwY5HvlQkma5tsnrMYYEcMLSr7vEAaMFtqbNloshZxjUizPMRPHgO8HglckVeF9pBtGYhB2RTufE3Pi5fNnzI8XzI/nHOkjYr+laTTWePIYBAUNidD12Bw5Xpyg1ZxxGAgxQ7YYZVBx4HgO5+9ccnbmwGpyUHTrM4Jp+PL6niE55senaCvz9+IkoVkcLajqGls7Nr7jrY/ehsqwWq3ZLJcs79Zs1j3giD5T2Zp+6DlpLd5vOFosePesZt1vsI3h5PwDHr31EU07QyvxDrdorIpYM1LXHcN4hbGBqm5Zbzz/7nf+mFe3rzg5n2Gs+GV3/QAhE8nMneHqes313YbZfE6lNe3RnDCMiL6WQmuxGRUFVb+7WevWcfnkMePVMUPQUsSXOf1ysne0aJhofdOYz1SMC01suoIo15jY3e27nbugmKWTn+KaONwR+ntePv2EnANKWfEM0Q6tIkanXeB1zoCyhUkh1+dulKio37a147333wZqUtJ4D/PFCc3Rgi70LBYLjo6PaCw4rbDaSrerjDxktVffRUEudPvMFCRV6RjJvSD+yz05rXj2xc/46X/+Ie+//RYnRy13L29R/ZK5PaKpNMknVivD3fUtXT/y6tUdt9cveO/9d7i/uyIGuZ8aa+g3K959+22qkwb38p6s5jy7uidlT+w9dYZHcyMcim7g/fff4Ysvn3O3WovIXzWjnS14eXPPuu8ZcxYBwCgCpCiFM5q5tQQD/eqeV6/u2Iw9PiZGr4kpsdXr0rlOxLgBlBSlMeKio9FFBDEnhqHHAXkzovzNr9BCAAEAAElEQVRAIrNVli9e9GRtuN+MPL9Z4xPYqqayNTdbT911mOSpkodkII1YIo0xZCK+X3J/G8GPtNpjKfOq/ZakpsS0rMdlndVRErPGVlQ6kfyGOETGKICESQlrRDwxx0y/GvFb6fylEASkVRZjNOu0Zru+wo8dVituXr4iBk+OXromWZG0YzOOvHh1Q/YikLiYCfrej2EH8KCEsea9B2V2I2PW2iKgKMBz8J7ZrMEYI/7qWQoGpaCyrjAcBGQYxh5jNO1sRj+KVk0IkRgCdd3QNFIEhVEU21OSe2fWtqAVfpRRtRilS9XO5/RjT06CLPthJCvF8eKIlAVMGaNcAzrB6ekZMQRxBwoRXwCH+Uzonr0fiDEyev91w+qvtv/Ktu994x0BRZWiqhMWz+rVF1R55KxW/Hffe5fVe5dcXtbEi0e8PfPc3N1xfHqGVpp+6Jk1LVYbbu/u6PuBi0cXrFY1m80RMWeqpuHx/CNCyjx59z3u11tsgpv7ZaE7W6wRMeisZZbaR0/OmTENZDUnxJGQAyF7YtKE5PFJE3LAJCNaS9YQlWY7Kq7vMubjLTFamsrTb2/ZbAb6seWnH9/y9MWS6AL3yyVjEsZq121pmppJkLWqK2GlpkkgWwTAQoyoIrCminn7JLKrdmOFRfxMlQnxMjsR04gPvljLl4bVtO4lCqiuxK4WhEEIwpgIiZRExFHGREZSDORsCDGLeKZxJCOFjsKgJ1ZKkV4LIbDabGmGjtPmhMrVAjj6Udrf2qB0lvljrbi7X9KNnoRoBoXg6fotvbXE5AnjnlnnvafvOhm/yrKOdF1PZIOtbZmzFkDVkDk7qvhn/+33+G++/w61WeG7a7TyVHWNdgt++KOn/L//t9/ji5e3KKe5eLTg8eM5bZ1RKdFtOrrtgFUGrMFnx839wLNXS05PW+rKopTE3mHocLWT680kcYMio3WFqRqiuicwEELkbnXF48eWoyaDm5FyQ7fKVG7G0/s1edzy6OhcAHBr6WePsKcfkqsFWUVUjmiVimbANAo6Of1M3JLMlHbswIXdJrmWyhHySL9ZMvY9n3/8nI9//ILjdsZH//Axi4s5V+sXrMMdUXkZ28kGox3aWnKO6KSISDNTG42rTGGkll4LSsZpSpM0xMh8NuNyfsR8cYyrGukiVxNjwaGMFWcwNXWe1G73C09UvmPJHzVJnDHChmF9RfZr7l59zmc//zHzeQvjY9KxBnssjSetSRicE3H445MZb799Trd+zjh6tLLUdV2OmziZ1ZWldppKQWtr1tdLuu2Gbbdmeff7dOs1H/3Gdzha1EBi0dSIe6BCKcdsccrnz5+y6npsDozDVthXOQGB48URTy7PaStDZQ2uMtytb1kvl5ydXaARHbMQA1fXN9yvtgSdydqQVGSbBlIO6KAlLo8bmvqI46rlqLLoMLC9X3F9c4XF8/z5DWEcaauKfrOGHLAWlqsVV69ecH52zrvffwvdarqN4ra3+KUnYEWXJQSapim2w5GQAqSIMzWPjmY4V9HmnnXMvHXZ0q08SkXW96/I45K+3yD6OZl+0+Fyz3GbUanHb3tUV/Hyy1tW3jOGmsrOWW8yY9fjE4wp4dmScxEYz3L9W61EAF+LDWnKsbDzRS9s1dZUVnN1s8JVHa7p2GwHfHEb0trQ6EyMMq6fkuhmBB8YChCbUOQYGYn8zc9+yma9ZL3ZkpViWN/TjCM5JtZKmJ4yVgQme7IXYWKTErWxmNih8oBNnvbQC/6XbF8bZFDjVH4Vq46s9gEEEQSMpSuekii/5xgIg1DFRJg/QUncM5HoRTl4J/KSEgIHe6JTvLq+ZdAdF8e1fGkiVivi0KFMwpoK5Sw+KVRj2Gx7lsPAqV4IImkyRtXSSYtxp+iZiptCVhltBd1GJRH4KcEvpYRJmhzBZAMhkqInIl11rR2oSMweHeU9fZklzGXAbOx6hm0HukZXQs9RvYeQ8CmRs2IcBm6vrwpTo8VZI2rIQyC4jMmThmgijD3jZkm3vObm5ZoQPHW74PjomNXyjtBH4rCh8ppuecvi8QUYhzYVxlnmiwXatfz1Tz/j8mzOk0enDOPIar3GXG+4fHROVRmsk+NC9tg0UpnMrLXM2hOUEqG9+XzB8m6DrWrWt56uu6auzogpsqhaHp89op0dobQASFYprIlo3VE3HTHdkFWHqyuiUvzpn/8VP/rpT5ifOGy9Fw/tOi/yJtriVcPt1vPs1ZrLx8c02lC5jPcdrhHrncqK/kQ2maQSSSdQCdsa5icN6cpx5xOPdMSoEZNGxG2wWNdoYSzkZAQBzzImUDgMO4okBpIRX21hQhRa6BRaSjKl88h2+xSdbrBmzd/7jY9oP3vKl1cr+iSznSJmpKXgK0CdM4XhUxBjZw3GCOXq2Wdf4nONrQy20hwfH1M3c5p2hmsqMMelw3qoDWGK3agsIOqAhqvKKMbExsjTF9SqUGIDOo/kuOLVlx/zV3/8JxATazdg05b7mxe886QiJY9xijSMGGaM40g3DIzjyGbbsVkPvHhxQ0yKtqnohlGofa2lOqmp/RpU5oPTc0y1IOC4W498+uUV6y6SkmLoN1iTSXkg6RFs4GZ5TTdsSWNA5USlNA2UpCKyaBoq7alURsWA7hWN0SiXCUZeY43HaotCEuopTzBaoU3C2URdScd9uepBWZabHp0Vnkw/WDYdrIbAi7uNsBuamrOmotaBMIxC34wjTfaY3GIVKL8hjT0xKbpOAKkUxVdbRdHUCLEnpCSOPtFLl9/WDOOwS3R9VdE2FWPfyRx4lHG2pmmIUYJXDEW5uXI7lpn3ojNR1ZU4zhSByZQyTd0Ik8pHYojEnFgcH5e5yowfEzlkjhYzrDNsththPBWxusV8AUDX9+ITnxJt09LUNevtGh880XtWd4azkxNyyqw3HWOUtXbWNjSzlq6Ta8iHAZUz5zxCGcdmsxWB4RhpB49Vx1TaEXKk86NoL0QR8T0/PUaTGJFuS0wRaxSPz8/YbDfElKmrmpQTdVvRtjO60TP6JPTncWRWN7RnM8ahYxhGMgJEzpqWtmlZrtdc3y2pm68bVX+1/de2+c1TxlL0zOct1y89X372Mctlx2YzcLw4IfUrXn3+c77/a9/j5SefcG7hg5NTLi9OaOpLmqqiWbR8+blhsx04u7zE+0u2neeTz56h3Jw+Jj579hy/uefu6pr18hbvIz4lhhjZ+oD2MtoDE4tOU1XinCL6AhrrHDsxn9IppzRsxDXCETXc9LB+MfL09oqh3/Ls+XPu71eEbNn0iT5kFmdFL8CA1VqADu0Q6oGAH3VVY40Gq6hzhUqJaC0axTgE7DjSRBGBS3mkKOdKt9hoYbCqBEQMBp2lix5GVajrIjKcSqxVpUlALg4cKEJMZa0SsEPliA09VerQtGSl8SmSsgcCzmiMFfas0sKo0BgqLE1lWCwWzKYxXieAq7WKRJTGkdWgLK5uWXUjITt6Lza21lVUVU1VO5SqCRmMtcSgqKoKvTiinc0YOo/RlqqpaeYzMBUKQ04i0mk0fOOjd3nrvUsW7cjYvwJ6cR2zc15cef5f//zf8/EXdyRdcXpa8+TJnLZOmCyd0WE7iJ2qgVnVEpJi7TOfPb/h4vKExdE5lUk0i4Zh7CArtDJolcgqkPFko7GNpWobhs4zZE92hplu2SxvOHn7CSFm+tUWh6IyipOjI2x7xOdrRT3/gDR7h0afYJTBpBGjkogdTpRPJSPSuTR1ZGwCETLVh3fjVF1MoFBgHDcs7264ebXmz3/wMZt1x0cfHfOP/8m3OXvUcLM84ic//wkff/4ld10k5gZT2DExIx13MV8TRX2dQQVUsUQWLRGL0RVZO87OzrF2RtPMsK4uoJPZiUSjiu7WBC6wo7Xudl8a0wojFB40QZit2yug4/kXP+P5Zx+zur+jUoablzek4JifNHzzo3eZnz1mfvwI40SHaXE85+hkzocfXRCKeLXTVu77yuGKdpdJibjq6JZb7m5uUAlmzZyblVh+fvb0GU8uTli0ov2SkhaheiK3d095/uIZYwislyvq4Hk8a+SYGcP3v/dtvvGNj6itwTmDNon1+p5+vebi9AKK2PN6ueKqPeLm1Q1+CIwx0QdYJ8fNasuYOvTY8eT8kienFXWOHM0axn5J4wzdqsNauL/bEsaBNINh2+EqzXxmxYLUanStaM9rTv0R9TpxxIzzYPCqpQuK5Wbg6voWZQyXl5esVitWy3vmbcX7J462aVjZwHjkOD8/ZXtkqec1P/hPP0DHGfN2xur+XkaRYsLFHqehbhyVNcy1ZZYUccwsnKNyim0Y8HaUnFpLraBUxCiNMaBzoqmcNCb6QEiw6TwhgU8RtGUg0Bc3ndX1kqw1zjjms4bKKHRKOA06h/3dkjMpdvg4kqlELi5GdKW5u70RQc9SG1idyaEn5cSm2+JjpGkkxuhcWHNB9OIqrfHdksEP5Kz349V/y/a1QYZcEtCMCPJYa8lkQhiFvRBjsboQlNAo6YhWzmFnc6wVK8iUNTEHtAkiwEiWTrfK4tIQRY3+tJ1xeX7OkR1Ft6BVqDjSNC0LnwQ51I4xG9KYwFTYrKhwWDPDGEMkiBBkyihj0RlhLyiNsqbYZyZGH6mNLrN3IvQYYyAOI8mLSrHVisZZVNuyWCy4WwdCGFE6YbMWi8gkKLz3gWjg+OSCtk/oak42NbbaUDctMWeI+f/P3n/9WJbl973gZ7ltjz9hMtJUVZZpw256EjJzBT3cwWgw/+o8DOZCEISRLnVFipRIsdm+2d3lstKHP27b5eZhncymHgboAe6DCHADWUBlVkXGiTix19q/9f1+PrhhoJwUfPLZZzS7hm7bMHt0gcwNphToYUfo+qR6C+nEcjopOT2ZsVgoUKCzgtxoVDYHkVGerZFlnm50Pj1MiuCpMsFyXrNvRl69vETpJ0wWySd7ubljdzjwsajRytI2N0gpWS1rZIAwDCkuVmkigeAlVSEIRcJSyKnBKHGctFYs148oyhVg3qveFBHJSGYahNrQD/eUlUabgl9+/oK//bsfI41DZRlepAHBaB3Op0idVBEbNM3oeXvb8LSD+bpAK4fOHQRB1/VkskRKTSDiY3IfS+WRRlCViTS0bXsCBRKLlD4lBMQ/qAAeaz3HQML7OCgco3zHjk3q2IlErRXhaKaQyR0tkkvaDXvscEOuGgod+OyTx6xP13z94ppffv6CbTcStUwEcx/en74KmR2nkumT0KYAqXE2cH9zx/rkIevZlLKakhc1Oi+QRoLy6e8/8k4C6TQm6mQXCe/AYcdN6ztGiTwOGNKQ4d3rlukEAgeh5bC55POf/4T7q2seX3xA9BLXO3abGx49/IjDtsH5yP3BsW8thzEgs4LVxSlj6Gn7kaazrIIm8U8Us/kEXRSMrkcYyf3dPc1+xAfF7/zuH6GF5tf7Hc1+ICrFN1/9ghhBB88sjsimQUfNozziZbohZlqlQUFIG8xMeZwb0g1ZKTQRIwRloTClJlMKo2WiXkuQIm0gUk/z3ekDZHn6Wg52xuXVjh+9ekkXc/og8MIworncttw1Iy549GLKKDzO9QhvMUpQa0WuA0otyY3Gdwe2dy2HoBicRwhFVdVYZxlHS/COsiqSIri3WJ/e0/PpEoBD0xB8ep3T6QQhIvt9Sz8mNkBVVkynNfumPT6oj0yn06Sp7YcjY8BTiylVWdE0e+xg8T4idYkxJePY4iI4D1kQTOZzdocuDZBFxAZBrnPqqUoxuq4nilSHOD8/oxla7PGEL1Oa2aRmNhYMoyXGgIyRaVVQFzW7pqO37miOEEwmE8bRp9d5/D7MpnOm0xlXd/dc3twRSCcBRVlQ5Rmz+Zy3N9cMQ09WZ1RZxqOLM5wdubq9oR9GfJCI6JlNJxA9m90O7xO8rDSa1XzKbt9yfbhLA3Xv6Po988WEYGHbNkghyTLDpDAs5xOqPOPq8hr1T1OGf7SXCD3Re7TWyGiwgyMGRSY1fWjodjcURrI4mfHkgzl3lzlXr2+4efkLsvGM5QePKEWG7DfY3XNk0OShxPaW0FpOpimKbEXGza2gb/fc3d3Qdi1RF3ghcEIgsgy0BqnwIdD34/uUTJHnNLuWXBc0+5ZQ5NjBQhAMw/AeXioiiZElFfe9x9mew26Pc57t1tMP6tjjVkiduvpd3yNFJJicoR/TqW5M0MqqqOmbgao0hGA5XSxY1wW20kTXpdrWBzOyrERrTTvsORwOdEftpj6mPRHQdg0xelZVQak1zo4IlxhbmkieK1zwDOOI1snsJZRGGcN2f6C33XurkBEKJS2xlkwqnWqvwwD9AaU8KmhQCqELgiqI8R0LwHGIjpsbwAysJ5rCpdSEdxZlFJKAEgqPpO8dQ4w4JZNJBGjbjqYfKE1SZ48hHpkuGh9gd2iZ1lU6WAsWH3qEGRBaJ56Z0iipqMqCp08fslqXBHtJcA0mU6iipHMZ/69/+x/42S+eYVHMZjkPLubUNSjhIOojl0pjTKqNKaXpbSAquNn2vLrccXG+psg1ulCoPKcfRqalTMYoLMG2yFwhjUQVGaoqMUGj/UgtoN1sqOdT2nbky1/8PffXb/FjizILLJF2+Snj2bfAZEwFZCJg0q4jpU2Ou6gI79XfqZoJUsvjniolO979d/COCemJoeewuaE7tPzyF894/vya1brm9/7oCR9/UlEUnouzNQ9Wmov1lB/85Eteb3sciuhTksDHwEBMbDE074zZPkYIgiyvMbomyyagSnJdInWGEseKqRRIbdIe67i3Sp9sPFZK5fsjqXdJ2HQqndLTMOLtgbG5RciB51//PW+//AJcQIuUVA02MvYWk1s+fPyQx08LFusHmHKC94GXr9/QHHZ8/MnHrB6cwNjhvX3/RRtjABfYXN6y3Q4c9hsO+y06z4gm53BzzWBHxjc3zKcLCqMZRo8dHU3f0w2etmuByLSuUFLy0ekpznuc89joMX3DcHvD3b5BKcH5+ZLQNwybDVc3d9hxIMaAHTyuG1mSEXTJq5tLnjx4xI3fIJXDKYGXkW8/nHNxMmd3c02RSd5sDrT7PWM/sDg/4eHFw6QqHR1BjFTVjOBbRFTMJwuyo9kQIk23x7mGboTFyQWffPghl7dbLp//feJBaIdyDjUeUDqyf/uczWgJLhACjPfXibdUGMRwIErHMHSEtsX2iQ/ohx6hBNZqnv3wbwlDz0qMTM2AkB1GdojZcQcej9pWoY6WNgcxaWqrsiAi6AbB/Xbk2XZD6xTBHtkmZWTfW642B+72LQiYlTnR5eQqwmgJBnTm0PMpUiu8G2gON+y9oA2aIBTBe4osVcmSSjkl8o1J2vKULHU451HKvD/cCcj3YMp2kqNUpOt6nAuE8D9MBP9/Xr/1kOF3Pr44wg3fmSAST8F5R14UCYpBismOoyV18DV+DDCecpo7Dn3quf/x9z7BRk03WFx4p5yTaKmY5UuWVcF4uGV/e8ned9TG4eYlKgb6IXJoBvK8JGDTIMFFBtfRDAODjXSHLfPaMEQNwaYahkqUP61Uio0LgRSaMq+oyzrFYLoWbQzTyRRVVAQ9gZg6t1JptFYchgFrRxAyJRcCxChx3uJsqkLY6Gh2O7a7nsFGAjtMOWMYRrLMEp1nHHpevfyGs0cPWKzO6PuOdt9wf3/HYlZz8eQMFX9zW46AHQf2t7dsXgq4HdIeRBsIEWUKFmcfsHl9ichLaHrcak9W1qjuwMWyRtg9SmmG1YSyVNSTktvbe7TSLOcr8rxiHHY8OHuIIiOGnJubPb/+4hkPHpxzXi+AiIsD+2akHdNASJuS2UwjVclIQTlZw7ubs9IoQCqPlgNFMXBoL8lMxMiCu9uGv/5vP6brW2aLCUEmKJMX6YGvLAqcS6f7g3XIGLnftdxuGh48XKKUICsFYzcmmwIKqVM1IGlXAoS0PpSFIVPQHQ54WyOMOC7y7wzev0nmiHeRTfHOGiGOPUHxfvlLi2V4l/hL/786PsTHAL7BdrcYZREyooXE1BVlNWO1OOVsdcZPfv5L3t7eQPCIcIwUHmsfYfSImE47lKppO0/fb/joyYes1ycURY1WhqAEUsNxJXv3j2M0NAGppEzTVCF/s9ALIY685jQxEcevgZSpZ5qqkQ4RO/rDDV//8qfcX74hOvfecBCDwNmRoe8Ze0fvI9vOM2wGdl0kVxKVl2A0bhQMNrLdDXgbUGLPYb9jPp3QjQ1D17G923B/1zIMge/+TsCOI74fsG3D4Eb6fSSTsJhPcZs7FosF45A2ozFKmrbj7Mkj7DDStS0KhbCRQsJge06mK7r9HuUjs+mcJ49OyI0mvUnCMf1y/H7HgIweETzG6JR4UQpjKtbTnFcvXvD89kA3BPqgCaZmGMbEmoiBSmtWdUl36NMmwAWqyZzlJEuJISmYz6Y8NBNuOk8zJiDsZFpSlEu6NkVtjVZUdc04jIxjgsyW1ZRJXXO32bDf7VBSMJnUGKOZzubs9i1D7wjRcXp2zjpEtvsdXd+htWZSVsRp0qwFkp5uOp1ST6Zs7u6ASKYUZZFjpGS3O0AIVEXOdDYniIy7u9eIkEBe9aSkqmt6Z2nvG7SUnJ2tmc8mZK3gm+vneOvohCCLa/I8Y9cc6NqWXCtm+RqJpVCw3e5BavrBkRclo7Xc328hJrNMXeaIGJlPplxdb1LcUiT2Q5kX3G/vGPsDQ99RmBmr5fyobwOjFE3wBCRC5SAz9p1F5xWFVMgYmE2XDINHmxylU9e2tZZMGTyapvMoU4MU5GWJkBn32z15UfPJx5+lh8N/uv5RXmdnp5RFydnFAxZnp0hjUsd+GPnq15/z6sULvveH3+f88TmT9ZJ/mZfsb3d02x3NfkPb3LOYnyKk5PGjB/R9qr+9PVxjvUaqjN4NrC8e89285LYZeHmzIystQ0h9ZZ0ZjDFkJkvVDZ1RHAdXxo5kJqOqKrROoFejFSH49xtGrRQxK1LMWqTa1/OXr5FKMHQdWmfooqYyRQLMhZTc0loxqSukkBR5SXCQZ0Uip6uM3ORMqwllBqD41geP+d7Th5h4oDYDq5XG2x3z0xM21/d4myoKow2JW8K7+oFkGAdGP5IZw3w2IwZ3PPHVR4oyeBIsUYh0qKWOJ8c+BEaXQGRuTDF/RRoETkrNajHjT//597n48CMsBqU0nQu8vN7xzdWWZy/f0Pc9Wigmk4LVySkXlWCxqqnyBKyso0qbcyTCC5SSFHlO8AKTlzR9UirbYSDGSD2p8U6hfUj8oaAp8ozOZOgsgzHxn7RWZHlGVNn7Wq6LkYsPHrFYlwjZMHY7skxh8hIvC/7TX/6QP/vLH9AOAZNJHpwvWcw1UvVpX2jTNmc6qclMcbynw6Ef8doTvePyZsv9rmM2m6NFpKgntN3tMQ3ikTjc0KNMgcCkPT0tzqbKrgQ2ry5xw0jbjlTG8O1vf4hzgeXqBCcK6pMPOZQz8ANaRoxItQAhw7FyGt9zsN8VkdM+632hIB1k/oOmRPrzAHGg297QN3t2m4Zf/PxzdKb45FtrPv72imLmEvtBak7P19TzBfP1BX/+1z/l69e3gEQqgwgeG0a89cRgUCoDkRGipirmLFYXZNUUoSuizpAqB2mQMjGGhFIIddxoHe1h8n1NIrEY5Ls0g0iHUTKmIYsIFjseGLtbTB755Y9+xObNN+B7jMqQQZPpxJjKVYYS8PrFazoXmZ0ckKZks9vzxZdf0VvL8y+f8clnH/HwYo3WkaLIcYPlv/zFX9L3A3HwSK+IXmJdIFOG2+0dd12fovk+cnO/4+rNFW4Y6AfL7tCxa3qmVcWjhw/BugRSDWCEQmQ6mQRHz3CzwfUdQ7B0MnA4bHDjwHy9RhqJGx33hx190x9B/JHbmzs++/Z3ud3d8PTRKc45uq6jlgNx2OCHA4d+oG1aNpuGfgyYrKSoJmzut+z2B7zzbHcNWRYZBs+h3XBRnjE0fdLGDiN3Nztu7rdoqfnow6cUeHyzZ7tr2V3epH2/UIi2pXWvE5h1tHifkr9tO1DUFWenJ1xevib6pOSWeU7bHKjqEhFhf7/j8u6a+aRitZzy8Mk50gSiDChzPLg+vpdjlL+pi4UEm85yk1AEsuTtVcflm9dc3x1oRsUQFbJscCbnbrNl31tEDMxyjQiOdr/D9w1NsJTrGqMWmFxj3cjV1SWxnuOE5tB2WDeiApys1mSmYLff0XU9SMl6fYKSkbZt0gFTCHg/Z31yxvXNLV2bdKZSlCwWM2azGbtdw6Hpfqs19bfeDf2vf/QRr5+/5Ox0hVRJ8eiB0btknhA1VVEyuhGjZOq0SajKEuEl2kUybTj96Al/+C/+EGs9Pj3/HZ/bkpIpU5JMK1zfIONwpPkGNJEQQOoE8VFZfoylOQZr8YHEQQgWYzK+88kZv/r5T5lNNYOz5LFgtAM2BObWJSex99ze3BDGgZPFBCEF1lp2uy1y9EQTGJxltGlokuK3juBHZCwR4d18MkGY4lF/JUTqLgXvMTpntJFxGFKftx8SnFAJsjIDArbvMCqR5YdxIEaB7yzCcMx1+fen5dF6ZBvIlYAhDW2CBxcauniD3bcoB68+/5JX4huk0ihpqMqM05Mpqyg5WxVM64rMeB5fLDk/mdN3Qzo9KHOePr1gsZrx9s0V95uBzRYGt+PxR9/FuY7ReU7Ozun9Hf0YknYmeqrSMFudo4sJKi/QWfLRSjxS7KkmPcPwBiMsZV4xjpEf/O3P+eKLr5mtygRJOrp8fUzaSCGOjAUf6IceAhg8z19e8/TpOXltkEYhBkdWZsdYuAWlkd4RBwtFAk9WFZTFgaFJmzLeOZp/k+I7xvUi8dgbRLyrUqQ/CwKiTG/a+A8WzPdAo0SqQoiBoXtL8Lco5XinUMKYBLjJNd+ezDg5XfDTn/6Cl8++TtElHDoTSU/jLD5o2tbRjJZ1njNdzymKlF6RUhPk0QOcokNElRp/8mhUkSRt4lHkm3gLx9SCIMG0iMdFXqTaRJpwOCQOYnI3P//6Z2yuX4G3GKWoJjUyFvgokDI9rKlcMx5aRlERY8ngW4qiIEhFP3rcIJnM1xRFjZSCzf2OvITNdouUEaMKzs8fouWGm5t7+nbLzfUNZSFYhoK+V6wXNUO3Z1EWtG7gdD5hHHusVURh+HJ3R7ADWkTyDIoq4+rqik+efsTbFy8xWjAKElOmH+k2DS7XxGA59C2t9xz6nuV6wcliRq4SHz1TERQo5ckyS1laPn26opgOvNo43m5GRmmYTxTGeHJtyJXGCImqcsghek9VZhRVjjISFOwOexwlMQa0EgzeYhTURYYdBq52e6RWZFVNJFlt9u2BsooUZcHarNnttrRtw+nJimld0/bpVDG4nvl8SqklPsC9c2xu7jCZQa8kIXiGcUAoQSFKdrstWZZRViW51igERV7g85xmvydqKOqC7X5PXS1Yrk/Y3N1hoyfgKcqCeO9Q0TOfTHn44AFvXr/GaM3ZyRn31zeo4JkowXpSEXcbWiV5cH7GOHY0dwNKVzxcnWCl4NAPzOcLbq5v8dZRFDnr1Zq7+y1CKnRW8OTxY67v7vFuoKxLdKaIfiR0Bxb1hMcPzgHBN89fUNVTJrMFQRfcb3cEk+F1zm70VJOKvJwwKSfctR0Rxeg8k/WTdOraB5AFQWUMQkGmqKukLrve3OG9Rw97Hj64SIrnf7r+UV7f+d63MUXBfH1CVlfpvhki3XbH/fYG63uWqxnT2fQYXZ6jvUCOFtsdKAtNrjPssRqUCPUaosSYHEnGECLO/wZu3XY9u12Dk5LgPX3bsNvcJwaDHbHjSNelDd0wDAgE+8Mu2WW6A1mW0bSHpEVEEINODBYpqSc1kvRxxm7g/nbDZDJFIOn7jsVyzuFwYLQDJ+sVm80mAfCcoz/cUegcvEOMA7U4I/o9Uzll6BrefvFT7r/4ARkHHp1l/OHvfcjY3fDp7/7fef6zv+by5S0Pzj/g2bOXtH3PrulwMbJar8iMZBgbfu/738VkK370wx8eD6jg7vaAKTLywuB9oKpKvB+pqpz5vGa+mOIcfPPNa66vdhRFTl5oovd88OgBzRvDy+dv2Wx79l1gdjLnu3/0x3z19TNeff2MfozY4HF4Dp3j+uYecs+8DOhoIFgOuwOmnKB00hCiFd722N4noHEXMEIwrWqqvMS7VOcKIeD9SPAiRZRFRIk0vE+IhzS08iHxm6IUPHj0mE8/+xitLeN4TxQWpQ2ogp/96hX/9t//LU0XCRJOlwWLqUDLkRAsIR5rmiHxLrLMcL/d4GwyOuhpjkZxc9fyzctrTk6nzLKktJZGMroBOSq0kYAmOJsi93kaglxdXnL14g7pI0IbhgCT6ZzxsKM/NDhVcrmvyc6eYLMCHyImROosO+q40/WuDpHOP0KCAr47DPkfDkTT4CUNI1KlkxjxoeXu/hI3en70oy+4vd/z/d//mO9+75TZPFlahDRInSOEYVZrvl2vmcxP+D/+4gf86quXmEyhCQgX2Tc9flFhshptSs4erMnLJVk+QZkCL9PwQaos/fzKNMjiuH8S8h9YXN5bxN5BJY6v8/ivMgpEDITQY8c7TCX58V/+OW+++jUXp3OsT/pbdMRkUJbZe7XmzfU128PI19+8xUfDoe3YNy1ZnvP15muG7R732QeIODCpCqb1HNlYYjeS65zJZE5A8eryhq++fsHlbksxyckImDzHWUsmwRSCiOejk4c8e/aW0uSMhwYnORpxFPFY2URKhJIsJ1NOH57hgkVnghhLhFacLVa8ev4S5zylzNGlxnm42W4JzjLaPXUR+b3vP+Xq8pLLNz3ZUcVuvaB3Dic06JzBATI7Vr4NSqXPwzrLar2g7fYJVh8CQ9cjomA+mxOt4v5+S3SW7c01l6/espotEF7RHDrmdZUYD0oxjB3n56fsdzu8dYyDp3U9oYdH56fs7y5BHysvKjCd5MymqUra3g1JF5wrKjVjWuYE4bAxKYp76xjsSF1PyLICON4TVLKB5blCaYnWEqUnfO/bF9T1htc3I5sO2iAQUjEtipR4KnIerOecTEvEsib6kTj2LLUjNwqjJIvFgidPPmQwBbKsGVwCC8sYqLKM2WzOaFdsdgeG0WHygrIoWM2n2HEkhjQMfXS+5HRW8/rNFbvDAa0VeV4c+VcTzGbzW62pv/WQYXt7zX/8d/+OP/3j7zGdTXn2/DnNMLLZH7jbHVjMJzx+/IDD9p5MRoSzfP93v02+mPCTn/ySw35kGCOnZw949vINLjjO1mc0bZv6UN5T5gVlnjGflDTNjrOzE9q+Z7s/sN0e6NuRqqzICkNZFjz95BN2+z2/+vxr7rYHJuUcFTr+5E9+nwePnvDmxSsenf8OVT0jy3OyIp0GFEUJUaKkYj6fc3Z2Qq4ih0NPlhmWqwW6rHHUqaOvFcMRvOSDxR+BkfKdF5aYqKsinYULpZlO50xWK4QuOex7mt4zesliPqNpd4hc8fCTjzBaIURGrTN878n6HiUjQaiUxBo5sgkFY4h0jWN3bylCILme00zYFCU39x2HZqCQWXp4SQgMQJEVU7746gUIgRaCbaFRWlDPZoy9heDplMTZjt2yAO9wveXh2ZrVbErT7BBDy/7uluvbDWW+5Kc//oq+t+TGUGSKP/yjD8irNdJM0SZDK4mKASUtRT4Qwg3WbpnUBUjDs+cv+Zu//Tu0IU3/fcCRqNhBvOMFgAgeKRI1NziFFXB7u+N+26SpaQjITJG7DNeMeHmM/2PBjqg83fHLTFDmI7td0vkh5BE4RTrJFzF1ySTpe+v8b5IKUqIyjRTHcYIgTSUi76sJaTE9noi7hqG/IvoNKksP+VGlLl80BoFGozitHvDPqorVpCS0t6hcomSJiCX7JnLoE3fjwcUTqsn0aDHRSK0SF0Qqgkh9PFSa1kuS+gwhjjFE+Z7gDO8YEOJ9J5L0FktgG53UOVIGROzwdsvm8hnXL7/CDS2ZVkhZkZkcITKaQ0Sqkq6PHMYA2ZRu2/Ls2Q0+jlhvePZiw+VNR1UsuLpt8aNkXpeoEDk/O2O9Okcbz6tXaYhhhwajHcHvKIqBpx+v+OLzV7huYFGv2dsmxYCtY7/fUuSS1brGe4kioOI7uJQg+PQgXZQlox+YTifsNlvuN3uE1IR4wzvLhsoKvDQ8f3HJr7++5MnjC77z8SNKI9FBokVAycRGsUPHrJac+oKbzQbX7lidzyiywC76ZDc5tKiJ4LC9RWrIdQbB0R5G4rJESkXXW6wuUl9QK8oqZxgDVzc7lCnIyhld2zNayd3NntFaTk5PkFnFN6/eMp1OWK7W3F55ZpMpy+WU/CC5fvuawggu1muUEGy2G+b1hH7Wszsc6PqB65sbyqLg/OEDNtsENoqh4ez0lKosiZ7UU46RqA1nJw/YNS02RNrdPeuzNUIGhmaL0QIRRlzXsj5Z88lHH2D7lr5psVpytl5x9vFjchE4XUyY1zl284LV40eszk8Zh4FDM/L5l6+5ePwhA4GxH5hkOUNV8Md/8L30flaSV69eMZ9OGEbHdKLQqsJZzcXJFDcOPH7wgFxIdKZZzZf88vMvCWiGEMFZ8umE2A/00dPGEVlnOC1xRnLd7Li5vmcyWxKDog0jq/kJTTeijEFkBUFriJDPZty1Hdf7nnoyxQ8926+e8eknT3/bZfWfrv/Jrp///d/xvd//AwIdh92eKJJaUCL4/h/+Dt1+z/b2mjKXvLq6RvlAqQvcmEDQ7aGhr2qEMXSjwwWJNppyOiPLp9zvBg7djvsXb7nedThdYIqa6cIyRuCY3ptNZ1RFSa4Mo9EUJsM5h5cKQqTMC7y1CCES98Z5onCURQLTLaolwSWuSXAW60aMyqimNSYzaaisMrJckTuFUgalErleRzhblPzp//qv+dM/+ohMDhAdmdBEF9jeNnjrCR4O93e8ffEVigYlNEU24fD8DbnIqKsp3kNRTXFkLLIpPoLJM2K0GF0cYdJ7xiGgpURFlZgNzjF2ibmg8gwjYGKgMoHKeJyUFMKSMxB6R9un5J2O59iuZ+w6huZA3zWYfEQ6yyQvKLMJg4+4OCJloCgzlstTlsZS5hOKLCKlSjBYKROfV0GIHqMli1lBXs7ohsj+6i373Za72w3L6SnBh6N9RqKEYRgc282e5XyawG4+0jY9Y9gj83RotTxd8ft//H2mywIf77D2QJ4rpCm4uu/53/79X/L6akSaksVEcn4xwZiRSLLtEHS6bx9rlfHIrnDRk+zUGc579u3A2+sth8ZRTzRKRubTmv32QBYlMSadux+HdOIvJFIJ5pM5+jyjzDJintPsRmwPX778mhbB7PwDVP0hQ/2EMWYJ8u49dWYwMmlf33OsOO43jmlL8W7jRTrhedfYfP978V1l1bG5esM4trx6vuFvf/BLTk4WfOc7D3hwUWCUTdYOmYGpEMIQoySvJR9+UPNv/q8T9P/+V/zyV1/iRQCT6kc2SCZFhcknCDVBmZqoC6LKU01IGZQyoHTaBwJomQ50JIj4m7TCu+vdYc27V5uizg4fOtxwj9AjP/6vf8WLL3/F6SxnUhkO45DYAkqRGVDSorUHJSjykrYTyXLSWfwQKWSOCskI1Ww7Xj97S3N/m5JTc4cKkiwoTNAID/ebLTd3e27uGiyS0guUgDwK1GgR0vL48ZrJrOLZN9dMsoxZPaXKMyZV8d5C54PHumRhsm7k0cePOD1bIyuFyAzbzT273QGja9gciM1AlAHvBg7tgc6NLJYTqlwRSw3DgUI4ykzR9xZrNftRsGlHrrcNW644tI6XV1uub+7RwTMpM4KNrM9mfPzph+Q5HA53yBiwfWB3v2FoR4wpOT9dM5sUHLY3dM0907rg6u0lkyLnd771Mc+ffc3QN4ztHjdWCCzf/8Pvc399x3Z7i7M9d3fXhOCo6opu6JnOJtixx8YeEZNRo+17irLg6nrDdn9AG41QkXdK0dvdjm3X8fDRQ548vKA0CcYu3q0twhFUD8Hz6LQm+sBkIvnF1zfs7w5Mqpo4KWh7EMKxvX5NHuYoPGPXEfuG+ckMcUwC9cPI6zdX3DvB6Qcfsd9t0QTc0OInFWWuEdrQ9h2v39ygdMmTJx/QNh1j3+CHnofna/r2gFaGaV1yd7dhsz1gUTx8OGFwPf7/bCaD63s+/uRDXFS4oFCmREfDbFGiyxl5bghRo00NIZAXgr6TBDxtLxlDsh1IU2CKDOl1guyItKgSPNNJRV1knJws2O8iZ2dzDl2BKQxZlrPfNxhlMEYxX0zJM8V0WnGymqVBoNSEEYZ2jxIR60a++eYF3vWcPzhJ6jnv0GXFioiLns32nugtJ8sp3nvsMOB9RJdTVBEQAbTJU5ddG0RIHZUQ/LHPlSBR4b2qKTKZTKlWU3yU3G0arE0Atm074IJLdGZAmywRpIOkPexptgcOuz3GCJ5/fcfvfvqQeWlShCwKQpA4L7i/b4htguZFU2AVTBcGlSuyeonUGXmlMSEp3qrJjLdXt1xvdpRZyWeffoSKjouHJ9zdb7GdZ1rXDG1Dsxv45Q9/RWUkFxdnPHy4wuuRONUouadcz6izCZurA/e3PeVkBtLw9NOPma4eQjZFZiXKZMmIgKfIR0zec2hukjLIaG7vtvzlX/0NHstkXhNIBFhHOOq5HN5zhEAdoSYiEkXERtjue27uDjz+4BSdCYRO/dIQHWMfENYhc4mQjug9EUckkJv0ADgMHUzKI1gxneoH0gIdnccHRwg9xsDt5oaxH3ny0aeovEpQUyH+QbQvHIcUxy5e9Di7R4geaSLICDJFp4TSIBSRpKlERKrFnO/94e+xu35Nv72htT3dZuTQw/LkgvXJiqLI0TqBkbRKNQ+EJAiJMhqhDKmxrpFKJbaCSMMa8S6uQYLCSMSxEvG+SpgWRpmApyKOSNHj7Zb7qxe8+eYrxrYB7yi0Qec1WgiCSqqf3ns2hx1X13s++uAzqonk9vkNg49MJqf85CdvCcPIp08L1tMVmYxkQlLPJ9SzkrfXr3n4cElWKqIMzNcVqg2ofGReGsrJiss3L2luGmzXEXyg0AVaauqiJssSDCjpDF2C/XlJ3yVwn7WBu7stQiimiwXm5g53v6GYzenGEVxMtZTeE5Wkypds76559uyaQmZ86+l5ctJLR4gRaz1tl8CCadAXOFtWnK1KDk3PWTnj7dU1RmnmE0O7C6xWp+9PD6tCIZAQSCR5oXCd49HHH1DPZrx6c0lUgrFrmC1P6PwtnQvsrSfPKtYPHvPFl18Rg2ff9Jyul0ymM7788iuePL5gt9uQZ4azswfsDoej29mjhWB9smbfJLVmFJJiOmPw8PryitVqTXto0KZESENzaHj44CFlUfD4g6d4ItvNHXlRIlF07YFHF+e4rmReFWRG871vf5uiLGnbhtube6QwFMbQNQeW6ym5dswXAtvd8Cd/8i1MXhFkjh0rZouM15ct/ejJ65JZZRkPeyZa8PB0zXa/Zd8dmFYZy3lF13Vo5TmZFnhn6Jo9w+hYLk/4zu894ub+joGIyzM6O9C6HtGMaHtg1I7B9WzfbLHeYtBEawkB8pkG4xlai208RgpW6xMOu21SOWlDWVVYDzd3G5rBkk0kgwv4tuOrb57/tsvqP13/k11Zlqp93jZE75Mm0jrqomS9rhlKxVeff86sn/LV3/+Mqqj45MNPk/kqNxRZilBrpdE6MoYASIqyoqxntMMu9bZDRGmDznNC8IzDgBWC4CzBjYztAR0h2BF1ZOZE7/DjQDeOxBBxQh51cpArg5YGI9KQeRwsUqT+rAipJe4HR79vkFWCJvZ9S6EizW5L8I5JaRgOO4TWqFhwvsr51tMTctmghUUEydhbvthfUS0WeAddvURwCmHC6eOHXL95yS9+9guENGR5SV7WLJeGona4CINzaKOwtkOrKd0YUNYidEZRTiirmvn6DB+T4UISmc6mzOqc2bzAlJDVOXq6ol6dcvPqhq4dkESyUvPRtz7FaM2Dxxds7+5QWqFMRjEvqAuDVorRJjiuVoFD2/Li1UvCRHIxixQikmcRQTJT+RCx3uG9JIwDPlqMlBQ655PHZ+Q6cVyiCJRVSa1ScsR5T5HnLBdLZpMpzjYQLHk1w9QzxpijleJP/vh3+eijC5TuaZt7lAloUzC4jP/8X37Er379Chcrlss5Z6canQ1EHM7blMY8Lu3BC2JU2NEeF/XEQAgkm8LgIne7hqvbHYtFQV0Z8qJkd3NPPx7oYk8xBUmGkBkxGLSuqWeCWa158OQJ1vf88P/4C4wq+Ox73+WnL3a44jH5/DNGtcLGY0XVeYyU/MOAwjtpYyC834O8CwYcXeDH//I3pz4iWqSwjN2GZrNDxQl/+ed/gUDxvd99yre/c0JVpcSv0AXIgigMURyhpxF0JnhwseLf/N/+F0SQ/PwnvyBfadaLBXU9x+Q5ylQIXSF0nl67NkiVqhRSaqIQCKWODC5F6sSKY+z1H6QXOGq/3wO+PDGOeNdi+y1jv+UXP/4bbl58xSdPzhB+oMo1exII32iF1iqZ34xCGsVyOefBo48ZreLt2ztevHyT+PhAcB47RF6/usHZnqZ/RZ5fJb4Jkbpw9IOnOfTY0eMGi9GaR9Ml5/OSSnqUHzFSkSGJNhCdxHlJvV7yL/7lH1NWCq0CRJv2m0Firefu5pbZfMKzF88pV3MWTy6gLlksFmyev2Hf79BS4rG0rsGKnqwUmDxHyxGlkpb74uIxY6h4e9uz2Vme3w5c3jdEXXA4CLAZzle093cUwmKCpM41q8WEEBqiVBQVxGhRSlHWOUGQtJP3l2SVoqynfPTZQ/JqyYtnXzAOHbvdFdF1TMqM6AyZ0XgrGA9b+mZHOLL7FrMFl2/f0vUjXT+S5T3gOXt8zvpkzdXVn2NHy4PHTyDA1Zu3ZCaglaSqCpCQizntYeBHP/ua/cHxnU8/pNSJbZhYoIFgHXboUcKRK8/D0yU//NkvWc+XVLVhMc3YH2Doew7NiBEjZVHQbO5Zz2ZM6zrB3GNSf+cmIxeKMqu4HTYMzrKcL1GZZnPo8XRMZiumLWz2HV2Q3O57xq7noycPKedznl9doUzGdLpgdnLG9tVL7NEZfDgcuLm5+63W1N9+yDCOrJfLRNI1htP1Kf3oGVyiUma5ItMCXU847PZM6oreQRyhKmeUhSDPC6azJcZkjMNImReczJZoGanKnLrKmE5LZospY39CWZTMfODCBbbbA82+wVuX0gYnc9ZPHhFCZLmYcH+/o8orvO1Yn54SlUHqAmVKsqKmqGqUSpOeoiqJ0eN8IC8qprM5WZETxojHUhYVppwiy0lKEFhHPzoOhwEr8gSRifHogU03SBHCUYUZcW6gOUDTWu43HT4ITJX86bJTuL4nOk8/diid4z0cdnv6Q0v0aXhh2w7vQvJAR5UeSLSirAqWsynTzOKVRk5nOKMQRr2P8yM12ijeXt9xfbfl9KEm1DVPf/d71JOK+aymzBWnD8/wb94i6j0Tk7HsKxZZhhsbFJ5cBkS0qTdUz4gqx8WSYdixbwbOnj5FSMV6NuHk4WNiViJNgTLmSIoGrUdM3tJ3l2iZTjGsi/z4Z7/km1cvmS0niGPP0h+d4JEjSTrI5P+O8TcsARHxEQYbePP6lm9/+0NmRY4yCj84bAjs7reUZU2tK/zYY7sGldVIpanKDOMjoffve/cxpK9zEEc9VxhBjVQTz9hccfnNT1C6wj5YoLJzRMwQUSStYDzO3WP63KLwxNDgYwI3xSQiRIgjF0SqtOAe0w8+RKICXZes8ye0Vc3blz8hOMWTDx9QVhV5fiT4Sv1+8RNSE+XRzazMcXp+XNze1R4ECPGbRfB9PywKYjiStt8NRnh3iuAQsYewpdu94fLZl/TbBj96JBKpNNNpjVISpTO+fPmar+89lEBekdUFdZBEoxhsZD+AHANzXbDIC55+6yNkHFHKg7JMVwUPPj6nnCrKhzMgJSlCdGgZ6JoDb59fI8OIEukGKtBc397jPGw2O4wKrE5m5IVKVSUJXZf6edPpDOkVhc7xY+Tm+gbrRgKBbhgIziOFIXq4v7rk5PSEiVYsypK977nabHnQLakmNblSEBMF2HnF6BV9P6YaQ5ZxfXNNnhcYI5nVhvlkgh2TbhUh6QZH1/TMi2nqeaIotAFjmE4rJmXNdtdgTJZ0qMExLTPkeoEPkcl0wtn5I3btwOBTz7kZe7jbcL5aEl06XVRCUkwXWBvpOov1kWF0+LZnMp1wslqijAFWmCzn/v4eqTRBSqLRbA57lJIYLSGMeCt48vghQQju2wOHdqDrBlSEXCpW0wotA1oIJtMZvfVs9x3N6BldRIuMm5tLXPA8Opvx9m7LrJCoPGO+XHB1c6DtPV4WrB8+5OXbK4S16e/3niLLaLouRftMSVbIdAq1OOH27o5uHOitpR1Gemt5qAzs9txu71CFodOWLgxElaBuw84SOGZyY7pvN22HEAOZKdB5jpIjznd4D10rmZaKxSQjw7KeVyANb66uGcchDZqix40D1g7c3/e/7bL6T9f/ZJcRMWm9ZALwBhERIdJ0O7y1aGXYbq8IX42cnS4Jo+P1yy8psxJjEt/HupwxwG7X09kA0uODQmeeqqxYTAZoHB2OoW8oomeZabKy5EGuKQqBmRcYo1jKgd44+ommPD0lOI8bx1RRlCpFuYU8KoglIaYTcogoo5IJIqTDklhVPJ5NkwnLWoRYYHLDqiiQIlLkGUspkCJyNstYFopSBDIN4vjg6mRAqlQbJSqstYzWgXPcb/aMLu0JMyMSW8k7YnQ42ydxnx0oyynOvqOsx2Q3iBapA/1wYLvZ03cDJjO0TaqB7acl212G8z3T+QQvvuHy7TWXr28wOkcrAEdZRiZlwZsXL7i6vuJ3/+APmJ8siLMp+3HH6DqKUhOCRIiAysASmMznnJyfUquO0/UEqTWHzrJpB3b7nugjFxcPOTQtk9mS2WyCHDv2u3uCHXC9RWQgNbjRgsqJeA6HPUM/Q5EOL/q2waEhE3z68VP+4Pc+o6ojQ3dLjA25yYjk/PTvX/BXf/MLtgfLZGY4Pa/I8xEhEvHd+/RLHp9pRdSJKh8THV7rtBa6dKcjRsHdpuPN23uePDqhmmRI7VBasb9v0LJAZS6pFYNEqYqynlBWAqMFUlp2z5+h8wN5AY+efJf1d874ZjPlnim9zHAxDc9klGgFQniIBgj4KHEeejfSD57gLMt5TVUqNAlEB/yDVIAH6fBux93NS6pyyY/+7gu++uoNn333Q77/ux+yWOZoE0AJotQgEnftXWIzEgkyoozg9HzC/+Pf/AmnNUxywcX5GUVeIHUBpgZVpMqnzo4MBoOU+rhzSqyUZAn4TTXiHdcK0mAxvut5vBswYAmuY7QNfd/yw//2Q66ef82TBzPyTCKdxiiTIH8epNRorSnKCpQmINhvD9zcdLRDZBgjZZ4lBlzwWMB7dzQ6KdrR0TtHCB4lBQh7rBoojBKczKYcDnsKH1hlBYVw6EwjZKDZ9Az3lldvG15tDtR9x/xiTmYszeYthBEZdeJ15CXT6Yz72x2//vkXFJXh7OVzcC15ZujbjplyCXhYCWqdQ6w47Bu22y0yerqmwUdFEAVWlOxj5EVr+fK+5/q+44OHC+q84my14LMnT5DDCuEbchPIs8BkllGvp5gSVFFjXUAgkSdTpoPFDwEqzWHb8uLNJZ+gqW2gyAXRR6oi417ExIZxgX5Mtf8syzBFSiKMrufQ7um6Lul7XcDZgFKC7f2Oqije/yxmZc5mk4xxmcrYbjd0h46iKtB1lawefccXz15Q1zmfPH5AgPRMEB3eOobWMg7pvdDalrKqKWdTru4uOX/wAD0x7HxDzKA6JtGVElRlyTB2KY2jBUVuWE6ndNuRQmiMVMisYDJbst/vWK8WtG3a01+cntK1L9AiPXfVixWLs0fc3G+4aQNCDWyHe87Ozph3BwgWowOCESF+O1X3/x+EKo/3I0IaIp4QHcPQJy963xBcpF4vKHLF5dsN83lNlmdorYBAZjKEhKFv2dzecHd7x2IxR0vJzfUVi/mESZ1TTwom05KubZhMZrx8/YaT5QlX17cMffKk55ni7qZg6O6RWvLy5Sturm/4Z3/6zxCy4OKjD3l5uWewnrysyLOSGBNQL8SAfO+cjyiZToFBppPQZHhNv6ImxDSxtM6zbxpilm48SiVQZRCpaw0B7wIiJmq58IH+0IFLH1MCRZ4lWnqIaDQmzxDG4EJkMpuRmzypnrxlNs/Iy+I4+QUCWD9ioyUrMvIi496OdEOHjwXaJ/+xsyNVpZGmZNfDwSrmuuLhxx+ic4XSsFxOqGqN0vDobM4j57E3d2w//4p6bnBjnqwhzrLd9+SrUygvCEUFynBy/hHnxoBQyGhwu46x90RToLOUzpAIlPBkWYf3VxAbyrJCKMXnX33Df/3rH5JXJqUT4tGqcHRwCykRQb5LsVNkOUqJ1KP37oipkLx9e8tu3zNdLpBSIEyGUArrHZn34AOOAXSP0jlSSqocCm0JQ4cfMxAW+Q75KARKRnQBpvSIeEe7+5Ii2zOZlkS/I7oZQiuiTAyR94mhd+yfaHHjARHHpDjCIJVEYhAq46gvSNWMGJDHxTAKkEqT5zWr5QoTJNn7vpZB63TCIKRB6IzkwDFJE6XMcXBxrE2ImB58xHHq/u5TlL9JbBxt5PzDMwQICAYEDWN3y6svf8Hd61dJj+TF0QstqOoSFyzBwS+f7fjFVUDVjo/WS6LMyTPIi4Ld0DN6R1AKET2VlDxYTBiGbbqRZxmzWYHSnqg8NgwUpiRah1GC6Ee0TlFVnWlcSIotaTK6pud+f+DswTn32xuyUrJYLanqEmWglHnaAPcjWijGdkgT3swwmZTc3kmkFJiypG2TzzxiWc4zUAJVTpmICiEjnbN0g0dLTcTQDpFdA00nud1aLDm3+4Z2sFRFRNBRlYkib5SmNgVtb7m535Edae5pMxKpKkOUkgcXp9zv7ri6u01D1PmMJ2dr6spgC0nbW87nNVFI3t7eU+dZSsj4SNfuGScFZ2drCiOYlCsG63h7fYvJSnx0oCTtbk/0ltVywWQyYVpXvH59SRhHZtMZRmtCZnBDzzi2lKYmhhGBpK4yVJ6zWswZx3tQAhE8zeHAopTpXiaSbWi72x/5IAptCiarNS54mnFgP2qKKmNZTBisph8Cu/2BN29vefDBU773h9+herHkr3/wE0KUzJZz8mjYNR6jp4w+0ATNm+uWk/Oa6yHy8vKKzvZ4ETCZ4e7V10TnOfQHyrogyyVeO2Iq7tJ3Fu8SA0NrSW4UBAlxTCmqsSOaEa1T17IoPGXmIQzkyjErK27u76kyS3lS4lFMqoyZmTF26j0d/Z+uf3xX3/W4fsAXY6Jt+wQXDM4mNks9wbYNP/n8Cy4uHiYd9TDw8QefUOUVzo0oKbHpFkymM4beEYm8ffUCo3MYO4oQOS0F7egoHszRH5xysj5hNq0IbknXd2y3W8paYLIa9el3qetJqh+NI86P5DqpxlwAIdWRIxLThlulU9aqKhgHi7ep4qmNYhxH2q5Dm5xqUmPdiLMDMkbG7oAdW8rCs8ih397jMosUAYnADhblPd61uFHQ7w90hw5rO65vNox9ArMFPD4I2qFnsJbBDiAU4zgwdGkPhRQMowUBRVUQBJhcs1jN8NYjhGRal6zXCyaTknKSE8LIdDEBUyKFQYscrTOUSOmDyXROYRTTxZzROYosY7QjrusISjJ4z2EIdC7io8d5jXaRH//iSx4uNGXY0jYzposFm6bHCUNvoekdhRD8zd/9PVIo/vSPf5+Xr99ycb5KXA6Ts1xN8cGhixxEgnLOZzPWyxV+2KMIZJMp2WSFlxWPP3zK2WlNcLc4d4sxyYL26s2eP/vPP+D5qztMlrFY6VT5UD0iWiAgQ0CEiJLxfYUzCCjyPB0E6gwfAsM4Jr6VF7Sj5/XVHbf3B5brU5TKmc5PaLdXRFECNVouwE2JsiarNEKNhGHDcLik61+zODHMZhOquUdqw6XI8I3CR0nwMn1uQmBMSlJab2isZTNYNoeB+92BtunwtuPTpw/41tMHSLqUt3hXoyCC8BAHDrtbgvM0beB//49/SV5K/uCPnvLkgxlKDyDTnirybu+THvKjD4BLGvAIUkQqY/ndbz0AD3leIaRBmxKlS1BFeg5QBiGOHIZ3HIl3ikqpjoc58rhzSqmLKNNnfZz2ACEN1lyPdy1d2/DD//4jfvg3P+bRWXm0DIAyIp1ke5EOO5AJqC9TQjVGyXa7Z3/wuKBxLvXzpVBIIoqkZxdaEEKCegeOukQhGEJEWkuWFdR1wWSa8brfo4LDjz1ogc4NkUA3OLaD4GrnuPeCNmqCygnC44XCeokdHHYYGPs9XWO5v9mxv9uwLNY8WS3pDpZqUtKbgJAFkBSvdgxIWfLixdvEWUHgMRyaQGtHDlby4r7hi5sd10OkExpPQIaOWa5ZVpHl2RzbQWYA5ZnOK6rFBFlFoo5H+L0mDyF9PYfI3e2G7V3L3e2Ok9UOFTWr1Yov77+mHwcG72kOHePoyNuBoWvYHg4gAkWl6XcdWaFxwRGHlAqP4fi+7iyZ0olrEhJ3YbAWlKIfLJvNnsWkoixztJYs1wtaaQnOcr/ZMJyvKPMCIRMfbrCRZgy0o2RwhheXt0wWJ7y8vGN32DNfrJhNKmKw1FWOMoZ2GFmtV4x2ZJAp2S6UwgdLVeZMRwhDw9lijs4Lbjf3SCmZ1AsOu5am3fLx0w/xD5bkWUSfLtPf+fotgwfyChcctze3qDzj4uIB7e6eTEXKTPLk0dlvtab+1kOGtm8IBEY7gATrbHowOp7kd12LtTUQqOqKKAL90GGtxBiJFEeonck5OV1SVjnT6RQpJdN5zWxWU5c5RZlRz2r6rqGuJxTzOdNqymy+wA4DUiXabD0tWZ2uQCRlkCJSlZqu79neXiNjgclMOhWMgRg8dT3FugGjFFIl/ZjUaSJZ1TVOCWw/kpc1Ki+wweOPoLs8yynLkgGFVoYyLwkhMjqb4kZC4X2yP4/jQLu/Z7+zRJGlm5DzRDvSH/YI6xFOcLfZIjKDdQ58QEtFZjIgQlYQpcIDLqaTO+sVvVfcdYE+Sm69ZNMGgrIUJt32tFIUMdJ1W1683TKMlu7nn/P6q+coFVDC8enH58zmGUI4hIjIEIhNSy0EAsvY7dBZTp5V7O8OvH3b8HLzK2yeIzScP5yxWtcIBdVkTVE+BDFHZWUaMsjU5zPaItUd43BLnqeO9N1uz7//j39Fbzvm00l6rcGhEeQSlDFY50kHMMk6IqQ6sgIElnRDiRGaduTlq0sunqxTaiLLKKqarjikk3wCWgDWErOkkKq04OGpYlpFhHcpVqiSIhMRyUuJyUeivyPYG7IscP5wTVFOQbQ4v8doQ4x5UhWJd/AigRCO4HtCaJFifL9wptdgEEeGwru4XYxp0PAO74CLHO42FEaTv/cxK9QxvieVAZkl/RWpeqFk0oT6EN/TjsW7xMI7ccSxFqF4vy7+g8HCu2l8QAoLvsUNG65efMH26jVhHFC5wogUwbLeoYs0zNnvemKxwOsWoyZU1QmSjEkpeXi+phmvEMIdoawBGaFvG4bxgI8CjcCHCh/TAmuDw4iIE6CFJAjNGBybQ0/nIjHLoSjwNmCFwCvFECNeSUxVUUym6DJHZJoqrxh8YHe/p/MjXkQGb9F5Rm5LsixHKkVWlum0yqYTrqff+4Sb22tuXzcpxhw8h3HkZt9yv08Wl6YbeX15YNeM7HpB5xVRV4gY2Q8OLQVlXSFVxWIxxdqBcbxjOavRhLRQysCh6ZBSMy1y6llJtj2wnj8iz1IPrsw1WSaZztcMo6UffDLrxJKmhbLIsbak71uyTLCaGBQphtd3HZOyBFPgpaMoBUWuKZWi0opKgCIyr0uq6kjSzhWuyhFhQqYESkt8iDT9wPPXb9B5+ppN6imi0hRKgu/RakRJT6YVo23TAFdr6tkCkZVk9ZyFzGgOO+57T2wkfbCUYkBdHmi6lm6A9vU1bw6BbeNxumLTDNzcd4w0GKnxo8f5mN5P0fGm7RltSxs8QQuElgiT1qbRD3jlaO2BHp1AxVGiTY73ghgUIsZ0785zJtUU4ogNIyF6YnQgHNU05/zBkkJL3ry6RuDJuxLrBZPFhGo6p+tG3DiwWFTIeSJS/9P1j/PK6wXldIXQGXWZE6Lj6s1b2mZkvpgThWGxOOHxR58xX53wo7/+G+pZRVZOAE1ZlihZ0vWOcYiM3nFoRnrn+OLzz6mKjLrIU2TcKPIQ0UKAFXRvruBOsVxOqTPDOLymEBnrasnDiwtub18i2y3WDWijOJ2n99lmu0NiiEh678jKgsl8xWZzYFmv2fuWzWGDzgzzakETGzo/MLo9stmwWs7IMkOwPa4WBKfJjEQOGy6/2eNcS/AeEWA+mZA5y363Q6oS5Uf80NF1B0KYobVCy4w8K3AayrICGg6Nw2SGqSkpco33kRAc8siIii4QhcPFSNt2iSsUI86O9F1B37dM3QQloahrIhHvoG1HwJLpdHJ7aDpCkfH6zQ1VkQCGTkjQGQFJlOYIrkvUf20MWSVZZIrZyQmn5ZJ2t+Gr51ccbCCbLAhB40RBtJLlyUPKPENmNbPVOb33/P3nz1ksKj7sH6B0ZHY6YFTGvum4ur5jVlVMS0XXjvShRfmc88cXPHqyRpod/eENSo9oU9B18Nf//Zf83Y++ZLSwXpXkhSeEDqE8OlqEc1R5zqJKEeludOw7S+s8zqcofRciznpiisQhXUSjeHt1zzcvLnnywSmVKdC5oigNSs7Q5gQh5wiVozKJzBzjsMHbe7LMMl2WTBZl+p6qA6O/ZFVp7vqS3tp0kBI9aXci2bQDmxvL1a7huh059J4hRUbRXpFf7nny5AKpxHFvknTl4NO+wbbsN3um9Yr/5//7P/Lq7Wv+9J9/h+9+/wOKKoESo9JpT3RMiXKMoIvoIMZUGIkR13YMuy2lSXVdqXOQBVKXSJODyhEiJYPemehQApT8H2CPEpHqE4jEZDhyMOK7bsjxNQQ/4G1D1x340Q9/wo9++FOkdIw2JZVDTGkf70aUkvhw5GcpcNEjJMSo2B+GtNZPpnhnyVTaXyMCPsZ0YArIeDysCiCiJMQElx1sIErHg4cXzKcLJrnmbL3iZDmnUIIwjjjncWMAPNlMU+eOUZ3x/NaAk7T7nMPBs9vssW2L8Y6SgLQD51XJhMju5QuGcUt7JxEiUtYVQsk0wPQCZx1922PykqycUE0DQRj63qdqVVFhzEBeSIypyJQB12Ow5CqZQ6KwGKWRucLkihhGvI+4GBLrRQSCc0g0PgiCUHRDIIYsGVhissU4BLquyWYzNp2lCxYnJH2IdNayXM44uzjjfvcM7x2T6YS2HRisxXlP8J6qyAgRFssld9sd1WTC6CNt19MeOlRmWJysWZ8s2dkWISOz+YT9YU9e1xz6gbww6DEktk3juLobaDrB/dbSWkPrFJvG42LB5e2B/aEjz2AxrVBGY0xG343c396Q1Snl7CM0Tcv93RVVvWRSaQYXud/vCEOPi3D15jXb2xtEtMwrzeknjzg0LW5a8eb2lquXzzh5+Ijzs1Oub2/YRsvdzSWn0w95dH5CmUF2sqSzvx3g+rceMvTjyOAsWZEniJ1Iv5f0jopCFkghkDLphuxgMaVKDvojhC5EUo94v2MYRpRUCSYyDHhn6bKMoszphhERI+MQuLq8pZ9Zrl5f491IWeUoGWmGllF4yjzn9etLSpMW+Bg9ZV2y3wl8MDSt4FdfvOX0dErXe5SKBArmJwHrkh+9HxwQyJXE9pZRtCirGGybum9+RKsCpQRKaCbTOVFVHJoDzqU6ATbSDSPX9y1ZEFxf74giPcgjLNOq5HRSE4RkVIrLtzt+/dU3qKKkdyNCRBbTGq0Nu+2BzW7Hkw8foYzhbtfwzb3H6ZpXveI/v+ioJxUhqxiFQBuNJt0Li0yj+oAbPJaKeQ0frmtWE40OHcSeaTiQtYkmG0mKvkxJ6jzDCw+1QGeSLItoY1hGyaKOBA0BR100ZDEw2hLhgJh0S3leoZQ+mgIsJtth3T1SCrTJGb3nz/7T3/Dy7WuWJ1UC6MSIjiOnk4qL0wfYYaDpLa8u77BSYYXEiXiskUSk0mnK7cF5yZdfvOA7v/Mx81V6gFeZSYBPn5R2Cs3Qx0T0dgoTSy7Oa5TKj8/gAoRHKocpPWbiCfaWGA9IA/lyRp5lxKhw1uLDBiXKNPUO8sgzAEQgMuDcFmghDsRoiVKm/n3yH4IypK5k0nJCmotLoN3u6HcHMmXQKksL33HCHmSCRkp5XEyjRAj1m/rFUZkk1HEaTkrtRJFitfG4OB5XyzT4OKYq0ufgEaEnuD3Pvvw57c2LZMoIKdaqlcAYRZCpV5fpku1uTzQgZGBeFawLR84OnWkeLwsuLyUCz9B3hInGi0BvLchw1HM5LAFTV0gj0sJiCsp5hXABQmS7u+TNbcv1fsDlFYcYiXkGZc7qwTmL8zOKtiBfLBDVhGq5opwtyLKCZnCo0THLc/R0QjHM0dMJlVEUd/d4pXBSk1cLROso5xK9WrC9fEPMC0qpYey53XTc3LaMLiKzGmthf5B0o2a0ERtAqBwZApkqOF0tuTg74WRaU2pHjDs+/WyGNhG8g9EybluurvbEGFmWmnJqWE+mVJNpiksKkZgZRCazaYpHjpbbqytmgKgNi+WMrhu4vHZc394xhp66LlFZhvAtu90OawpuDj29hYLIk2XFSamZyoHcaK59w83eY1VBUMmyIQlURpMZzW6ICF0jmoEgIzZqXNQUSmMmBRPVU4YNueyZmIg3hq9fXvHVdcOoStYPLti5HdvNhvZwQAL5XU6mTQLcRk8gHsnVO4TscEHS9AN9iNi+x8WIlh4pxHHwGFF4GAI+jih9BL3K1IVum56ht0QE1ts0SI4K7yJKB4qsINMpQpoCRRrnYOwtw9iTZSUuCLyT9MFxfXdP1x2wtiHLFIfWYUeH396jbq5411OTUpNlJaMM/NP1j/P6Lz/4Ga/vG05PT1mtF4xjw+sX31DkOdXJCc1+T8gylucPeHt9xy++fsGnTz+lR+F9ZFoWjNHQjoJq9YS3z1/TDgIfPN/6zseczCYUeYFzA1EFQggEl4bN6V4byfMjzyjOqOqS5cma6awAKqoysaSkiMymOcG54wA7WYKGIZBlniJvkGVLRsTJhp29xJiSWRZx7T0udox9TzdYsjAnFjlNu0OJROrPZIb0Ne3Gcnt9Rd/24AN/8P3fRTmLsC2z2YJvnj1DBMfjR+d89p1PUCbgncWoHCENRicFbT86lNaIENIA+VhTSnR+wW6/hQi3VzdsN8+OznaHtSNGGbwdMEaSF5r93T3IA9vbe/bbXVr38AgxMq0zirMTMi0Zh46ubcgmc0Lf4foeoyRnJ6fsh5FDmzb/3luijOmUNVj6AfatpIkau0kcHq0M0Xfkkxl1VdK7QDFfs9vteHF5jbpuuNqn70l1PjJfSN5sWl5tW+zLa04XU7RUqChYLwoePF5Rzy19f0mUHTpLj+a//Pw1f/affszdfcfibMFkblCyRwmLCg7tLKWCP/zWp3z85ANeP3/DZtfx1etrhpAUjgLB6ALDMCKTxBkpEiiwbRwvX2y4u4qoxRw/CnTxAK1rok467LwAYQZsvyW4HdqAEjWmTppEbWSyfoyWB3TsmoZm8HShSFaj6Pj6akvXNWz3gdY6xhDxR8WjimBEwe1+ZNcPCQRISgBLPAIHcWC3uaQsSr785Uv+6r/+DevTin/5r36H5YkGPR6Tm8eEqNKIdy4Ln9TlMgJREoaRYXsg+sRBUSJH6gopS9DFceCgiVKB0EQp3nMYUEl3H0jv03cesd/so1LqOR3e+OPP8YC3O/puw49+8FN+8N9/iMCjspzeOpyPjM4zn5Y0255qkmF3A8mmGQnSYzLNOES2Y+AQK3qKNDycTpjWKWm8ud8mLpGMOO/SgZWIFLlBak1mcoxOlaa6rMmN4dGjxxR5gchL2hDZ9p7RClon6HXO/MEZBQpd5vz3n9/gw8A4OOwgaPYCMcCFdtRZSyEGyjyibcvhZof3A8RAXudM1yuiTvtLa2Ego2lGcgq60NEOI6eZYtx2TOslD0/PeHHb04ySsq4olCUOLSIEZIiMTQfeEkXAxcjQaSozR+qYzu2EQGiDNCBRJBufZNuMdE7w7NUNrVPcbgesKtiPgcYF+gBkhqAgqoAuNWiBMjlZWZKVFdVsTmvvcfQ4EfFuZFnM0VXBdDnHv1Ds+j7ZgZRi17dIFXnw2Qecni9589Mf0717/6uMPki+fnPHfWuZTz0g2GwaLt/u2TWOpoMhGroQ072rG/Fk5EXJcmrITCDPBbUQNMZiG0EKlgsOTcs4jsynFfOzBfVsivMwn2bo7AEqy9lttzxYVRgNTz96SKbgfrPBjWD7hu99eMrqfM50VjNVIw/mJVImWPtymhNti8wK/P3ht1pTf+shw+/98z/BWYuSEqUUo/e0zQgo3NBBsCnBLaFrO4iC3GQUWc5+u8O7xDUYxpHtZsth3+DmkcN+hx0H8jyjzHPKMmfZzYFIZjJuL++wvefl60us99RViRCOSSWJoUOu1wRr6WykaRuGrqGwI9bltGNG66a8frPlq9dvePDmQF0qzi5GVg++y66Fy7uBrjtwv/M8ujhLUZuuRxWKXdszBIkVis4FOmdxKqCLEk3iDmidTsRkEEwmC273nmU9wYkJiOSUjTEQMGT5lKazvL7ecnW/5e6gUS4jqhKI3G0VvQ3c3MNuKPnhvsUh6UfBrs0Yco3PM0Q1515l9O2YrBxF+h4YI5nPcnJFIuybHJM5TlY5H60ypplGUBDFmDbex6qCHwYkAa09wUBZZAgZgY4yF5RFwSOZgYEgNS56xggDK7rsBKGnmKJGHysxSjl01oK4QzCQZyVS5fziZ7/kb3/4MxariizPj1NfR5kHvv3xKd9++pj20PHm5SXruuTXL97SCYGVitGDtwlyleJighg9d7cbLt/eMF8+BiTCaPKywnae4HMIJcFWhLEixApJidQZEkkMaRoutMNMHaZs8WFLVB2mNOANWZGjMkNwATMG3NARQoNWFVKk+FwUiSEQfYN1O3LjkgdXJNOIUBlCmRSTRxJEwDlHDMflKkZ8P9LebclIoFEhDEKVSFOCygjvBhRKI4RCimRHCYk+msCXx9SCeJeU8Cm6GN/9eg8oAogk7biAYJFxAN/y7Iu/5/Of/ZAnZwu0VKS9a0DIgMkUkBINUmh8gNWypni7I5cdcxkx44gMGQuhOa8LmgBjs03RfjUyup7M+COpODJay9C3EBT7tqGaRpSNGBTeQpYt+PjT32N1YgkxYzFb4r2nbRoyY1gv54zjQF7laK2pVg+T/1hqHj71jO3I2A9oJTl9eIGuS4IxnDz5gIhAqhJdFSxOnrA+zbGiYHL6Adki0A+OtYjkecFoA70lRQht5GFUhBgYx4HRddTzaeK3jIEsywluZFpKzhYlvev5nT/9hKpOvdnD9Z6f/Jcfku8d59WMSS2pJ4LeRVzconyPRxG9YBwtzbjDGIOOgcpumZrAZJ4zmQr6IoNe09wOPDo54fzilKzKyF8Jfv36G/Zjzjc3LU0nmWn4+LzgW58uOCk1N3vHr1+/4X43snUWiyL4ESU80zJjPi3JrUPpwBgd0UzonQIjWc0zuhBRruXhac53H59SaYGNOX/+g1/x8mpLI3purGQ2n2LHAWsDRmWMziB8Sq50vWV0ySGvZBriJobEgAtpsChEqqblJkNnBiUiWgXCMKIzjzEpLhqjwgZBNwhGlxGRhJCATgBaaLTMcKMg2oDONSE6pBTkUUE0yOARQeFsRAiDj5L73QHrB3z0jKTEhtISGRwxdqmHjGIYIy4OvLrc/7bL6j9d/5Ndf//NNV+93VGWOdNZhTECb3smVcFXr69om4boAz//+hWHw8h2lBxGw12TNM49lsE6fv3VFedPPuUvfvIV8+WUw+4t64mnWS+piilt1/Lko0dU0wn7zT3TSc1iXmGk4vbmmjevX7NczhnGgbfXr3l7e4XUmtlicQRBe3xR0hwO+LKkKAqM0lRCUOclwTqyoqDIMk5OZyzXNVmeIvzr04rBpbpbjEfKf4TBLo+nuRJpJEVuGLsW5wRl2aOFoJ7O8HYkIone0zcdh11Sd/pmTzPuGdo2DdxMRVVOsM7RDj15nh56vJB03YAQyWiQlTlKAVKjsozJfH48Go7kxrCY1EzrnPXZgkAC/en5CfPlFfP5jLbtidGTFZLHT58wm04pywlvXr7Ejg66nhhHyixjvVqRzc6xV7fc7xtiDLTB8+Zwx/Xtgbtui3eSvS+4PIzsxsTVUDqDELm7vWE+m7FaLdk3Le2h5XAAYwybaJk1ht0PXvLwYeTLG8ez25Hr8cDDkFMqyXQS+Ox8xmzhce6aGA7oTILKub4e+f/8h7/jq2eXVLMpJ2cT8sKjlSPDor1FBctnn37IH//Jd1nMZ5S15stfvuAT/ZDbX3xNUZYcrE/abaFQUR2rq2kIRTTsN3D9xkMv0KpCZyVOKJQJ5FVEmAPOHvCuTzDCokaGiLEamWmESanIUkWWouPhrORqF7gP6Ti/t4FfvtnQOXBRJy7VMbGpZaIVBCE52MDNfcP5cgnRIqJLyQPhafe7pHaXM/63f/tnDL7jD//ku3zy7Qt0ZpFKIGWW6hLv0ikBojvuvUJSq0VrGXcNobMYYRDKIFQBokDIHKnylAw9KinjcXiASoYuIdSRvQD8g+RqknmkanXaJEWInhg6vNvhxi3Pv/ycv/tvf8vQtdSTCTFIRttjvcN6T1Zm9G1PXmn8bgQTUbnAlGnfOTrFph3Z+BJFzyzLmM/SXrhrhgSef1/2lWiZBiHKGKazOcvlCjeO7LcbtHe4Zo+RkuAG7BDReUE1KyikZmEKHKly5ZxFFTmdOzCZL9ltBU2jKaRmc5lqVSIm1oqQ4EhgMhtEeiaScLBDurdE8KNgcIKb+4YpFZiR0Y8419F3G3RUqJCeYVzvqCcnGN8wdsnYE4LFxZFMRvLc4NWx6pvlIF2qUguJqidEn75PQnrmZ6esH7egD3SdZzcqiuUDFvmULijqxSlZMSfTkpP1gma/oZgtkUVGvTKc24xieUbVRwon0PWc6WzCMLTo6RxrCvLlA1YXDVaXdH2DyGvKZUi199mat4eRN/c90mSU9YxptWJ/OLDddFxuPNNJstns9g2HJtKOEesENjh6G7AhsFwv+ejRIx6ezShzRwx7yhLqmSFGS78/Jw4O2/S8fnONsyMfPDphcbIkCokyhqJYYYoaoTOG4SzxAwnM5xWCSK4E97f3PJhnnE2XnJ2vcDFSi5JNk/HmZsv1dktY5CwnmtFZ9pv/k8GPcexwfUc5nWK0xihFplNHX4sJRsukeRG8jxFJpTBZwSNP+oFH8OqblyAkznmqaoKzFq0UZWnIjaaocmarOUPboZXhO8ogo+Czb93Qtk16kCIwm2XMz2pmixlPnnzE9au3NPs9Rku63R4ZckQ25bpRdPqUQ7tjdxkRsSN7/Zo3zd+x7wa2rNkOe5prS0N7NFBEguzoRsftAcY20AuHNQ3SGA6do6HHWZuAjx6UMMzXazYD/OpNQ4yJcusjuAAv+p7+q5ZtH9h2ARsk6DnKF0QnGQdH0/UcBkvnFI2PuOgSSTaA8x6hIlk+xZqCwzjStn2a7ul0ut11PTZ4cg2FiFRiJBhL1zp2RrB6OEergHUj2ujUyxeCgZCI/BJGN+KjJwyWqqoQEYbxQMRTmRqlMoYx0FkJeU1UE7SpUDpDSomUAa0GlNxj7RYjST9Eu5Z/9+//ktk8R2dHIBUeERwXF6d8/J1PmK1m1CFQrJY8+/Uzfn/2LX78688hgkOgVFrEEqQwRZ6HceTl87d8+q0nSJ2sJ+jAaEfCmJPpGYQZQtckDLFK4EMiUQSk8uSTiM47fNwh9Jge8qUijA4fAkYKZKaP3btA3+0o5BRJWtxkDBAHrN0RQkvEgYrJACEzpEi6x3fU4xAj3kVkfFeT8HS7HTiHkRqtMzxpSi+1IUqNVBppTAI2CpU+NsdOHwJ5TAtFUmJISonSBh+OgEv5zqedFqQEvUyqWRiIoWdz/4b/+hf/iVqPKLnCxfCe2pAcuSbZVpTEjp6rN6+53+b4/YHRj8wePWRdpsV5MAI9NEzLGjcxPHq04Nvf+xDpduRZGr4EJclnFVksUBhUNkH6DLxCC40Iqc7wrc8eE2P6uForgg+J8q8UWktCTLaPAJw/Bnm8/yT4hCT4eOxIDgQsXd9zdjhwc3vDdnPg7uqWD55c8PSTE6KAcZjStS1js+HkZMXpgxN2h46m8wiV/OHCQwyOoe3YbbZ864MVQgi2+5G277l885IiK1jPT8lmC1ZPzlClIQSPKEu+83/5fV798OeUY6BQHik7lHE4BKZIitKuG+h9i0ZSmTrVqWqI0TOdSeqpRjc904lkUknqQrKeVxTTiuvtHucifYwMThFCirKKGMCORD2SoSB6rB0ZrGQM2fHHQzA4jw8B5wb6YaAd7oiyQOQzJmaKiorDpqM7vOVRscC2Cl+kzZon4KTABsH1ZkvvPVpECJ7RR3SEzOT0dmR72DM4CyIeafIq/XyENIhKOCKBFALvHSbmKBExOhJDR6k00Qvs6PEeRgf71hF8qmwpbfDeE0MgynRKZmOA4MDnVIXGDxCkQquCqCHLMt7Rw/vRs93uEQqUcIyjR9Ylw3igqDKqssD7wGF/INNZUq+G8bddVv/p+p/sOviS3cEju5G5zygKSXCBF9fXZEalE+cQ+equA6ewfSB7u2eyfkJwjm10+JBxOyq2bzaI6ZoO6LzkarNlu9mgRIZzgTebHQ8fnLOcT6iETDDfEICAkoJhaOnHDpMVjM7CKNFZxr7rGO3IbDbj6u0ltu8py5xgHUoI1os1b15f8uzr1/yr/+VfoI3mP/yHP+PkdMlHnzzlwaOzRMvXnmk14ZtvnpPlNaas2WyTeeGr599Q5Ia6KDhs77i/vKHOBBfnDznc3/Ff/+uP+N73vstsOmMYPHlWMlmeMDcneD9i+5R8UNKgrUSqtN7hHd56+t0eH5JJSkrFvmnRJqftHLv7HV3fY+1Ilmm2k4qz9YLeHjg092Sloul/QbRw+eaW7WZP242YXNL1DfPphOuXb3n7+iX/6l//a6Q2FKsT2vFn3G13SFtw6Mbj8D7SRcFgM37xomOmMnrrue1GrpuBkRRlRwUikrvesBGSQ65wtmCIgl5oXG/ZOE/VDRxMz9v+jm/uPQ0FcfCIzZbT2YRPLh7w+PEKJVua5oayAmMKep/zwx9+wd/87a+JWrC+mFHPJFoPZEqgnQc3sFrN+ZN/+ScsLtZIJTl7+ojdtkffNDw9e8CPv3yFWcwQ2hByED6SSYWKIL1gOZ3zrY8/pdArvMvR0hBlRJiRrJJ4uWMcdkiRYHY6L/CDp73bMPQ9WVkwXc0QWmNkYCIiZ9byaG54dTkwiIoxpARZOCYAZEiAbHHki71j1vRRcHm74ztPT1BCpKpEdAQ3cnd9xaSa8Ff/5Wf8/a++Zn1e8af/8nsU5bFtqtSRoXDkUwkATwwOQkSGtPG2TYdrhzRsUWlPhc6BHFSGNNkRlp7SFIltJZNVgrSFSHurRM/6TUqUY0L1XfnUE+nxfk+IB/bdFc3+JZ98vOD58w6ESzXWwTPYAaEMSkVMTnqwjxaUR5mAMeKI1VL0nSMqiUYTLNzc3LIjpIMhEckzjTEGNyRAclHmFFXFfLlkvT4l05rgHXVZkGUabUwCs2uD0JKo055UqWMiNsjjmpsSU9kR0tp3AecF7eETjN2SuXv8sGfsWvq+wQ4th80dMQwUsxWrT77FMHS4bsD1AddlXG0PiGrOrCh4eHqGHkfWRqONxOaCWgeU6zAiUBcZtpdM5xXlJDvuXSxWRqKMWDy+axG5oD/0SJ1hBn8EoWuil5ycnvPJtwpms45xiEhtOD89425zixKR5WKGUYK2OSRV6cUT2m7Axki9nvDB/BRVTlhfSFSRU2QZVVUiFIQY6b1AZjM+/OR3qMoJRTVnUpfUZYUPoHWJtPDpZxqVw3ReoTPNdnPA2YCQCiU1MQqsc/TW0Q0Ds+mMPE91q74b0NIkmHceWK9qRut5+p0nrB8tEHjC6BmakR/8hz9nd99z8WBNXVZMphrvHc634EdicyBIjUbStj0helq7SwmdEHH7G04qQW5KlrPEBak1yNjxst0wqysens6Y14KiyLh48NvVQn/rIcPV119ye33N/5e9/2i2LEvTM7FnqS2Ovtq1e8iMzIjMrMoSKKhqmFV3m3FCmnXTetIDjshfwx9A45RDEmgY2Gigq7pRECWAkpmRGZEhPVyLK4/acikO1j7Xo2BNIjhDmeU2i3C/7udeP2Lttb7v/V6xt7ePyTLazlLVDRFBbjRlWSQzuBA4OztDC8lsNksRekogokRGg/WRl69e4nzSkG83G44O9wmu5/j4kP2DOdZtOTs9hSj55uEz3n77Ps+ePuf07Cz5I+SGxV7JvbcOGY9HvHjymvPXF/zd3/oJves43JtRbTWnV1e0q8iqS8WtHFjr45GG80DTeR4ve2LMOKss574iBo+PEaVTvvB6G+lbwdIHzqxFmy2n6wYnGqzvUKTF4oXk4cWGF+tIngmUTgW89zFFM3qLI6J0kSIQI7jQ4kJyME9Re54+BLyM9MFjvU27HJHZdMp0nHwmfAg0TUu1XZELKPZnFCqhrCUW7Xqm2nI40dw6nrA4yZkejMgO98iKnFEmiSoDqZBaM5aCARZNUZkkWrcg4NqGrq4heOq+pW1btj2o4gYyP0aaBXk+RassqQGEw+iGEJdIOoxJBkS//6/+DW2/YjxPVPB0qHgmk5L33nuHxfEh0UikEMzHM+7nIx5+9hXfe+ctfvHFQwyKKFKjCRCEIwgBXvH85Wuqqmc2zbCdpNpG2naGEQtinKPU6Fqzmqb5ASUCKgvki4gstjhWmAxUOSa6mqsnn+M6iy4m5Cf3iCZPh48A3zt6V5ObESKqgU7X47oNRQku2CQFQaZ1rxOLQaiUaR2cRYaAjBIVIt22wlZ1arKEQmY53mmkfsOAEFIn4CNKXCBFHQk5pEhIlFaEQa6UjoiAEGIwhUy+FskdOSSmA4OzfuyBCsGGujqj77aUMunqfPCDRjIlYCAixmiC9/R9YHl5TtaNWVCjtxWhXeBVRu96tqvAJIfxnmHx1l0+/PA+x2/dBd9ilEIIDSon8bwAKSiM2sWjEBFolV5zUpjEAUSBZFCZDxZRcaBJDikZUqR4rG8x1iMyAUzREFxLnjtCU1P5K1TeMD5sGOmXzG/cpN6u2b8NUHL7wQlaBoK54mghOYgpElVIAyhEkMSwINicLE+F1AE5URi+98N3wbeM5jk6E6xPv2ZyeIjOMspScHBjj7ORItiWUTlNOcuX5wilWOwdcPDgHqvTM84e14QYyHPP/vE+vb3E+8hsv2R2sk/36BnQIaXn/PQlWnqme/tUm45N5TnvevpeIWNECU+wjmbT0AVFjIZCq5Ti0DuEyIY1xTVolVJfAkYrttUG7aE8mNI3azbrC0ZuTbXWXJxF3LhE5IJc5xCr9L1BUdcNRiWQUIZEF+3ajt45+r7HDVFsXgSU3CX27D48gSDtCy4Eou0ock0gado7F9g2Fu98iursLW2fkjeEkNgu3QeZyRmVBYKA7XtQmrwsKfIkc2nqHiUCekhx8cFTNQ1Na9EmI4QUB5hpQcjVYBIMTZuiU+uuw/nArMwQv5JL/K29uliQFxlZZmhCoGsCUhRJ2+5BxowgAt45vIWug59+/ohHr9fsL/bIy4Llasu2aTk8PCAGS6kUoV1hQ0dvk39JBF7+7FM+/sufce/2Dd566xY/+tEHZMpT1Rukinjv2ds7YrF/QFYW6LxAZeY6n9wYwwcf/ZBERrNE10GItMsNZTbj4PAe2eSA1bbm1rs/YjKb4fMRy0YwzgO5BBM1pZ6AGrPtCx5fVLRW8RefXbE3n3LjKKfewHrt2S8V2w68NJgs0fIzYyjLkrptOb1cMT+YoZQmXywI1qFNGj5MZdK5K5OhlOZWCEith6FUJFiP7xwvHj9DIHA+Yq1FGclsNiHPBONZyXheYjJFPj9g+focbzXT6SFGS/IC7ty6CSHg9g9wnUMIRdv02OWGlL4BzjnyIqOcjbBdTVM5isUhj1YBjUTIjGDGuLKn7+0gT9GgDPOjW5jMQF5iSkk0LZW9ouk9XR/po+Ww6/D1FR+9vcedm+/yzju3OT5ZsBiNmOrAdGRp2xcYlWjxUk84P2v4n//1n9GKwMHtPfZOCozp0TKiY0SLiFCSf/iP/h43bt9AZgpkOiPf+eEH/MUf/hk//N5bfPPkgvOrLeOjI9QkJ9qeZFcsuL1/zAf33+NwfkCZj8lVhtSgTMdonuFY07ZLMh0x+QidTzh99oJP/8OfMisUo1GKsmSagR4jlMYUkWlouHekeLRSvKoCvZcQVfJhG9LC4jB8lMlSCycSm+FytWZbN+yPUzIX0bPdXOCtZbuR/P7v/ykxen7jN97n3r1jhOoHcalOiRJSJx+qGJK5qU+GmCIEfGexbQ9RIHVih6bHlyAMQiezyOADUqnkbTWwF5TYmUiKwUcrxYcnJoNCvDFhYGdSae2GEDa4fs3p84fcvnPMndu3GE8KHn79DGctVni29Zqo9vChI4QeKTXBh0Hy45HSEmKP7QztpqOTPa6vcSoicsfBPOPgcMTefMbh0TGHR0dobRiNxxRljtYGaZIBO6TPICWcpTUUh9dw7Tkx1EtCiOvaMASPJyCkIA+G8UxAEMQwgrCPFHcTUB86Yl/jmxVf/fIXLJfnNGheXzTUXQ0uomXOuuqxUvH1syeMLwSH5YiP7j1gFCWxrcgaMH2Ddy2Xl69YnMyYLwre/uAB9753i66+INcDa1gGMJGq2pAFw9mLF0z3Don9KvnYiQgiozD70HumeUYvU72rfEN79YLjwwWlECgRyOeSrNQIXXK1DrgQ0ZlmeXmJdysK5VlMYW+WUWRpgNW0gW0LQRiiklSrNc5teP+9X2M6nVDVFdVmS3CWvnnOB+98j3KcvHgKNDZEfNxJbiA4RbXt8Ntzfvj2fYyC7TZyedlhXcvF5TkmMxzt36OYHHJ0d0F+MAEJzWqLbzfc+eAupYlMdc7IZGjpQESauh+wtYzWWprWUpqAlBIjOow2eNuTzTNC8IzHGXnmiALUTLPeBqLdsj8/5MPvv8ONoxyjJM9fr7/TmfqdQYaf/Jf/Ja7tCdYhtcZ7sM4N2rqkWwvOEvqOm5stkYgSIhV0IZkzbjcNvmogRnxwCARVs2G81YCj7wuqGmBE29QYbRiVivXVFevLC9r1mtnRIePCIF1HbDv6qGgbT1NbJDnBpwzpGD1kgsn+Pn1jCZ3H9o6qbVAyg3xE1zlWvQelCGg6JxPLKkaMENhe0AlD0El33baKUij0ZIzwNulwh1zSKAS11LQ6aeFDH3F9iorq+6QvhIhW/WCCK5ImPoo0ufOB3jq89+gsI4hIJICM3Lp5woP7b7G+vOLi7Jxld4VzAeF7hHIUYYvoVizynHu3D3nrwU3u3Tni+GSf2f6cfJKjtXgTZyglUeTJS0Am45rk6PumuI8knWjmPeNBaxZsi+trLl5fsToPOKaM8ilGlSipkcGhsx6hamx3RZkLpBJ88dVjfv7LL9CFoAuWUmfEYNEC3nnrAW+/+zayyAgqbewiwt7tEx4Ewdc//yU/eOctfvn1U7yLdEN2uY8OSSRHsVz2XJ22yDbj8tzRbEpknCH1FB8LxGAiFSUgkyOvVJ5soVGTDu+3qEKgSoXvKs6++ilZtyaLJBfew5tEXYBKsUbZyGLbllylJlzg8XYL1ITg0FoPer4MKb+t90vAU3Q9MoZ0NjmPb1uEDynCyJTptlRy8J9QRKEBTYgKKUxC8YVODv9CIgeGR6JaiMGeYfdZ7rwYGIwmxeDHQHJhlj3RV0jZcHQ85vf+q7/LL/78zxDRYzuLtYFSKKTyuODJTEkIGusCJydH7MWMWaaoqw19qOhQ7N845s6P7vJ7+yeUiwlZoZKxpgSl95IBV9TpoGNwZJYpElQOfi9v2BkMGpD4RumRfJhTcZrCW4ZXmg4Az+BuOSANUsYB6PSEbk199pj29ROyZomJnkWpiLbDvXiCtw1KJ9DG9TXW9chMMB6Paas6OREXkxRn5BL7RxLoq4DUkslkTIiRXMaE8C8jXfQE6XDbEi8CwmvERpF1WySeplnRNFvwHkHk7Nlzrl69QguBCA6tJX19xevHF3jborRhdfmSzfIM60DLQDasgeg83jqkNGybQGM12ozQoSfX6WCxvUeODZlW4Dq0d5S6ZDIa0XRtUp1K8DE15FKkQi7PM1wMrNc1QsNmVdHFli4AqJS24iO2s8QBHCWGZMQl9TUrIfhA23a4GIbI2PQZChKiviuMuP4RHhdiYqgEQdQRmWmUMWw2FW3b4X1ARDEQRxN9Nbq0b00mY5RSjPOCUWFou5blakW9rXBNhwgeJSDPddqDg6cYFYlxFwK2rtBCMsnHZFliDxXlJIFeAjrb40NB72C1rhmP//8IbfrV9Z/VtWwisW4QogWRzhgtQQvIjWTcgTYyLesIVechJHaj9ZdMpiO0Fhzvl+iwwtUrghTI3mOdxzrHeJwagfF8Rt81XFZbum+e8Pnjb5iNNbbr0FIzLkaUZYkx30DwFJlh73CPs/MzfvHJZxwfH2KMpmtq9hdzJuMRSioWiyOqOlJZxc+evGLbWhrbs+ggXzZMSsmdkzm3jxY4OUXlgpdXFa9XK9ZxQh8zTt76CZlRbHxH5T2MND4TnG0079x9hx//nRFN1bFenvPi/IpNtSQIy8nNfUS0KAGu7SjyLKVZVA0xCozRjIqS7XqLyTKUSR5XSIP1ge224fnzZ4wnU2bzOePZiPG4wJhBtkdgNJ6g51PixTnj+YiRhTwv0JlitLiJ0oZ1+5J5nNDHKcoU+FCCKNg0FeeXG4ppStip6g2dbVhWPW2U1JsGk2dMZzPquiKGiNIaFSTVOsl8TWEo8owwSADwCRiVSmBU5MMHN/hv/w+/x2KqyEYCJVP6WF9X4CpCXNF1F8ymBiELbMz51//mT3h+umJ6vMfJgyPysQPfI+JuOu/58Y8/4r333sNk6ewPSiHQmFnG93/nJ/y7/+kP+W/+97/H//X/9o95dfmUG2/dJTcCoyR3D4753t13ON47oTA5xuTIDHQemO7n2HCFdVuKTJHpAq3nPPzkIf/qn/0Lvn9/n1Hp2S8zYnuFrycooaAYIYRkNJFMm4rvH2U02xVnTLBCDQMAkix2OLwTkzJ56AipaLuOy8sV++MxMvYQWurtmtlkwb/6X/+SVy8vODzI+S/+0W9h8oHBIOUgkTCpUSbirSV4jwoRGZNMwjVtAgaFQcqBSYoBkQBoRDIClDpLMiEl0SL5RjB4hUSlhnjYnVRiMH28rkUCRIf3W5xfkxWBh59/yq2bx8wmM0Aynk7pe8vjh08IwrHtHEHO6GxNCC1Kpvo8xgSyEBLrp2satHAUouPm0THvvHeXt9+6wa2be0ynRfI10jli6D+CkNfPLw6OawwRpuwibofnLYfoWwaT8CCStFaJ9EkFlZgdgbjzvUxmqUEkHwyhkTiwkRg2XL1+xcWzUzqriCbj7HTF+ToxXHNj0FJy69Y9llevUGxRtqG5PE+eNEJSN5rcB4xMHn8u9JgMTAGb9oJPP/kPfPD993C2RypB71vGszFkYxSwfH3K6eszynHJzbt3UxiAaBnFhsyoVBrHiLIX/ODOnLZdMVEOPVJQZkzvTIiZYeY1kjTU21QjtJQoF2jPL2hfnDHKR2y3LdYGprJgXOa4XNPmCk+GzBrygzFNWFIWnjzC+yfHqGyFzTSxt0xuKERI7OQoEqATvecwGk7uHBHUc5xWjGaW0Z7BR8FhO8X5nk48ZzI9YVuf0Yots5MDTAFHdw6ZzaeoZkN7dk6eCRZ7+2yuruhJspP9kwXTu3dZvTrl5VdfIQkYCYc3jlFGcfrkEc5FppPEkKrWK548PoPYQuhZry74+vNP2JxrxqMR2+5Nct3/r+s7V0NPfvkJXdejpcbkBT7EFNOkU7xLnmXkeU5WlKBzitGYvMhTET40jkpIXnzzGFkUxBgxmeaD8D3KTDMZ55SloRiXFGXJrVt3MMag8hLfebaXS5qqRmtJUUiKiaHYnxOiZnZwxvnJFVaUxMxzdnGJKG4gjeT1+SlVMASpCTKZF67bLc9ev8DaRCWPJAmX9cPXAmxwoMFITcxTDGeIsG1riJEA+Bix1tP1NsVMknSEyIgkmZRZ63B+iKEUCotHD5p6Kd7Q/1EKoyQmdfxoKdDIJN9wnofffE1b1YTegYvEoJDRY33NXgH/p//+/8j99+8z3jtE5J4oHDFaVBBDvNWb/OB0qUFXNhT110/kW5unkMlwMCbkGBMRsksmPrIl0zk6y5BKJ9BXOZSqcf0VwXWI0ZhN3fHv/vjfk40iaJOaYhIgdXSyz/c/+B55USYAIJkEDNR+wcH9m0StePrZNzy4e5uPP3+ID3Kg4YWkOSQniyXPv6ioJzlaTdFqglIFUiW9XoBh4i8QyiOUp1gI9LghuhX5KLnSt8sLHv3Fn5HFmnKRJEHrekW7uiA/HA3gTKKzt3WDd1t0Joi2o+/XCJm8AhYH+8Nr0AkMGMyCYgzgeuLAEFBEurrBt30KTR1SJELUSV+ITG2ySNIjuTOQVGkCFEIyvbkGEUiTfJ9m98NhExh8h9OfSJnAKwIx1oRYIWRLDC0mz7j/7tsczCacPXmKMgopE51VihHbTpMzpn4d+Oabh7im4faDG/zu3/stAhFTFIznc3KTg8yJUhFVQp/TQa0Ig8NvHGiH12DbsPzCcMiJ4UCPYjDWHB6xIycOb8u3/u5v/vnwQ5AxEvCIvqVZPufpZz/Db5ccjkdDTCaUWrJdV5z//GdgVJpwRIF3luBtSh/Z26frHE3V4MwSAdi+RylFCA7re1SuEbMpaniW6/WSIAKLezc5eu8+5vYxIXogw51XnD57TaYN2oGSGbGu0CL5MORac3hyTNs1TPanSBnZLi+ZjE+4vFwxnc9p2xapSogbXjy/GHK2JUZGplqzV2SErECXEyaFYmFaRrnCWof1gTLPeP/+fe6+f4O//uIls8PbPHr2lNeXp2iT0/hAXdWJpSQceTFGq4LX5+fkeYZWhlExIy9GyahOpM9zmIWghsmhHHx8dp9gHECwZLo4rM0AXnpkHECCXVk6DF0ggWQuRjrbI1RAekXyu1JJThPl9QqJzqdJny7wFkLXs24dzEYUeYYRhrapQAfGRc6ozBFEOutYrtZMImR5QWY8TVMxnc842JujYmC1PKepOuaLRdqzgqbvu/Q8jYLsVyDD39arcUDcZd4DhJRCIARFD3lR0rUtm2qdJrI2gu/JvWU2G6Xztunpu0hBQLoeJfUgw0y1ANIRZMSMBWpUIKTAKUXfBZ5+s2F1sebwYI9cd9y/lfOPfvc3uXl8hNGglKKzDb/+49/Bdi2fffopL+tXvHpxSd0/IwhFEI9og2Rd9Vws1wQlKKczyvIVAs24mPJXnwwx3HnJLz95yGrT8fa7H7B/8zaqnPBXP/sF1jl8b3FNjfKWsRIsW80//1d/xevXz/l7f+c38V4TZyd88IOP+K3f/gmz+YisUHjbEYJL7Y532LbDO090ltB11OtN8iUiEkNks9nSd5a2rrm8umC5WSX2p53SbAUIy8XlK0R03L19m83yr3j67JTTiy1Cl0hZUtWWopxRlDMePX6FUoFf/8lP2D88YrwfePxizbpyRJWn2iamqGWZaYoQkMZQkprK3ruB/ZdOS0IywZ7v7SNEknFJoRBEpBbDwMljbc2v/eQH3L4zh7AFmR7re0d0HUZ2bK9eo6VDmAR8/OKTR/zhH/0V5WzBwZ1DJnsSfIMIDolDC8+D+7f53d/9exRlDkoSlCBqNdD/FYtbJT/8nd/gy08e89/9t/87/u//j3/Kq0eveffeTd59+w7ff/A2++MFZT7CmBRfrvPI9Likqp8iRMNonKFkju8Nf/Fvf8a//Cd/gOjXvHdYkO9PqLcrxipQXZwzVSPQJcIkwH8+FRx3S24tcjavJb0cY4UkEFCRwRBxR5iNOCJBCPooefnqirdOSjLlaaoVCkFbB/70j35GpgIffniPBw+OQDUphltpotLp7I8x6YmtG0wCAe+xTYvvHUIkMJqYao/k4aATW0XIARySQ408nEvfKoRjjIQoku7/OrI8/T+NMyw+bOncJUUZ+OzTv+TwcJ+Dw6MUIS41h+WU3/1H/4B/sfkDXjx/Sd10dJ3HuZSq4JzBZGMiJSGOaW1BjAUik/yf/y//DfsnJ0wO9pJsQoyGmhJECCAEDga5aOop4uDSkGp6NYAi4prlGRBJtjvIPogMwHzC+3cASpoTxGFIkA7hoNJQRWDpN6+pz77h6sUT1qc1EkGmZjx/Gfj6xYYrp/C6wHdbcnreubvH/uIm3dVzFnnGSGiirQhRkMuc/dkeo75murfHeOIZjUCVmr07h/x4/zcoNPjgCdajmKFyhTI5J7fvIPKc2+++BUKgYwQv2Zxe4ldnKC9p6obgLbPJhKZJLPR1Z4gykI1zMl/jlaAPnixPyRiFMvR1DUTa1xesT88JxZS+9WS6QJaB1fo1SiqOxwWd79HNa9zpliJayNMeUwiH7WtGs31UriiLEeurNUpohDap1g2pXpGzjOA8Qkbspmb/5IS+t4Q4I4qIHBV4EVhdvsRdSaqr0xRla0G0Duo1Jvb4DjaXjr7pkwQvWOqrU+pqSWJy10gh0Vpz+foLMpNh2w1ZXuDsluXrx7jOEkOHio5RrhHB0VQbGpORoZFy/J3O1O8eYbldsVmuUCh0ltO1HevVGiHAe0tuMrIiR0rF06fPmM8XzBdzmqYmy7OhyZJ0Dv7ypz+lbTv29xYcHe6zv5hydenRJk2y2rojIlnM55yfn3Nxecl229LbwKicIGQyDRsvZmTjCRfna9q656MPf4hSkfc/uMfzs5bnV1vWzkI+RuUZiCTfijHw7MULhAAbPK5zCQUVQx6uiIOcweGjS7EwO6PAGMmzDKlS7FLwbhi2prQGERPAYLIM71PM0iCcJ5KMzHz0KCRGJKQwUcp2m0L694MY9IpCUlVNyv0dEE7ibuIncEhGuebdH35AOR/hRcQLUqpBzIbNfSi9pSdNCdMWmYAE/iNw4W98MRT6AaKFGGmqhq4LKF2SFSN0ZobkII9SLUpu6Zol5SjR2X/+i19yvlyh8xFRxgHdDoxzw/d/8D4HR3sJZRUaYtrspTKgJUJH9u7dpe49Ds2tqw3fPH+V9FNBMS4m3Dm8xe3D28xGC6QsB3DBoJRJ4IdM2bFKKlARYQJm0iPyjs42jEYagmPz8iVPfvZniGbNaDGi2zSMD48womN7fkoxPwZZEoVI+lJlabtzjAoEVxNcxaa6Yj4dJ+YE6YBJ8G9CwWNMztvCg0IRvcP3HYKIUjKxQZTG2QQmKDWwGIbXwYBMiwEAkonXnpyQxZB0keYE1wdm3LEWBIQQBlDNAT2RBiG79NkqgZCGaAOT2SHczTi96LGy5/UysK46XEyJA5mxHJ3c58HdY6Z7UybjKRYIWiOUwQuFEHEYCKSiTQwTgnR3DAckSd6xW3JDe5h+I7l+NTtjsp1kQgzUy3T4fRt2YJCKpN8pEZC+B7elXZ6yfPYNl4++ZpYZNlVLcMP90EX6Nk2jhAdhSEWlzPFOEGxgc9YQPMSoCXFwR7dyAHISMq1lBq2itj2zgwMMlt62tBcVl/oZ8uKUrqvAS2QlYdWh9AwXJMgchCNGMCaZdV2dvaYPPV70mCxjW1t6u6XrHf35ZVpToadvLaMiPVcTJYXKOZqUvL0YMdo6tG7JpCSjR1pF1UQ2bUkvAqcXV/z06y9o4ojzqy1eSfIiR2UZMlOUyuB88ibpCWhpybJI6Gu0kcm1OhsyxXEY7ThY5BxXE2IdaWMyHct0fj1RCc4TRMR6hyekaVQUiX0gYqJpIgcTKU0CxIapjAjJNDRIMqPxIRWBO1AqyStS4+J8jyDSD4ZkmdLUdcd6vcX1FqLAaJOijKVASfBRUaoJ9bbD2RQ1luIze1zfooxiXCYmS1fbtE9JSVnkdK6lbipmIfuux+qvrv/MLt+nWK4QExArRJoEaqkYl1PG2RghUjrK+fkVBIGPgsZC2PbUTYf0PdMiY382JpeaLgbapkdIMCaVXEoIjM7ovaOqG4SB3mmqPmfrSrpXFZqGzeoK21fcv3uD+XyCyjQhRibjKZvNkotqgzcKH3Jq61hVHS4mGm/f+yS1QyCjJPqIj4FNa5lM9xDjObU07L8z5oYuODw6IS9HBCF48M67dM7SNjX1ZkuwFo3gKhSI4piwMPzV0zWZlrR1xafPl/z5Zy+YjQwHs5K96YijvTEHB1OU9HjfkZlE6daZZnq0jzQGkxfJa6gP+Ch48fgZISsQQjAqCkZlQZELpHS89eAWJhuMzKZHvP1yyenrNevGsWkCr8439E4TzZjpnX2Wmy1fXAr0ds3oXLANBWY0TzHbmSEScT7Q9xYfHH3bJWq9FNgBBCUKgoAQ/VCUp/3Mup7e9miZ3tcEOnikrFnsG6AbpuxAiLi+g9Bh3RohesqyQIiS9Rb+x3/+r2n6jvmtfeb7Bcht8rMQERPgYD7lH/6Dv8dsMUtA5hBtHZUCrRFRILTg/q9/wLJuyOQ+P/refZ4+ec29gwM+fPAue9MZo6zEZAapI1kZKfdz6u1LlLbkxQghC9Zb+JN//VN+/5/9O3Ro+Oj9faZ7I8xwTNu6I4YVUp8xNgmsEVJQZJpJVnFv33B6XtFZjZMDWzZybQJ4fUWBiwErFOu6o7OeIga6bYsm40/+6GdcnV1x8+aE3/rtHyLzYVAhUxF//fpjJARP8A4ZEgvAti2uczBIVZFJo4/IUhKLyhKbgcRmEFIm4DvRK94kgYkkzJAyyTejHJh0MLTqjhi2tPUZxUTw5NHnZLlmcbg/gCDyGrSYTab8w//i7/IH//wPWF60NMsGO03JBc5DVi5A7+HCPjCnKPe4MZshkMk7QQ5x6NKmWRyKoFK7v8NapAiwY4/AtxitqSYUMdWKCcLfxXsOj71+FLtCLDWHQlyTOGQckj98RV9dcPXyES8efsbVy9fkjNMQLTpmE8XN40jRafLJhPVS0qw9Xd+yf7hACajrDWZxTOktXin6VuLWV2hpmeSeXHvuP7iDzhRqnDM9vAW2QQuBvViCdRhTIKTCSg8S8r05seu5fPoM4SV0DvqW0pTE6LDOkouI1KClZv/whM52bJcXtK+XdNZSNy1FXqLkEDHvApnUbC4uKfKCxWzKJqzQOjLdHxGqK1zoOZpMWK0sy6++ZDvEfyffNIW3CWDu5TNGozFqf5/ly1e4kGQ6SimiHwaPQtC1DVJAOR6zXXd0dY2IgaataH3P+PiQ/GDB7OQI5y0A+XyE3Z5TaInXGiMlrm6ILnnKCW+5ePaUYjJm7/iA+XxEVDCeTOiqConE5HOcdeR5DrZFSsXebELdNBxMx6ANuVIoaZBopPhudc53BhnuffQbRO9RMSKlwfuAtQmpRkSitYPBquP2vXeoNmumk0kqIqUcpmdwdbWmLKcomTOdLcjyEUopxPDBOOsxWZE0r1JRjkZMvQdZsK17ZDais5LzTcs3Vy3kqWhuGk9bVhDh66tndGiaWCDzDF1ohIh0XUvXJY1WCEmO4ENIRW8Iaf+SqQly3uGCQyiBMcU1dVtKgZQhmcmFRDFKebwDpVtK2EXKDJRhEX1q7AdgICLwItDF/lqzNoz30pm0M2KMEREV3rkBbRRD7vEA4EoISqd/y/fEaNLgz8cBgUyO69fbTkw/fejy/r9+1uJb/3/Ttglc51gv13gXyUyBNvmAAgeU6Mjymr45w5hAZgpOz6/47MuH5KOcoCQ+BGT0aBm5c/sGb719H2mGmCCVjK9QijhkFUclUFnk5vvvYPIR67rnxasLvJPsjfd4cPdtbhzcpFAFmSlRKkcbc62vE0OTr5ROkgLTkc9B5C0uVhRFMoW7evWSJx//OYVbsZgacinRKmN9uUzGQl1Nu15S7I1AZwghKEaG6rIiuJy2WRHoWF0tuXHjZEh30CmTWyni4Jvg+xZvPWZYf7bpcNahpUYLmQ5AoQgko0WpVIq9GfSBUg6RSQN9T8jBLGw4iNgxH+IAR/zHrACVYjOlcMS4JcYViBZw7PwOghSgDNn4kMnBPb76+lMuzlfkoylvv3uPe3fvstibUuZm8EwIeBERyiClASHT5yd1eg92BpXyW6adwM5Y79srTUgxPPf4LcZC5Np7YXfgXd8uye/l27VLQlMiKgRkqIn9hu3rxzz69GdUl5csigIjJXvzGfV6S4yByXyKtS7pIl2qY2KIeOHSPhQHnCjI1OD2DqlIZrZx2AeQSJ+iDKP19Os1vuuAwHiyx/zWXcK0hEcPkS6gZMYlDRcbz2Zr0VnG1drSdTVlrrh9Y4HJPQ7YLtcIk7NcV2yvlsxmEw4PF2RKo7Mcu/Zs2p6m7nERvMoxKvC9m4fc6wP7R0fce+cuNnQ8e/KQEHpiVnK2bclncw5OYNUL1q2jdWkO0rQtoYOoFELLFJMrNUZBOdEIJ1DRoaVFKYV1nqg0Wa6YjwV7eU823mPbS5w0CJ0RiPTO4RiSVwaKcZTJs0f4BDBJkgt5SpuAMOyNeVngY6J+exfwWqQ87Dh8fxTEEBJ/ZzCV832H0RqtFD5C16dUF6k0uckxJrHUtFZIIq5PLI+ud8Qo0ZliPJni+47NtqYzyVclakUfHaHzSKXIjEKalEaxuqr/k+fpr67/PC8Xrnl9pM1IokTi5fQ2Um06tBGsNz2QUqF655Ltm/VkUlBIzSgb0dkhEjoErIdRliUfAplRNR2b9RXWOSbzCTE6dDbGi57eeowO7E9H3DkeY2LP6YvHvHpmaWxH33dM5wusc1jrkrkpij4IXl5WrCtAF5QmY7bYR0mwroPYEaWhbTes2oDeeKIecXp2yWg0ppgfYXXSI3udcuCd1NSDvElGGIcEHL/edMxURhYkqwqMzjGLI2wvaJeR84uKzaXDN4H5BDarUxCePMtQIsVL+uCRUpLlOReXW6IoWFU9L08v0FqzN5swG+WMS0meRfJCMhobxtMpTdfStB2rquXVVcNl7Vk3EacktnNc1patVdheUGY52XhBI56y3G7wAsrxCJNlaKPIy5xqa6nqGoGkyBK7xPsUd5uatbSnNE3NqMgTLuocUUq8T/WjwFNMJQf7C1KkdZJree8JtkVi6ZpkTq6NwceCP/rjn/L5l8+QRc7R8Yw8S7IaYsAgmI1yfvs3f8KdO7eTBFOSJmVSI4UZfKoCyIAQ8KPf+QmXjy/5R3/3R5zeu+L7H3zEfL5glJcYrZFZxJSRYqbo2lNM4cjyCciC07OWf/HPf8q//H//OzQdH75/wHRvDDIQvWAyGbGpG7p2jY8aaUrKPYEoxygpOZiXNK3nwZFi+3yLDZJe6utp+HWdO/ziY0ok2HaeTdWTZxbfR1aXFX/27z8hywJvvX2b7334FlE4UkJVqqeE0EghCd4lwDg4VAgE67CNhShBq/T4mDyZtCxAZESyARwZhk9CDhKMlCoh5BsT7etroFQOokRSXGWN7S5RquLy/BzbbXnrwdsoNfg9DIMlYkCYyO07N/j7/+C3+cP/6Y9otwHb5thR8ll7+727lJMjVD4jqgIpihRhLpKsQQYxACPDwEYk6YIfnp4c6iIkA/Dwpp9g9/U1CJ/Wc7JZJoEn1w+N1zUWYWDERiA6RKjArqjXz1idPqdb1ly9uqTa9IRM0/UOR41UhoOFYdwYsrzk7eM7vL5cc3F1xTdPXoKI3Ltzj08uN6iuZjIes2o9TV8zMZY7BxnaV7x6/pT51HF8t6SqX2EySdt0qF2ShgClNU1bo71lvbqiNHkyKJUZeM92u6EVDb5LRpnLeoNra0IIxBfJBL9rU6pFiInRrkronKN3HTJKTJ7j2h7XO6SAvmvRSuFsh28Tg2N15onOkQeRvEd8qrOVEORC0rYeoSRNs6Fb1kTnMUqhlMQHiwgRrVKMq3EDyK09m/M1wYckbe3SZx62PU435HOLaytUVLjKp6jLNiCjRstk2mq1S6EBEkblhMxkuLqj6StGiymj2RzvHKF3lOMRXdMlmTaKTKfaXUmP7TcU2RyjJUZpjNJo/d3gg+8MMvzP/+LPwdXcOJyxt5gTXaBqKqRS5IXBmBSPRITLqxVdUxGFYP/4CJllFD4iZYbzijt33qJzkelsTJEJpqVE60CeabQ2mKzEOY8ZtO3WeTabhqtVS1V71jX0ZY9wEqcKbN3SbbdcuhxrHfXKM5qURFPS2h6xTchQ2zf4wfH4unGJQyziDrmSCZkNLhmUFZlJG1FMTY0U4H24jh8kxoFVkO7G5KGY2ApJai7QShEE1xRMKQXee/pEcxjo6GkrCDEQJEQRUVIlSvjg9JQcbYdmS0IUgSiGBSnCbjdggI7TjnP9ZyRWw25SPGw+/1vXtZaOgco84Cdt3dNseyTZoNvTpFmuReuGEC9xfslkPKK1lo8/+ZyqbdCZofcWokWLwGwy4gcffcB4Ok595i6eUWaJRi9VotqLBNCYkeHkwW1wgrMXSzaXDfduv83+4phSlGRZmVBNZVA6AQpC7IALASogCkcxd4h8DcIyysaEEHn81SM+/qM/Jbfn3D1JJlW5ScZWV69fY4oRKhq61SWmnCNLBUqTZZrOeLr6EikC569P0cqgdZ7YB1IPzsfpsBFREPoe4ZP7MT7guz4BVEImJscQmZfkBQppMmJQCG2GTyTR4fywroRUg38BCCHfTA2um/S0ttMhO6zPGCBUOHtFjBuyXFyj7VFEvFKIALIw3H3wgNXllvv3IzdvHDOfj8kKjTQeJXfSFjWg/oN/BOm1RKGGe0kOEo0d2LFbY99agjHJOhJwMhyIO58Fke5LIcSbNTmwGsRwf4i4g8HSI6TwCF/Trl/y+ukXPPzFz7CbLXuLPZCG3qWC0vYdEqjXG6L3RJcKCxcG0FEmZ3nisLlHEKR0ENvbtDZJ8Ypi0G54F3E24BpLtGne0W9b6rMl0lvMaEZUsL60PLryPHy+5nwZiLKncsn3IleOm/WGsujpw5reVzg8m+0WnGecZ9y4OuDoYJ9xPmK5rrnoenyUGJUTth2uqdHjEWVmicJxuboi4ClmC3rnWDYdV9uO2eEJey2IxoJ25E5iYxoCND5gY6BvuhTdKCXlfM4kM5g8JB+H3PDidM1laGlOZtyOmuO9OQevt4yUJBew7jq61qaCKAak8+QSdG5wMQzrVqQtACi15nA2YZzpoZgQZEWGyQtOry558uoVTWtpXUx7YxzYYMNUi5Cyp5OfScQNrJk80yitBlAoEIh474hK45zDSInJcmzbobQiyw1CJ1lcH8B1jqrpMCZpshF+kL94lNaURUnTdji3K/1+df1tu9w1DDqc1SIV7b2PrJuOrrcIGVIKUwCtkueOdQ4QOBsISqIqR90lqnumkgQq6IJcGaqtZbXcUm22SBkQWUExKrm8vEA4y/5Ycvton8OZptAOo0XqK7OMrNQ4n9F2NavlmvF4SgyC1jp0ljOazNh0Na21SGloOg/BEn1HmQXKcQG9p+0b5qN9dF5Sjsb01vL6/JzJ3FOORrx6fUbbt3jb01Y13juyoQGzztIFj5cKJw1eFkSpqWKOykbocckkk/Shoo4jbh6fsH/vHZBp2iwJ9F0Lu/MpRKZHkboRPPqLT/jmZc3+YsF0NsGhsCEgXI/ofZps1jXNes1mGzhbbnjyqmHZZzQxo4+eIAV91HRSIPMxOhulabaQmDxjMZ1Qjkq0NujcAAwx66m5d8Ilij2Sw8NjxpNEn47e87OPP2a7XiMIKc59OK/S6eyYjieUeWJfpeY64tuOYBuCXxJjhckzUAWPn5zzB//rn+CFYf9oj+k0x8Yt0vdI5xhpyXsPHvDB+++iswTg7wAGIXXyCxjMFUUMyAhSKPb35nzvnXt8+Pb3kTI1qloPDIaxxIwDtrtEKkteJtnDi5cN//R/+FN+/1/+GX1vOVyMqB1sakvTG1whiUFRmJLldktfbamWF5gixZIKE8hyw+FM0hxIzjeeblUR4gg/DD92dYYYYDwfBE4o2t5zflFRzBzBKT7+2UNOz07ZPyr4wQ/fYTLPQVgQb8AV5AAqW5c8roZIcNu0aSCjdGJmCkMIEq1ylMyJGILQg8Q0yV/FIM9leI5vvBd2EuM3z1sMQ0MRO7puSVtf0PdXrNcX3L9zFy2zlNygkidFCGJI3YtIJXjn/beotxUXL55QTPcZTw/I8zFSTUBNQJawA0OEGgZMOwBE/g3mZ/IxSsOl3RpMXlYDQLJbm/LNoOmNvnTXG6TXt6u3vj1cTCXjwCB0G1x7Rl+95uzFQ55+/ZDlqxVdrSjHM1oUzy4vCEIwzifMy5JCKzbVmk++ekYdMmRWcnFVIYzg+eopTbthMTK8dSOHEMgLw71FyTRzvPvgLb749BO+/vQrPvzhfeazA0wuGZWOYC14jzGJrTweTVBSk2cjcI6joxvgk5fDYjFnPJmxfP0aQuTmhx9w9eQR2+Wa2WJB1zZE51ks9ulbS11VzGcLmmqD7y1ZljOdLHCNxUxGTPb2WJ+fYpRiMhnTtw1EmM9nNJsK16fhcnrXIESLlgrc0H4xyKLTRC/1J27YJ5xLH8MwbDFCoXVO7y1aZnShw1uL0gXFeIrOSpqXp7jGIvqIsoK2ERByqk4RUHgv2VRbhIyMRwVClTgfqXpBs6zYtE+o6i0CyWQywwiNCEniZGKkarvhrHP4YGn6ls6lFLJkgvmfvr4zyPDsQuKdog0CHxSLMkMFCzi6qsGpHtf2eOe4ODtLRiy9wzYt1lmc9YRoeLW0/PVnL6hD5GhvzPEiY2I8uQ6Mx2mypLVGKYPWBuccvQucLzsev9yw3EJtFcvWY9EEnbFtWpquR5iSCNze20sovI8JUNghiiE1DYFkOhnj0JiLnTZcDIVqQsiMEEihkm54J14iNfq7Pj6GQIwBIWTSYg9gAWEnS4iDJlle/xtCq+RK7gJykGfsbmwpBKOypLddcoeNnjjQicUAbAiSHj+E9LM7Zwedfxi0739zs0jfsHv+YUdyuH6UQPzNrk/spjnxGrOwXaRaNfgOMl1gVIGUGiU8SrYovWGzfsFoJEEJnj5/xcNnz+hJ3gPpEPQURvLh97/H3bt3UvMp5TVzITXmSWsnBkOdGD34gESwv9jjR9//Ps3SU5gJkhyti8Q00RlCJdMYMbzfiQ1nUWXETD0ir4E1WTbBOcnDr57wx3/wHzh/+pJbe5r9acnefIxHI5GMpguquqEwDl9vcNUao0t2m3pZjGiqJVpLHj/8ht/8nd8kquQZgUxxSVHotF5sT+x7lE8mUN5aQnBIrRAoosgQKicEgTKJ1RBJr0UIQQi79Zkm9ztwwZOArRh3Orz4rUMywQ0p6UJAcAjR4+wa2y3Ji5Q1LYQiRpKGVqT3TkpBWUjeeXBCtD25EWTaD7YZOpkoSZMO/AFUSYaUyUCJHQghhvtFXJ9ivEHWd+vzDUD2ZoooUmEXwgACyvQaB0NAOagLY+D6nhMEZPDEfkN1+YIvf/FnfPnpz5mXJft7xxwc7BN6T1VvsUJhhcYMExUXBV4IFGoA8+WQdJGKjmE+nu537wnBpxhWIQkhATy5MqisIDQtyBzve1yMOK/wUWFMiS5y+lpw9vyKXzyteHoZqN2IPiqcUDg0ou95+qyiyBtkVoNokSogokQJxcoGXr14TXl1Sa4MImg8hhAN23WLCYFu26B39/aqpnl6mp5LjKy2G0IInJycsHxyigiCm4tDclnRWM9q26GATOf0wMZDDJ5Cat66/YAbB3sUKiW01HXD06cvaDYd5/WKx8sElHgzIkYoc43WqUmr+z7JG4QgGIMXEhsjbXBY5zBSYITgcFzy43fvczIbkcmEwmdFATLjl9/A1elTMiPxUmKVorOe3gc8Kd6KgU66y2Zn0NVa71OqafBIETBaUNuezrUUIUsGvhH6mOK7VOyY5DmF0njfAlA3HYFIXmRpnzEaoTxdaMiUICsFsfsVyPC39QrDGbwDRdMAU+A8eOdobTJFTaAWRNcThcTFSIwBGSMuCGrXo5QgU5Iiz/BWsblsKXKLFpHgIqLIGJWKtm+YTkeY0DMdCxY35uQ6QqyJQlFZh/KCUZmnWspb2r7Dx4jOkj8W0ZEZzfHBghANpxcbuq6lb7eMRjnTSUmInq5rUMJglEArgdICbTRKG3ReILShD5AVY6TOsG1DcBHvehSJCSdkSkDqnCfg2DYNXWeJSPrZnM1qTbA1mfB8+Vzy1w+fMhkZlEoTuTLPkvRIQaYFhwcHSJnTdHDRaloxpWGC1zPMJCPPLJPCs1hkjMYKk2u8KVHnNdlaES41KpswMiNmJktAoQ1cLJfE6Onahs1mneo9oK7rJF3JAp3tCCGymM2ZT6dpuis1SimUMkxnU7TW+OgTs7Ycse3X1+wqSGws6T0ieBbzCZnZnVUQO09fbVG09P0SrSPSZKy3nt//X/6Is+WGyXzByfFhMsz0PbgeHR23jm/yo48+ZDQZDQxZDZhhKKOAlKJASGwZXCS2AeEkt05u0TWCtgtEAlI7shHoUcT5NboUaDPBx5ynzzb8k3/8b/iD3/9zYoxMZiOilizrnhcXnnEhGJkcpaAsMopyRG972npLu16hsjwxgEVkOi45mFneuZFztW3oe0ktMoIcBj8iSRQF8drvoG0Dj5+dU97U0Fo+/vlXRGk5uXXIe9+/D8omFoNQIAyoVJ8F5wh9h3QOHSAOfmVSpDpECkNAp/oKPXhBvZnaC/Gtgdp1Ez5sBGlqMVzfAh5JAIPrLunrU/rmgqpZcnR8hCnK6zo0ilRPpR6DNPAKyWDx/oMTDhcFk9GMIp+g9Ygoy2TGLrP0OpUahkgDsCDlAJoP8tjhuSYZapJEDS/k+hkPpcvfGO78zbniUNxfv/bBC0kMTA3RQWgI3Za+umR7+YzL18949vgbtusKJUZMphN0Pqa2gT5KrI9MxiVKKCSWg+mIbZPh9RihR2zWr9BFTpA9QVr6KKmDZ2QUo2nGfDFmXEqEcFxeXlKWEilzlNGgIzJacJLge/oqYgxUqy1E8D7dB846MpMRA/S9Raw2yaQ7BNrTM2zTISL43mI7i3OBvu2TOX8IdHWN9x7XWwTQdRU29Air6OstzjqEj7jG4fuUAmTbHmstzjokEm2yYRgdUtKJTzQJIRNYiffpOSNSiluIieEkEjMTKYje4/oW23XgLLa3aQVaC84jEOTlCEVA5hntynLVBS4uG67WDVYYbIxcrbeE0LGYl9y+I5GyZ71dgnYgAleXlyihuH3zFndPbpLrjGh79icTomuo7BXrpqMKVeqcpcHHDZnq+Og7nKnfGWTYiBFRlzxvDHZleKuYc3h8DKojBMt4PIYQCdYh8gXVasnewR6j8RilNd46eiv5YvkVLxtF5yPlLOfuZI9R5shVYDQ25KWmKAximBDGCF0QXLVrquDoTYHIC8aj9AFFpTBlR9F1mKLAx8hVtaHMS0yep8ltJE2XBrOfOFhDp0nvLvs2Dk0c154FSipCiPjYJ7rSbu/ZmdXFYSjtPUqBEiYhUyExEnYNkRqMZdIPSAiqVgaMSJNsRJoME5ILejFChHjdOCqdjKiC98jBqCYVOAKDoLcOtzNQ20n34w5U2KGXuw2Fv7GXvtl3vv0nkHC4wRU4CGxtqTctUmRkWYkyeaKIy4Y876nr1wjRkuVj6rbhy6+/Yds0iDxLN3+MaAG3b97kw49+gDGGuHPvVYYoh9QEBRFPjAmtlNEjHPg60m8ajveOcDlYK/E2HTpKJ3qakOrag0EokQCGUUc+80SxARHI8hzvAl/+8hv+9A//PU8fv0oMiZWgPHeMRpEoeuaLgsXhCfb1KW3TUogtbrPGlHsgU7ayNhnaaC5OXyOlYDKb4klFKVIl+YBUSO+wTY2wFhUi0Xv6tiOGgBIaOegEhc4IvUfKDCl0ooGrBEJIMSjmBtlEovPFdJiqBCj43URfiWtT0WtYSiZNHX6L7a/QKmJ0iRAJhAveERzDZ5rYDLZP93ZWaDKTo1WB0CU7U0ep3lA20w2grpkL1+wFYqI37oiGwwThW7yE60UYBzPP3ZKN7OQdb9avEHGQVQyvTEqQAyXRd4R2zer1N3zzy4/56z/7KbNRSTaZ0dSBurBAxDlJ1UhcbyhNYi7ULtK79L7JmNIaQnDJWFDIRG/TCXQMMQGu1oEmFelCCrptIFTbISbN0/aaziqWzxr81XNurC2Lwwl9HVldeLyf4L1FqJS2UjUNnbcE4dGZp1SO0ghEEEQHRTGisz1SKowWWAcqQvBJlkGMLFeBvXzB3myPeWGQ0VOWGW0I6Lwgoliv13hnmY4nEKBpW/YOprx/54A+RM4vV2xqy7r1vFzVSJERhCHPMjw66c9jmk7VTtHoEXUh2dQ9X29TslAQhhACSlgyld4fJcVQjKb7IwiFjRHjE8gwyjQmRmZlge96tluBViR2S+txbgPOc+/kGBckUWc0feD0csWmbemCw0mSoVcUiMFAdZcy0lubmgGS3nk8GqNUTtPWrOseoe0wWU2PSXrePklqJsk4zA8Ml7pPtHkhIrlJMhGhPCYTuPgrkOFv6xWCT1RnhlZoCD9JwKMYTLQAkuRNiAEkHdiHlogd6gsZDa2XbKokK82DZV/B/v6Y0pQYbcmz1Cjm0lPuFYy0psgUnU2Rxq3rWW82jEYlWVkiVI73Hh8F5WRG71IBG6IA5zHCMDKa2aikbnpAMMoNZWHYbnuadoPJJ3gk5+fnBF1xflVhijGmnCHMGOHg9cWK4B3edti+IdgeETz7+8mLYL3ZUpQlMQSM1pTliP2DffZmc3JjkES0lmRaEYDlAM5KF4mVo6s80bUQWhYXkr7vWG4rLpcVyzqw6bf07gU3LjW3DgxHC00QHc4LTC5BtazXHctqy/lmzdYFMI4s00wmI0KAqlpRxkkChYg4l9K7RIxoqZmOJ6luVVBkOSEE9JBdvwMmldbEGJHaoIxmPJkhhWa9Xg++/QGpJC44ZHAczucYJSEmxqKta2Jfgd4iaMmKEi8yPv7lF/z1J59jxjk3bh9RFpLoK5TrEK5jMS754YcfcOPmSdJ2D/LDa58nIiJ4hO9Tw+IFsQ24KiBtOtelEkhlETqQjyIi7/Bxgy4UxowJccTDr1/x//wn/5o//uOPAcFkVpIXyaNm63rOq0h21iT/G1ewP8uZzabQ1lSrFUIoTF5QaIOQOUJZ9vYLbvSe2wtYv6pphCAKk0KiBIjBm0zGxBJrredpdUEZcpYvnvPk+StGk4z3P3yL/aMxCDcwDnb+VImN5vuO0HcoH8AlqQQkKYWSGVLlENIQC2FS/Png6STltzyteMNmSPVuupch1UPXJtUERHSEsKZvTumqU7puxd7enPF0jsgKosyH8tsNEpqh2hGSECxtW+ODYzqfk5kpyCleJgA9DkblSD0wFvTwHBXX0odEXEjP+XoIwrcGS9+65DCQuh6OfmuoSExtwQ5g2DEiBlaMoCG6K2x1RrM+Y3n+irNnz1hdXrFdNyByxtN9ug5evL4AXWLMjOlkzPHBEWMloa2IMXD/uEBmBbV1yDtTLILGOe7tH2J7z+F8SiRyvqzYNIJAhr5cs64boi6wDq7OUgLXdrMldB5hIwfzGfv7e2xerYe4zlQvK1MQffJaExikyIhBEoNAOkn0EikMOh+jbESIDqULrGvxIgE7SqX7XmmFMknCLIRAD/tBYroqBjXM4D+apFEgcHh8TKx3L4ZKePAl0UPvaK3D9z4NqKQmepH8qAa/NaXz5GHlfVrDUeB8wDmwvUd3lmIyQ4wyhJhwtT7lsxfPePh8w7KVtMIRhCIyBC20NY/aFiFqrNsQRT+k9KWh+ONVzd3llsLkSG85PjgkkzlLF2llgY4lUc2wTHhxWuPcmv/6O5yp3xlk+PrJa7z3ZJnhSWb47CHM8ogXLSFaFrN5Qmz6nrbeYCSMR6/Y399jPJ7Q1C29l3zx7IpNr/FRcLq2HNWRXBcoGfAhuYFneYZREjlQ/qOVqNxh/ZZN4/DaIbMcZRLbwQbQQ5ydUJq66cjykrbraLsuNftxyF1WEmcdg2dwuvmGHkYP6HCWF4QQMZmhqra0XQ+Ra/d0uSuUYdD+glHJnTYOEZgDTx0/OMCKEBOFKka0lOQmQyuFtfaa4RBCuM5jd86nH7GLsowJoVNKDn5CabMMHrZVS2/9t5z2d0jxbmuJbzao4fvk9SOHx/zHYKcYkFJEyoGvGvrek2cjtMmHZt6jdYeQW/rmgtlejhCKFy9e8vTZC7TO8Mk1CREik7JIyHw5Gkx8EqUNZQYmgxzeQJeMfGJABEFoPfXFluaqpZRTTJFo1J1IBldiOHhSvNFAD1OerIyYicezQqierCiIPvDwi2/4oz/4U16/ukKIHCsCaxt5ddUwLjU+KEyRU04ko/GM5eUzBAazXZNNtiiVE4xByaTX8iFw+86d5KcQByNAkZBzESXB9tiuQgZL9BHXWYLzA9orB9Q9Ie7pEBziLoUaNJcyofNDuoVUCdlOU7e0YV07LYud7AcQCYwSJGQ4UfzOCb5iNJ6jdJmYJNEnAMsnVgQi4qyjtz2myIa1miNlgTQFQeXD+62RZKkJ331+g2/E7qaSQ0xUQAykocFE6T/aX4bbZfj83hiiysEj5fo+Hb43iJR7qESaLhA6fHXB5fOvefzlJzz8/GtklMzHR7x4uiHPc+Z7R4zHOa9ePuX8vMI7yc0bUx68e8KrzSseP7kAWVLkinu3D1FF5NGjl9SVRSrJrduHHN845JuvH7O6rBEeZqOM+w/2iAgePnpF0wUm44L79+eY/YLPPvmay7ajU/B2M+KHHx5TbWtWlx25UDw4GSOLDCcCT16+5tnZFZ1Ie4qPAR9SQSx3khsvUEiiFzgXQIE0KZNcZRFhHePCUeqGm0dT6tUFH/7gLq8vz7h15wZt09M3BZvVBusC88WC1WZDOVN88OsfEYJnW1W4qHl91vDv/vJLfvnojJ6C+cExajRm7SJ91eKco2oqGhso5wumo8mwbhzWO1ZXlxA8Ki+RgAsdwQ8xpTHg+hSBLIOnQLBnSrT3zEyO73s2rmMyyZiMDLNpyd5in64bc/Ic1tuOzkmqxjMTcLbSvFyuaAPYa5+OHTE3La6dD48QIbF1Bt+TKDU2OmQcZl5aYbQmLxTGJCZN3/VUlcW6xMrRWpOPx3jf01t3bUCqSXF6v7r+dl4hpv9dw5phoEjzxgjwOl51ABoSsL9jPexYjwP7MKbUASUDe+OcmwcFi3Ek154yE4DD9R4jBdZ2VNUWJSdkg0Gk9QGpNF3v2VQtudE4C95BUeT0NuIcLNcNnh5lRtiuZT7OmYxyMpORFxrrbZrw+57OBoJwhNgTgmR/f85oNCPXaR+J0bG/mCVJZ1vT1hJnDCJ4ApKyHDOZzEAIXEiJLNumB6moqwYpUmFc5Dl5nuNCoG3aNHnLM4osQ4sssQZ0wZUz5OWMLNsHewZWE4Ri7SBrBSfZjGw6ZjzLGU8hGymyxRGtumTyylOMAn2bhk95kSVgwNnBbyVJorq+w2SGsijS8+l6xHqDdY6izFheLZmUI0AMiTo6+cc4j1Kak5MT8rJkPJ5w6+ZtfvnpJ+lnBzsMgRzKCfYXi2EAFYnOYtstWnbY9hKdpwSL08uaf/snf03VBW7cO2I8yYixgdCA6ym04v333uXBWw8weZaM/7RJNYFOEkFCIDoHNjEYfGcJHWANvpf4jhTjpwJmFJGFJbBFm4gxOd4pvv78Mf/0H/8+X3z2DUczw8nJEXXX0QePF+CsYNX25JXAvLhEhjnBO0yWkWcl682WzfKS0XhKVo6Tllt5siywmAruHme8XDfUTYeLEj8Mu+QwwNpJjZ2PLLc9nz+85Iuf/YxN23Dr/h7vfvAW0sTEYtj5JYjEmAje4vqW4BwqRLwNxDAktg2RlQJDjGKIa0zgTKqFBoABdqgH3/Z8GsYZ7IZ0IoKQEYEjhBrbXtI1FzTNitG4YDyZoc0YdJbADJ/qPMLQug9G7a6xuC6QmVGSr8gRUY5AFgPAoEGqIdVk+A/5RsIxsKqSr4K4roeuAYhv/zJ8z26viruB4e7aYQskua7YsTTwEDtCfY5tXrC9fMmLJ19Tr1bU2wbbObrOMRrNMPmYs6sLLtcrTO7BZPREzjYrmE7JtaSvaiLQVCs6F9mbl5TjGUEpnIfTs0v6tmbVNCyrhsp6XrxecrLv8WJC2ws++fgx2/UZfVexWS3BR3SE2zdv8MMfTXjxdEmWlTgPdWeZzxfkuWYkBTbk4BTbTiBRXFw1NK1AyIzlqme7cfStgSuHiwA52ybgOksXDPSKWAt6Z5BWUDWO3gk8kqoLOAwOSWMlba/ovEEKnViwIoUd2JCmf1JqIgHbgyAncZrTgvODMa8gJOA6CrrKJw8wB70LWCfpQk6sFd2lZTF2KKPxPhCi59HzS754esl5ragw1IFh4OGQMmK8p1k15Lm7XvNambTeZKT1nub1GbnWZErx8PycUo8RsaAWGmHBVB5vG4TziWX+Ha7vDDK8/+GPrhetgIGm1RNiR3AdSyR4R29rWhvxfcvEGy46i9Yb1tuKqrNsWkcTFC567Kan++oVLyeGu3slt/dN0g/ajDzXyBiJUdI4Sds01E3LauOxyqFzC1JiTEbXNmzrmvligVBpguxCxMeIyfKEBAePoEEomTLVEQMYkDwR4pAusbeYs39wmAALKdhuNzx6/BhrLchvyyfcwGRIi8JHcN6jh8hCMwAgbdsQY/p7ESI+eIwsMMYQImjjsbaj79q0L6hk1OjDjpaZNqgiL5JDbgz4PuXLRx/SYmxDisb6FlYZdwaPO+RBvpkYfwvL/BaLYQe67Hhiw+MDdG1PVTUoqcnyHKmTFlCqFjNqqJaPKHJPbkq2leXzrx5RNx0qK1KT4T0yBr733jvcvns7IaxDDBFKDyDBsOk7j3Ae6QMqCIKN1OdbmsuGTJRJV5doI0gtEEIng5/BS0OqAfwoPWbS4uUGUwS00cS+4dWjZzz6+Eu25xeoqK+9D3z0bFrP09ctUpQUI8dknKJqjC5oq5bRpMNX69Q0yTEgMXnBfO8g6SylRggDKkvPK0okAd9WBNcjQ/Ly8M6jGICF4UCUKmku06BfpoNkaNjjQJ8SYgcsvGnKQwjp78JgoDfIZXYygxR3FBChw3UrbLshzzNklqX3XySviJ1OXZC+rjcVBE+W5SiTNIJCFwiTJWaFSjKJKCSg0wa6O9gEw5RnuES8BkCuvxbxOiHiumSXENWwBuNgqirfHIxvfJHTulVCIIIDt6WvT7l8/hXPvv6Ui5dnNJst9++9TZlN6HvH/bfucv/tG0gRsLFlWb9iXE558OE9jo4L8v0xV41jue45eecWD96/ScTRSnj41VMOTw5496O3U0TbXsHP//oLmqrl+O0bHN07RJuMFY5HT14zv7PP3Q9uU4wy4gj+8qefMzaa+7dm7E8FhYHXr6/48KOc+eEBUqX3bdPv89OfZ/z7T75kaR1WKYo8TTNk9MltXEm01AMwJLAxkimJkB6jLcVY8d5bB8i64XARUYsZs1nA5BMODwuaymM7Q24yIpJyXDCaGchyVJFTtT1LL2n7yNOriq0NZOUYJQvyskAXGdYFtC4plKa9DIgYUFmByUcI5ZnsjbDOUTWJ6udkckBvfY+1Pvl3iUgmFfPJmLbaEPuORZnh6gbhOqr1BiU7Pvr+9/n1X3uHo6MpmZb84qcfc/r0nMNxydlFzcXlmkwU3JyPaKuKqzZRHC3JXDcdhHEAmtN+aHSS5PkQaNqeruvQxmAySfQWbQRFIfGupcGRaYE2Bm0gRomPGiUNbWORIn0GwYNToFRkNMq/67H6q+s/tysCPux4f+mPhunB9cn4rWI+Dmft7pxOM8Ek5tLBk4tAoT2TPHL/qGCSB3RsKZWi0JqmsbjOYjKBCJ4y1ySPo0T7D0Ky3qaEByUVxWyG6z0EiRQa2/f0VuCc4mq5phh5VGaS99F4Qt1agpM0HXRWILIRglQESxVAtETvWF4sMdsph/EmQWpOXz6jt56ubXE2CYq1kuzvHVLVFeeXl9zMbyKH2qccjZjN58yns+RbZV2qFWXysRLDQMU1DU3bYvue4Hr6vkNqgdYZMUbqtqOqKiQwziSXa0+UkXXTc3AmOZznTOcl+uyK09cbXrzY8PrVJdteE6RmPBlzeHhA0zTJsV14gnAUWfLrarsO6/21707VNpSTEd72BO+ptxV13WC0uWakSSlpu5YsT5PxzXo91G8BrTQ6k+RFxqjIuHHjmJ0C1jY1xJrIhii26KLAo/nLv/qUX/7yObKckGUj6qYlyzo0FkPk1s0Tvv+D7zOZTohaEoy+HiwISfIPGijTWPAduAbwhtgZbAfRBYT0ZKOALFo8W1QmMLokeMPHf/Yp/+P/8G/YXJ3zdz46YTIuyUczTi/XPDu9Ym3TcMx7xbJNunL5akUMU/I849bNfabTGcv1mtXVFbooGSuJUgKhDONCcTAL3DnQrF709D7HMwAGIaZo7BBQPmBth3WCL16e8+nD50zzwJ0Hdzi6sYfUcUi+2jFVE9PM9x2xT+ya6IfyAo1EIWSOlDkxDp5mg7dBkhYIGNIiUh08pH/J1NCnudwg9WRnSiwg9MTQYJsrmvqcpluTlzmT2T66mCB0nmS/yOuzRg4kp+gDrnPY2iFFhtEGaQxQDkkXCZxIqSFDzSd23M/dHkMaQCHS8Oh6UDNwFcTu96RB6ABK7ICF61+H4WHcARSIYZgbgJYYNsT2Elu9YnX2hBePHrJdrVBRoYWgi8lsPB+VKKM4PNpjNp9i8jFWCD57+IiL89eIGzd49/5d9o4PaDs7sK1S34BIgNO2alksRlytt1jXIWQgM4a9+R6TfEwzbthuLnj05TOk6Imx5+J8jQieUZ6zWm1pm44+Gi4uHc+fL7m83LA3v2KcB24eTbl//wQhBE9ef0YMAp578rzk6PiYvo58/fCMrosg10znJffu32DVtLx8fg5RpejJSc94WqAnI5bbwMWVJ/Sefe+J3tCFgHCR841luw6UhUkyNJ32gfVqi/dQlhIbOwiB/fk+VdWmPklKtIyMRhlGK9omDVC0EdjgKMcZGs1qaalaSX/W0sYlb29L7r59n65uqJqKy0uHdZosy7BRUNcNvU2mxMZ4hAp0gyfODjiPSLx70we3AfreYbIIzrLuNxgZ8JnC1w5vPTrXLPam+CHZ4j91fWeQ4a9//gsQgulkTG4ynPV0XUsUAZ0p8nzEbDxGj8cEcpxsyQ6OyLOcLMvJj2HuPI+ePUe6U4S3RAW1EFw5waEoMeMZ41nOYpGjRbqNpM4pHaxcw2LRs+w7pMzIyhF5kaO1pjaGANemeN4H+s6SmYzeuaEikOgsHWR5XiaDxYHGL4YYHq0M4/GEPMuYTGd0fc+tm7fITM7VaokbEh826zVdZwfUNG0q3qfbVGtNnmeUZUmW5YN2j2GKJtAhopQhz/MBfAgpIg4G5L+AGMmKkhiHFAugyCcIE+naBu96wBGCQ0uBj+6apvMGzkwTbiF2cMKbv/oWf+FvfD1YlaQNOKbGNThotl16P7NxMlvRyajSqB7vLnD9BYu9EVEonjx/wS8/f5g2UZeQURU8i+mEjz76kCzLE7VeJ20dg7QBIDpP7NKYRjmILtKtaqrzmkyMUHKEixIXZWIJDLo1Bg+AnS+SLlJMpZdLVO4wuUT4lu35KzYvnlL6nrdv3+Cb50uakLKmXZD0UXJVOcpLS1k0LOYtR4sJ8/mCy/NL+rpBra/QZYkeHPOj0OiiTOtAaKIyiEFXJ4Qg9h19vUF5Cwi8Td4gWiZjHyFzpCpQKh+QfgfotAXE9JpCTBM1RBikPSQ67s4YSCZ2TZLUiQEu2pW7JOpmrGnrCwiWrJgglElSFe/x1iJ25gZR0FT1NdNCqwyTjYgyR5ocqTK855qxEAf2xQ4Q2GkG0zUYKzGA8uwAdpGSL+IOf4iDdEYSh4ZwdwbuGEfXBQKkYmO3UfqWfvuS5asvePXoS6qrS+r1lum45M6dE46OjxD5A3IdiXGL94G7b+1zfP8mJsswssPZFbP9jN/9vQ+wUZAbQQxrRAz86NdO+N4PjsgziZYdIVY8uJNzfPQe3nmkcIS4JssKfvLbN/j+j08oM0P0S4KH99+dcuvej4lEShmw/VPmY81v/sYR2XyOMobV6SnFeIqTMxbj7/HJ11+yWvVIcmzfoRFIEdC5RGUS3ye9b3CBIFI1E1Ugyz1COWZTyWQ6IYstN28cILVLPgJZpF7W5NpQZILJ7AAXDT2Byil++ukrrtrI+bqj6iJVDWJyk+OJpLWOTdPw/NEj8rzg5ORGyoAuy6QpFJLNtkIphck8RqUIYuf6JDvpLZNxRrQSQ6RQgjLT7O3N0WJGvd4gcej9EeMiZ1oapGy4/95tbn9wh7IA19WEbsWsTDuWOihReDa1QxiBPJlwsW1Zuci69VTWY2Uy3kq11XBviEhve+yqS87NJkMhkzkHieUAjt42yOAYFWPKUUmIcNlVlMUIgWG93iTWzcAEEsEzGY8Q9j/m6fzq+ttySR+uwdBrAD7whr0wbExykITJoYnxIRCJqJjUWxpPqSKHs4Ibh1Ny2TPJOmy3BQk6H4P1eJv28b7viT4kaUOZY3KFiw4XejypKdZ6SBOIkkhK85EqUq8r2r7HGMnB3iRNv5Wm7SOr5ZI+ZrRBYBEEGbBui5aS0bgg0wJQtE0FfYOYjZhMFhyNyzQUigErFZGAVinJQCrBdDZNJ4xIZ8xmtU5eFoN8Qg9GqGVZIJRiHhKDSWuDFBLnLclTxyfKfJsAyFevXrFtW1yItEEQneDh6y2nVxWlChwuJhSZxgeQuuD1ec+msnTeE2KPkTrVDXXyUBH6DfuUmKROk6IgL3KKokAazXg6JoaA63qKskBKifeJCSqlQsok3bWdpSxGSCW5e+c2VVPjok8Sz+Aoe81sMk1DJ+9oN0uKrGOzesV4liaZZxdb/vKvv+By1TBVE87P12R54GAhyQzMp2M+/MEPODk5RhhF3CUk6GEwFDzROugDeIGvPb6L4HO8Vbhepdh05SlKEHmHdVeYQqLNCGs1v/zZl/yT/9f/QrNZ8zs/ucuv//g+eM9nXz7n3dszunZL2HSsOk/QAqsUWw/Ggl5WmEySFRlHB1OqqmZ1dYnUGpMV5Eojc41Skr2J5sYcXlxENluLkzKxJfFoQMUIwdJWW3Dw5OUVmw7KQnPj7m3KUYZULvlaCUMSJ0JwHt90YD3CCaJPdYtEowZvK6ROhtfhzZ0MaXCIEEPNOAA316ACXJssDrLONwaJlhC2dO0lXbdFZ4rZYo4pp4isHH6WSDWsd4gk5k4eAa3Dtg4RJcbkCJ0NEpiSSEaIqY6VUg3PZajjpbpuBMXAWNixnr/tcfVtQHQHnVwPE79FXri+FAzGZwNV3kFs8PYcW78k1mcsz55w+fIl3foKEyVGa5IjfWL2ZHnOqMyJMdB3FaNsgo2C0DXsLWbsHx3QxMCm2tJbOzAJQWtF33Us9vd59623sJ3j7OUZm21L1wcOjk7YPzzCNi2rGzNevnjKrRsHjMcZXVfhuopge27eOObO7WOyUnFwfMKnf/wl265gNJsQRcNsYbjz4IjD4xHGaH4gPuDzX35B10ZuvH2Tt999l+AjW+v4/LPHhADvvfMO99+/ixABWwg+/stfYHt4sDfj/g/eZTIvePzNM56cXbJdN9ySkR/9+gfs7U/4/PPP+fTpI6o17B8c8v0P3+bk5h7ffP2Qzz9/QVNHZrMJD967xfsf3Gd5es6XD79is+rJ85x33rnJre+/xYunz/nF50+pW4NSOTduz/nwg3u0bcPXX33B6akDlXFya4+jG3epNzWPvnnK+cWai6uGB7cP0WWBk5Jnp+d8/fwVVe8Ig5mkFxFPRMUUlxmCx3uGlJUkx0jD7ZCUWd4RYgsmwxhJbmA2khzOci4uV9/pTP3OIIMn0vcd/dKSaYPte+qmoXPdNSU1y0fkRrNaXlFkOV8+fklb1UNUkE4kKSlp2pptvSHPDZMipxGBUFUYAr2fc7ZpKfNk3OX8Gmsjzy8tz19f8vTFFZgxDpjNZhwfH9G1LcF7vHfUbceN45sopagHqo4qyhRjKJMkIc91mjqIN6YvSkq0knTWcn5xwfnVkqRxXhIEjEaj5C0RPN57tLbXm4K1Q0qEkhwdHnH//n1msxnj8ZimSWhSJH1w1bah71ueP3vOarXEB08Mjkwno5Cq2iKFSmZMUqJM+jecdUQfEFJRFqOkC7Ie6ZL5pnNJW5P8Hd+Mh98YOA4dnYD/7Z1nN6UZtqkICEnfWqpti8CkDVIO1Gxp0aZhefGIcQlaG7a15U//5GPOTteM5mOyzDLONEpFfvTjD9lb7A8HZmIQoAeKPYIYPKHvwHmkFUQfaS42VMuaPJYYWeKDwQWF8zI16EpeN/NIkCog80A26yHbomXE5Ap8zXbzmrY+4/bxjH5Zk6uSvg88Pa8GU0VwRCyCy6pnuha8fn3FbFwwGo9pqob1conUhmKyRZkxghKkxpicqm0oRAIYIooYFTIEumpNu63QMYIXuD6QiYIYE5NAyhxEQRBZ0lsh0GikjNeUPnUtQ9gBCkmaI1RCtHcHU0plEDsW3HC0eqTo6d2KIJpUROmCIBNNKvTdYBqaHu+tw1mHkRlKZWidp8cLnZyahUlN7XAwC5EmalLLazOs5MhOYqdc1+bi+vnvAC+BuC5MUZKoEv1YfotIEwegTAB++LudNhJXY7envHr0MavTR7SbDV2bYmFv3Tvmxu0p6IZIoLuOJQST9/hwRrQKJzKqVcvebEq/vqIsRvSVp3Me5xrmszEmBIzPqTYNIQSKvTl+u0QbQ9NsiQRClrw1cAKh93j25AWut7z7/gPyaGmbFq8z1leXzA/myEwiHXgXyTNLTsuLx6e8fGpp6gZiSpdR4k3Nk2J2UzMcXcBkGb3r6VpHzCRaBHIlaJqaHIkIFm8nycehyAihI0RLWze8fHnGUSywlDy/bHm+chy9/RFdlrNhxbKvaRx4FFJJOufpnGVTbehsz97ePs57mrpms9qQl2GYRoJzjtwY1ldXONcxHo0RzjFRgttHhxwUgtv7E/A9znUoqXGzGdY6pNYoBSJapPRsnn7Jz1dfY2RH6HtUF3lwY451GusFd4/HbBuL9wKTHxFFTh0Nnzy+4E9++jkrBzLPkqu+jDRtQ/SBXGU0dYv3DpElerMElBaMCsV0ZhDCsKmW9LYj8znTaUlV1ThXo3WBEC55p7hAsAERIxKF7X4FMvxtvQZSEd77N5TpmGJqw7AXqSF6bEdJ3ul1jdKJMNh3jFTg3smc/XlGmfVIerzvKfIUp4oQ1J2jcwIwiBDZrJf4GBnNxqyrDevtlqKYcHCwx3w6J5OGi8tLstwQCWyrGudSVNq4lMxODplOSrTWBAxPX17gQqDuLFsnaHzED+eDERYpAuPZCGe3HI8U40lJITaUHu7OFVcbR+YEdUyyjeAdoq/QRPrtEiciKsuIfQMupRs0Tcs2JKf0BMYonLdsqwrr+gTsGoPRCiMTQJHlOSbLGU2mSKmYjKcIKdFSkilQRqJHJaPJmKg1rQyMi5Kr5RprAqN9QSkUZVYwHo8pihFCZ4zKHKUzWtvT9z1ZkZMXOX3f4yqHUorpuGRU5IxGI4J1OGt5/uw5tm/IB3ZX27TUVZV0185xeHTAZDJJe4oS5GVOphXTGNg/vIEPkm67RYoGay/xoULKCc4nFsPjZ88HryVP39Vpfm4LdCa5d/cOb7/9FiobTLCHBCmQiBCIXQIYoovYxuEqhxQlMWisj4kBqyP5WGDGkb5eU+QGqTXOSj7+i8/4Z//kD+n7mt/7r3/I3//tdyhzi+sDl6srVsuOd24f4J9dIo1gGwJBeIKKWKPYusD5tmd8tWVvPmH/4IBnz56xvjwnH43YMwqDRGWCSQ5HM8mNGVxUdfKnHA5379I53ncNnkhVVZyeXeCiRmQZWTnHi3Iwo4ZgNdiU2NCtW/pNj3Qgg0KikWjQGmUyEDkRlYaICQlL9YeUgyl1qpPkTuJ8XQ4P039xPaMbJFKW6Ctsd4V1WyKW8WyGHs0gH6Wo8gHECrZLht7BQ0jS2L63IBVK54PPTwYYUCntIqKTxPTau2pgcKqdT8S3/CJ2solvE0V3Q57hNYhvDxuHKxCvWa67FiDVVQ7ElmgvsJtnLE+/prl6jm82YGsymSJOozDYPjGnlTJ01vP89JSmrpO0cJRz+vIUEZL57cXF2U4Uk3qcmADVLFc452lsi7UpXnpSjtFKcnm1pq2WPF0vWW+2rJcrQugZrXOePLtEmkDdtfi+Jsg9ZCk5W17x+NmSL755zv23PuStuzc4nAXevj9lUnapTxMd9+7tcefB308frPdIbcFFfvSTO7z34QmIyGReIOQaEPz4t97i9lsLrA8cH+9jdMTZLe//2g32H8zZXG25eeuY0SzDdhU//rsfsLix4OWzC+7eu8PtW/sQe/ZvvEt5oHn+eMkPPvoB994+IrgNx7dvMTmZ8OUXTzg+POTOrT1MFvjw9rtMb+/x07/6nP2jQ37jNz5gNpM459AF/Pzjb9g73OfHP/mIvb2AVjlR5DTdhvc+/B7laIT3liglPbf4+JMJ//bPf85VZ+n7gDKG3ckWEOnzQjDKSrQxVG2D9T3BQZABLUBKj8kie+WIO4sZJ+MRpbKMbky+05n6nUGGn/zar9P3ffoipsliiAEbEtU6IlLWfIhslkvqqmIx30tUIeewtqe1HV3X0TU1wgWECtiuZe0stoo0neOzp6cIGZhPJ0Tn6G3HfLFP1QpeXNVsO4umo+8tWkn6bka13RBDoJhMEDGi8AgvKHSByXPGswlKJdSm63pm8zld3yNioq63fYdWCV3fNh1dl/SDmUkRa1VVEYhMp1O89+RFwXQyI0aBcx7nHGU5TiBM1/P86XOe+MfM5jOUUjRtNzRdiszkTKcT5rMFeZ5xdXVJ8IpxmSc3WiKT8ZTJbIzt+8Fhf9g8YkwGmn2H1skcM/QdZV7Q9xE/TLF3VCkGLdA1sBnFbtfkDfa5uwa2Q/zW9hSg63ucTQ2EGKL6hHAYU2HtGcFvMfmUKCVfP3zGxz//imyscL3DyIjQghs3b/LW228j84TMX/8nJUpIIgFnO4SzKCfBQrduqVctOuZksoCoCUEmIz4GY8PrjOOI0AGlLPkkorItUXWYXEOoqbbnuL5muphinOGdH5R89vFj3r53RGN7zjaWGBSe1LT3IbLceiaXPSdHPScne4zKmvWqomtamu0GnU9A5UhGw9qBZu2JKuJEksV0bcPy9SVxm0ygvE/sBK3BGMgyRV4a8lyhM5Ei0QbpgWSItmRo5uUbWl8U4ELSNiaPgzRtQ+0YDzvgAaDHhzUubCjGBcbkyZ1ZpfvX+/7adZsosX2P1gVKZgiRE6RG6DxREUSW/n0CQiRzIh9FKriVAj+wdoZFJ6VK2cMDFh9JB9vfNHuEoAfEPaTnL7S8nh7ugJZ0/6QCRYYe4Rraq+ecPf6Ep1/+gnGhiDbQVi3leMLRyQnSaHQmEEaBKVCdgxjJJxnWVok+F3JyWvRszLwwhKoiyzMKE4nSMB4rvG9RwhKDoO8SJX46n6BNnrZrAfm4SBnErUPpgoP9fbxrmU5LbC8oMp38OCYjyjxDFwphFCIzrHpHUDl1l/Pp189oO4HSBdFFTGnItUQJjwyW6aik7Rp6KdE6x/tA03X/H/b+69my9DzvBH+fW27bY9JXlkFVEYYgQTmSarluaaY7emL+07mZnglN90xPRItqERIpEIYgUChv0x633XKfnYtv7ZMJajpUF7qgIrAQiEpzMnOfvdf63vd93scQfUKHAl8aLi4OnNy7w6yuKZsGI7ImW0iF0CV3zu7z4K33sEFSr+9xuk1sfvopV/ueXkHvAmPINDvrLcNhyK7j8wVvvJETGELmnNDMF1RNk5lcKQOph+6ALQqq2QzIsVJaSioCTWwp+pYfPLzDZx894eLyJf/kn/4jNjdXbG8G1utzUJrN7kBRV/zhD7/LydogdWC/u2LZzEgpYV1g6CzJR4KfgDFVgKr55cdPGcKMX34q2W96tDAsKpgvSvoh4sZAWTZcicDuYBldD0IzK+eYIjJrNFIEQnQ0Vcl234LoWCwrqsrgfWA2g9V6iVIFXW/p2wFvHWUDTf07ucR/rdd3Ht+hPfTsuzbLFVJitHnLHuN0sqlIxBKnnPo4oaZCZEqxkoEHd5fcPZshQ4eII0bn+r9eL3MP5AP94LFe4XwiOY/3msFKQjREr5BJYaRBKUU1qwg2+x9c3+zzjlUkyrri9HSB9w5nW9r9SFHVSFVRFprVao6wiva6w/tAiBOlG0HbRmppWZSCRieatKPyA2o8MI4Cv7e4g79NNpIpga9IJErlkYxE5zEisjhZ8PDRQxZnp/lcFIJxsNn8VUl8dNP7l7DDSN91eOdxw5B7sHHPZrOj73v6vicwpSKQ0EZTlxVaSjTQVAVKKYauw3mfzViV4qB7Nu0h16XcKHB2fs76ZMX52SlfffMVMWQPhbOzM85O1lRlQWEUM5P10uVsTpPucjjsubm+5vryBaAxVUXVzNkdDjz5apcZDNOwKqRCS8G6bHjyo/d49uKGJh1YzRLXL55SlQJjZnz9vOWnP/8YHxN3768omwYlPEYGjAqs10u+/wffZ7acT0bKGqFVXqAImWWN1oLLPlXu4FGpQooC5yB6BzJgKkm5kNjxBlPnpDbvJL/6xaf86//bnzF0B/6H/8s/4I//8e8xW4BUidLBO++9yYe/+pKqrtkeRtLeY5KgTwGtBTF6LJHrdqDSidW84p037zGfN+z2LfvNDdUssySN1BgpWc8K7q4iz3ce1/b4VBCRhBiJ0dP1LaVMPH/+DW2bGTbL03v0YcnlfkE5WFL0+AAxJMauZ3u9w/cjKmSuZl0qilJQFoJZoykqg1aZaaTN5FslRN7gvya/TKTcA2RfxwkwnOIgJymnIGSpqdsTQ4sQgXLeUC1WCFMjdAXSZPbKYEne5edkkvr6IJCqQkiDFhVonZdQqkBQIlFZIyqOZtjyNolDMjFDObIqXjEUfgtDuJVRcPxGuEVLjr3Y8ZwSaYItjhGrPam/xu2fsb/4iq8//CXzWmBSoJCCpBWm0HS9I6ScelYVK0IoqGYN9x69RVlojCl4q57x1nfep6xm+OAZvWecDM5vNhvqekbfHZgt5sQYsM4yHlp64ei6kc2uZb5ILFZr1mpN3w/sW8/FruXrJy8xZWTWKGywfHPxAq8CQ+/Z7yVVrUjhgJE9SsDQJlRwFNrT1NlPxo09kURR1ohp9E2+x+AoCoMhcnV5iakqZrMFJ0uJLAzBXzGmlKVTomO21JzeWaJiTxw7/DAwmwveeDTjre+cE92IG17grKeqS/7gDx7x9/7k+yQ7IuQOHweU0Dx6o+bN7/yI5Dy22+PtSFkoHr814533/lnmlYeepy+eM2sa3v/BOd/54UO8dWjZs90+5WS94p13Frzz3j/CzJZ4P3LYbEhJUMxPuH/nj/jw00/ZfH0BRhOCJ9iInhYrhS6RkwmrIEcwv1LeS6QWoBJaB0ptmVeRt9++w7C54tE7b3yrmvqtQYb//c/+HeM4spjPkZPD5jAOJJEojaGZNZRlSVnVmXYRI0kImsUcYwxK5wH1yddfMQ4DxkikEmgpUOQNgi1KOl3TNDW2KnHjwJhGhiFn3RaLE87KOUoptJ6kDWXJ+mQNKXF6umIYLHYcKQqZTYdCjg2pZw1CSGYpslitMjBQVQSfY0KEEHjneHlxQdt2vPn4DU7Ozvjqqy+4vL5isVgwm824vLhgv9tzdnIH5z273Q7nPA8fvsEnn3xCjJH79+/z9NlTNl9vefDgAZDou57ZYslsseQ7777Lr7seBJwsc8766ekSO/QMY898XiFSwAfPrJqxO+wQCExZc/TaMMaQAkSZI16uLlseWAhCUKQjspmjAdU0nAlxHCYnjPH2EHp17N7+N0lSgKG3pJg1yVJKlEy5MOqOq8uvJld1zf7g+ff/4a/QhUBrhZpurllV8v0ffo/Zap6RWTW52Ips1EgSJO/A+Zwl6yN219HtRhQFxtSkVOCjwuU0y1sJnZCJpCY5nfaYxqObkUSf35/o6fbPcf2Gsmqyv0DUKCN587vw9PMXvPvWA4YvnuK6kNHklM0S29azLRwvn29Yr5bUzYJC7zjsDxSzGUVzwOiCss4HrxIVX3++4+XVltYbXBC07YH91TXCZ8AgJIlUGq16VOEoC0s9cxRmoDaJ1Uzx7lt3yVUvIFC3TvkISRATGk8GZtL062JC6jOJZVIbiszbjbHHjhskuRETqpjckhN4C8Hl+yJBCpIYBFo12SxJ6qwZlLkwZqBDkULKbr5CkkIkyZRvKZlfj0yJYya2nOiJtwZ80y13ROUj3Ho5kCZTwMh0vx6R+6OiMGYPhtAy7p5z9dVv+PKDn7GalYgk6A8jWlU0zZLLi23etrtElAFTKtq2JaUcByp1iSmyL4Ed93jymSXSSIiOrhtIBKQqkToQhaIfLNvrEaU0LgSamcCPidE5QtDEShCcQwoY2pbgLWF0tPs9fT8igP1ui7M9RWPQXQFCstsPVFXJr//6C1482+FdAhmy5MqonMogBH4YUCWsmxndOBKF4OATlVpkV/QgGNpAWwU2247V/RV+9BkbwtDtRtq9pygjJgV6OyKKgWGIWGfRixItSxKHbDjkHLvdns3mhkTizr07SG3y92ItPoSJzZW1MEVVMg49L16+oJ417LY7pIBlVTMXktNFycPTgjuV4mSlscM1MnbMS0VvIoQ9xpwwusjNzQa/hbsv7uF9g9aBZ09e8s7bbxBiNvTdbvaoJAnOEcYBEcGUcz7+8AP++mlLSgEhIwmHlh6dErX0SBEoZGTWGFzMbJD12YrCKIbumuBLJNnUTaiCYQz4OFLWnoRgNjMM3TVaa3RZI6XGFImqLqkLTQr9ty2rv7v+jl0nRUDKgfmqppwvcAFu9ofMshGSJBPdOOJDIvlJ3DxJzbIHQGRWaVbzkqqAYANuHJFR0DSGELIx6mBDZjLYxDhECJHSaKqqxntoe0ffOrRwlJXGxoHgE97n2F6pBMvFMrMaYsDbPvdSMkecjTZSFCVVkOxHm5lxIuGjJ6Y8VY1AZwN3Tuc00lOkkWR7YqoY28jYOVLKjDejdZbJji1KyZzK0+8JQuGcZ/N8y6HtkGUFOufXy0kW2tQVkYRWkqouWS5XrFdrUkwoJEopQgh0Q8/zF8+5vLogpggiZuaI1lnSpDSVMbmfSYFCK4R3hJDrl5rMn5U2aCkgRLQp6HvL7nBAS01VlkilcG6gO9yw1EvWVcNJCad37lAajXhwTnCeze6Gpy9e0naOi82WduyRsUcpxWF3yM7zWmcNszGUSfHN05d8cwd+/7HG22sUA3WzJqSCD37zKV9985LTs3OWZ2ukigTfobHMasX3vvceD994CGZqao5pTUqSrCONFkIi2sh4cJhYokSF99O9F7PJY7nweN+iioDWJW4UfPg3n/P/+r//W4Z+z//5v/9D/vQfv0+9kgidPZSEVJw+esD93cjNywOP7p9h0w5GD1FgqgI3DPhg6Vzictcxv7jidN1w584ddrstQ9/SHQ6YYoaUFlUaapO4s1TcbTxd53CiwAvFEAJDd8AOPeB5+fwJQkSqqmR5cp+nVwH70xcIsjlkyq6qbDbXtIcDSiSUUCipMDpSyJ6mMsxrR1N3LOYF7zw+Z2ZCZl4SJtBKEYWYZAJMZtWvs3gns+x0TJMYiWGHc3t8GAgE1sszlKpAVQg5JQA4i7cWGcgDrM9snpSKSdZUgixzvGUSt/KPFMk9D0coYIqsFDIbaotX3gsJeZsa9p9cvxVd/0paCkySaTEBDAmZYvaU8z2hvaG9fsrl1x9xuP6acLCU1ZIUB6SQaC1QSk91FBCaslrw+Zcv6OwL/uS/+WNCFHSHAak12+0NRdFx/94DaqEYTWaWzusFi+Wa0WZmadnUSCGx40h76HjyzQuqKnH/wUPu3rtH1/a4EOifWDo7src9J8s5FBoVS3btgfTUUZkaScUbD0rmjUOLFkIGaNw4UAggipyYMSUpiBQYWouAbIprHYUSGEAngQiR0mS2WMAjy4LeWVyCWVEQ7EDfDxS6AJc47A/UTU3fbylCS3Se6BPtoac0J4zDAeP2WXolDDfXG05XJ8Q44O0GNzoIEesdiIbt9pJmviCGvHj85OOvePudN2gag/M3HLqW9WrNV59/RPO976GMJkoQ+sAwHGjmJUMX+eaTp1xeSw6bK1IKaF0D4fb+VkIjoyR6xzAOxH5EigxuuyEglSREQTKCJDzaeCQdtr/i4uJL7jz6L8xkWMxmNHWFKYrMTPD5RUQiPkb2+wPb3YYYYbAWUuLJ0yfTXZ65R0rl4dPa/LBmWpJEK42SijEFgoxYLL2f9EwBxsMBZx3DxJQwMj+IvfU5BsRbvHPZ9FEI3n/8DvtDR4wR6xy77Z7NdofzjhA8LgQKYyBGtMx6I6UlTTMDoKxKIjDaEakkpij4vd/7PUBgnWM+nzObzzgcWowxeWsc023qhNKas9NTrq6vSSS6occ6y0lR4J2jb1v6vsfbPHh4a9lcXOJdjx06/GHDrMo0PmNbVipSNSUxObyILIzBe4cNjkIllPN88/Ulzf0NXhhUEhiTTVa0gqqUVAZKnShrhZ4kaHJKm2AaMvNnNbEfomDsLMNhRCDQUuXEDyJaWbzfQuqo6wqpCz7+4HNeXt5w9+6dSbLhqEzizTff4PGbD9FFNhlMR+q/zMhtioEwWIRNCJuwhzE3N05gdIXAEKLCB5GTKkQe1PNWdhqwjUdXDlWNBL+jaBQpOdrDC3w/xZkWNVIZki5BwunDu0QU+sWOt+N9wtcXOApGGyEEbExcH3pW2577+47VckY9n9NvN9kQyjlEb1EqElCkWLDrWp5djXQ+J0vsDglna3SSSGMQ2iCUyUkBQtAnRdtrzABlHOhby727eTBXx/dqci2XIUetinQsRJmtIJScIpIEUZAjWwFEgOgIvidGiy5y7jc6awKJkeh9jsFCIJPEjgEtGowuSSIXY3QBsuD41yIk6KlQS5kbviNV7xgDJTLTifSahhAxNeTHQvjaT+G1P5M9GdLxq25xsIiIFsYd3e45F199yLNPf0UYW5rTBYdth4yak/UZQWluNgeefHNF9I6T81NIjpubDRGY1QeMERRVgfPw7JsXdNcHvN3x4ME6b8X6FkGimQmid5TVnJurA998uceODc6NnJ9p/CjY7XuE6tFl9mMpS8HL53v6vmU+OyX6RDfkpLGbm8T+YCkbWJzMUaZAMOdwUChVslrMWWz3dBN460aLbUcKCfPCUCNpihIdYEySua5wweDGgDSafjjwPOx4sFwxjJLnL3fcuX+GkgV/84uveH6xZ77e0Cwq6qbGpmtSecJmu+eD3/wFg26w8dioAFGyXJ2AhIurDX3Xc2j7/NwfJTz5U5+ApEhMkUPbIrWCEOmHkUJJmnrBetmwKA2z1Zqz+w+Zre9j1ZL1/Rnr+29x594Zqqh5tOl4+uKKF/uevYOq1BzGGc+uJe0QGMbEbieww8jYHljMShQepQ/cefSIxf4Ji6Gn8xGhJTIGul2LEDnNKKP3kfWq4XrnMVoQ/YAWkUJBoTXJesbeIkKm2A7tgPCRqqnBFIAg2kDbDqSoKHWiWS4py/rbltXfXX/Hrpn0VKuGqAout1sOo4dAjsI1BqkFu1bSthYH+Di5w8SICBElIw/OT5iVihRGUnIkIjYEKgyD9Vgf8VHQjY5hjIik0UZwdveUO6cLttsrrq+3WZbqBbuLzSSXylnvdVWynDcUhcYHj5KR9WpOSBHnI9YlXPBEIeh7ixSSUgvqVGDbkYQgSU1AcBgtL6529GXkpDHUZcVgA9vDgdFltqAQkhBys2+dw08sAaVzIlJMYIymrDR3790hCIl1ER8TapJvjXYk+MxmWK5P8M6z3eyIPufFl0XBfLlgGIcsbRUJJXN/mKREGJO9vcoSoxQxhezr4AMxBmJIaK0xhZkWDIlgHS4mYors9h2j87TdgPWOUgbe+e47/F//5T/l4b07vHH/LstmNpkqC4J3jONA1/dcXN7w8Wdf8MEnn/Hs4prr3Q5chylqirKm70fWqzXLZo4WgkWjKY3jcPmcqsqLkmeXLX/z688YRsdSKUqjkcpNdS7y4OE5P/zh9zHFJCVVkwkgguQjsRvyEsaCbwMyGaQocW4CnkS4TZHouw3NTKFNiXeSzz5+wv/yr3/MbrPhX/x33+NP/pt3mZ1IKCYwQxpSlGgtufedt/Hha8xccnCSuO0xPjF4hzJmqs+O3lludgNXNz3r9Rnr9V2sB5kqnM3R89JCFIpKSu4uJCkqLAVBaIYh0JmSOF+zvb7kwUnD6cxQVTXnZyf0nePZ8z1SF+gy95jeWm62nhCySbbSCiUUJml00hxGzdYryi6xaHvu3FU0MwW4IyE0GyrKqSeZ/ptuJRW31o+ZJRo9KXXYcUNMHb3rWSyXCGVAV9kIW6qsax8GjnaSKYr8/1RkVqoqkLKcFjc5YS5NCVyZ3ckta1VINS1djnLUfC4lbityvsSRafHq949eDke5xOuSigxRZEkf0YLrGA9XXH7zGb/5+c/ZvXzC249WVMrk2OeoQMZbD7Wyrjh0OaFGJEUK2a/t2dcvcmKNt5RlzW6/pamzUbrSJSLlZLSyLNFywAVPO7RUQ5YrDcNI13Z0Xc+hbfnm66+5urhis9lydbPhYEfcbYpBnq1MVbMwMx7dOeOtNx8zDJaA4uzkPq7r2V29xPUJYQLRaMY+R5cHl3tLLSLJgzAarWqSAm8jSkRKVefgAJuQWqKqGUIrKhNQ3lE2DWUzy5+HNrjBMlM1er5gXRnSeCBpCC6hZUNVSUphEDJk7xCRKIVGISlMgVAJmxTBRep6hiwL7pyf5X7eC0KSvPHGfU7XK2Z1jbOJUjaI4Fg2FSJFKq2RpUGUBUpFxr5HyYqYGj74+Ct2gyapDETqQlAUJSnZfJ/ERKkMkBnHSWq0LOiGHm8d0ihCyqk20Qn84DBKce/uOaX6bUnO/9H1rUGGuozMZjOEgLbz+Ciom1Nm83mmbMdESNnEp5t8COqyQsqcHOBsdvE+tC031z5nlpJ1IT6knE8tPfQjYhypyxIlBCEFnAuEmKZDQeFDorUjikChdDblSZnWFwRs9gdSyjFjlVIUZYGPgaHvGMcxxycqTd/11GVBYQqiz5RzEaFvO/zao6VitVhxeXVNWddcXl5ivWe+WOCjz/IRCUkkXly+QIqs83r29EmOSJMZpYeIHQeePPmaGKEykuurF6TgWTU1RMfMKO7fP2M9f8hiXrOYV8zmNaXRaJ0LQAY5PCFEun5ke7Nht7nB7h0XN1vkFzcMosrInRDE5JHkf68qYFYb1ouS1VxR14K6lJSlyAYfTJnfZJQr+kS77fA2YnSNVqClQEuPUi1d+5KiFBRFQdtZfvznP8NHSa0LCpEgBU7WNe9/9zvMlrNskDZlEjOBMSJEwjiSRoewCddaxs4TnKDQNWBwURKCwIcjwJAjHYVUoCVCO3Tl0dWIDzfUjYDo6NtLXHdNXWiMrpCyJCoDss5glIH1GzVevCCZEhckzy/3EEJuTKSi83C9d1zvHc1cUy/PKZ1htDX9VUIXCbnJaGfbBbq9YFEvqEU2flw3BZAohMGUBbKskNKgREKohBQCLbLNo0oOLcbJDFTiQyCKhFRgxxE3OuarFUWVLWvlcXBnSnNI8XaOFyKRhCeFlhhbdKmQWmdqny4RQhN9R3QjMuaUh+BkZguZOjcd4qhdzBKJrMg46gVzHFRETACHyrTD20HzSEw4QgW3kMGrkvg6i+aYiDFVzWOZfO0vQnpP6LdcP/mELz/5G4Rv6XcbzlZz3DjgfaKsV5hqSTe0tP3I05fXNFXN7ssr6rrm6mKLd5FZWWK0oFnPiSiuNw7r9szmRTYPqiTNUuDDSLk6wfYtnZMcxpInL6/oxgNloRj6nqquudl6nLeUVYmSnqJIHA6w23l+/atvODtdc+gGYtLsW0g7iykk7RfPiELS9xZVNNhQ0lTQmBo3tIjBo4UkOoGQCVNolkXNu48ecXOz4eJmj4uRzkviGCcfjQXWOp5dWQQjs0oxMvLg3prLm5EPP3tG0heUTYE2Chsip3cfM6aaF0+esIsFVhk8QMzxtSH4LDdLOcrWOc9u+wpcEhyBpOPnl2N8lVJoIam0oqgLEqfZKf/mhtXde/zk46dYam6KaypjSKHlB8WMk7OSzThw0XZcHwZMJSmMZLdxfPj8M4TwbLYtH330JcFFKi25d9Lw6GxGWQRQFSeLiupyy925IWqN8z3eObQ21HXDYT8gUqRvW7SMjENLKRKrpuZ8OaeuDF1X8uzlDbWuiSnQbfacnyw4nS0xqyVtN3Cz7xm7kX50nMxnzE3J6fJbl9XfXX/HrkoKXAiMbqAgUMqEFtCohJYegWRVlyQ3GY6+Rk8WIlEVgqpIxNARkicmj48OCbiYSCEy2oTQFcoIknUgBFVdsVwt6MaBduzxQFPW7Luem6srTk6WnKwXrBYNs7phu92xPxyyiZfQt+ev94kQs4FXN3S0+44oC2pTglS0bcoAschsBhsSLw+Obes5jHB3XUJMRFliSk2iwLnIOEXTJiGIUWZjMJ8QIhBTIolISh4hInVVMZtnJlwmnOcrs8giUQi8C8zmSwgR5wMpZLAg+JAp1iJLK0KKxIkZJ0VedhTGYL3Lvg9CYIy+PXMgMW9mhBjAR2ZNQ1PW6LKgHy0uBKqq4k4J/+pP/h7/8o//kKoy05ZzAJErWKGhrhSr5YK7dxZ85+37/KO//32ePHnG33zwMb/+sOHZyytudht0SPQ3I7RblsV7nM3ADc8Joc2u+1Hx0Udf8OWXz0Aobq6uqUrJrNEoHPNFxR/90Q9ZrBfEibafzbAngGEcYQykMRGGBN4gU4nzCu8jQgR0GZClZ394SVWDlkuC1Xzx6TP+v//zn/Py5Uv+yT/9Hn/8T95ndlKQVEJIDaYEWWV5hhPM79c0uwGxsZyeRCIlep/BVZ9iNramQiZJFBWDnzG4Jef3zmj7gDYnxNjgvEZQkVRFXZe887jh0eOSNLFNQoiMw4iIie6w4Z/94SPaw4FhGOhGi4seJxVBJgIBa3va3Y7BjdPwnU2nBQUeAzIbiSclCSkgrOXQe+6IYpLXHlmR3D6vQDbL5lXOWk69yv0roieELYmOwXWYukQ3M5IqSaZEaJPv0XEkRT8FuGXzeZLK4IIwCDFFtYucwJVS4JgQAbzy0bp9jUcTyomdOslNp6eIY0912yXdfk+vvuJ1BOK2+0oeEQbC0NJvt3z18Qf86md/xVcffcr9O4vMBJoks8pk4LSqKtyEVihVIEPJ06cvcNYyq2r2mz1BJFyIDN0hy7lxvBwuKMsaN3qcDSitUVpz6Lv87SmQSuF9TmgKE9hYGKgrSXF+Ql1XfPPiOUNwdINAxkCpFMvZCbWEO6dnnKwWfHb1GT4k1os5goCUESEFShVs9302qTcGUPgQGX1gvztQVjUhGJyd+psQefL1BUXdMFsGdKGp6gKvBN04Mgwji6VFyMx0KBaGfue4vLhBy4LoOhoDznturm/ouxF5/5TAwGxW56QtG/j686e8+YZBCE/TlEQPQztig0epmpvrLevVkq4dEbogdI44JuIAtnUMQ09VVmgMvo9s+j3lrMKMnmHoGV1gGHp+8bNP+OWvn9L1DrSkLrIsOxFuF3lSSGZlDSFgypJ+sBxGh0pqusMMziZcVIxC4YIhpmKSrf8fsGr+1vWtu6Gr519wkRIpBgY7EBEU5QyjK0IAoSSr0xPO79yl3e9ypJMPIFR2HDeGpm4YxhFdGFQ0E4p4pPKDMQXKmMwsKAwKkTV0UmWKT8ob9xhzdnRGyOrskikFPmXm4ourK+azBd3Q44IHMcuUHwFFWVBWNUYpCJ7T05NJK5QNTGIUNFXNfrOj1BqfstnaYXvIkZEhsN9saYXGjiPeO1KCbneTEUABB9tRGJ2lIzZTi6W3GJnR//FwQ62hKDSVTmgp+eF79/mXf/wj3nxwzupsQVUXKKOmbSH5UMp8eBLgvGM87NhcXXFxdWBkTijW3Awp32QuMkZFTIKDl8gB5A7My57aJOYNnCwNJ0vDeqlpaokpJFrloc72lm4/IMlGVZluGdHK4f0Fzl4yn2uQBZ988hmff/ECU1YYPTCvDVrBW28/5t4b95BGkdTRWyAXApEEcRyJ7YAYI77NET/JChQVUBGTJMSUNUJTmgRThnBSCqkSRRVR9YgPG4oyIgi025cM/YZm2iIIVZBEQZQVSddIWSHRFGViLUqEuSAExdA5xu6AnzDpzhkud4mvnzlG16N1xe6wpL9MxCRYnmQJRkIjVMm904aymiGFoigKitJkoyGyzAFlcja4SBOLJELKutPslREBj0iBEAeE9Egc1xdPefnsJe999wcUZUWS8pVZUUqklCULUuRGTpIQacTHLVIN6MJAUYOq8nsXI8GNRO9QKSPv3oExDcJU2S1dHN3Tc7QTMpvahcSEsh8BDpkPG/kqWhIpb71HRUo5pUSQWSd/y/gjP/7H758sZVBHB4cJ/EqO0O15+dmn/PV//HOefP0Zv/+Dt1AC6tpgewt6RhKn7MaabRvZDzWxqukpSC5yXtQcQkHyECnYX7fcEQZTFPSuQtsGOVR88yRSloLDbiSkEVPX9F2iPVieX0q2rUEajbSCg3OcpIrtkLcWXhUM1lEVER8qWttiRo3pJS+vR8rasOslKUkKX/L05YEwSVG8cHiReHm1QQrDG3ce0swqBtvilSYFx7qaUYmSykHRWWobaLxDIxHzmvOzM/q+I4XAbjcQPaxWM4QuWa89j9/MRkWyyNTj0dksG6nm3AySb54veLaP9KpgSDJHsMeE9DLfoxGCDojJKT4ngeR2J8VpA3RMOIkhUzsVeTOpMrMqxIRCc3ndMYgaZ1a8HAVzaaio2bYjq5NA3+14efGM5zc99fIud+8/YvDgxp5S2syqEprNfkcxn3Fvfc7DkwqROpSuKNSMTz9/QhSQdJblRZVr0Lw2pHHA2ZHgOhbrVTbSDSPr5ZpVLakUnJysaDeHiQKssdFxOpvRECmVpprPOBxGBCoDYUKwMJrVt6u9v7v+Dl6zqqBnJLlEU+St4+gjpYiURmHKgkM/UsiU5VBk3yZBRCZPXRYoGQihJ8i89Q3RZ2A3SsbRM1iQUZBkzehHRAo0seR6sye4lrZrUbrAVCXBO3ShmDUF81oyKxNGW5qZIqaKi+sNBRIf88A9Oo9SJUmA8yN1XaGKmsMQSAIWtWbwGTCMiGzolhQxGUILEsfpsibgsSEyBp97CZ+yZ1HKyUBABg/iK2Cxbfd88ulH1M0cXdVIU+Q+MIv1JkP7Sc6gJqq7UpRlkd3NJ6M8P1qEmvLfpqFbqlxT9eTCf1zyxJC9EXLaRzbkbMoS7xxBOoqiRBqFVCrH0CqFTom3757zxz/8PRodEMmTRCBNAEMSacJPc41TSrBQgvnyhDtnFY8fnPD9dx/yy19+wAcff8GLqz02wsmq5sFZyawcaDfPKEqDMjMuNyM/+8XHOC9YrZZYZwl2RFUJrSPf/e57vPPe26AhaYnQWYKKT8R+JPUj2ETsEniNosI7NYE+kaJKqNoz2mvKMtHUc4LXfPn5c/4///rf8s1XT/nTP/09/vSf/oD1eZn3Brog6RpkDbrMNVtmXOj0zcdcxhecjpLoW4TX6CgZnEWoRGk0q1nD+XLNyckZorhDUc+pT2YgdF7+qCwjjVJRCEEjsvQzLykSAoVczHJduS9JlIg0sru55Bc//RUHW8JsydUgaZ1jGDyHQ5e3qkqjhAFRIkWBmAypg84Hb0gJ6eGm7XmcCvRrQ/hRgpluiZXT6iTdupJlmQQjwd3g/TUhHRAaFmdniKICWYIoAUl0Y/YTS54YgDRJS1WJogQmWanOfWver4hXA5p8FUv52+lcR+CSSS57XMq8oick+So94gj33wIMr4EOebXnIXSEw4b9xRWf/uZT/uLf/Acunj2nUgJ5lhd/KLJ/k9J56SjAuZGEoaxW7Fu4urqhns85OVkhVU56abuRmAK6qGmqihATs3rGzh4Yhh6kxxQF1zdb5ss5brBIrQjRU2jFarWgriru3rlDYQpSSKzWM4gWVRg++rSlKjSrqqLSEhksh/2e/a7hs08/RUpBWZSUumS0I/1YI2TJ5189AQHLxYKyyAszpQQvXw6UZeRqO1JVmsW8YrCW7cFCG2j7RFFqqqoEJdjud/T9yKHa08wqhITlKnJ9veP5iyvCIVLPJOX9BVLXJDqiiNl7LiRi8EhZctj1fPnpc4SbozScnizQStC2A4dDi7OSq5tLxEOD9ZEYA0+/vABfkpwijA47eHrd8/zJBu9KBudoFnNW6xOclyRR0R0i+JKT5Yqr/Q1OS2ZlRZSObugxUhBkXhJWUiOFQCUQRUE/OApVUGiTZzDnGQO8PIzMhONm5zld3+Hl1Y5736KmfmuQoTsc8CGnGEQSSMk4OsDgfaZauOBpmhnjaLHWk1KL9/EWoVNTcXDWZ40VgjTFZcSUcC6gRDY7VFpRFoauy47uMXqUkhhtSCkSUvYN8PMEISM3LgSU1Ahd5A3f0GOMJniXkUYyCuH6FhcjMQU215fs1QYtJbXK2p1S5yFnbFtcDCzrhjCO1FJxOlsAAq00VimCHREpMm9qRAxoAdVkTISQNKWgiYa1yl4Tuig4O6mZpQVKJAgeI+BHP3iHv/8Pv0dd6lzEp21/mopsXlOnfEALMKWkaM5Y3DnjoU94m+g6y7a13GxarveBmyFysBIbE/YWdcoOwVe95dn1yLwUnC0NZyeG89OG9bykkIluN2YKoszNgBQSKSJSjvTtBUqNmGLFvh345S8/pCwNPkL0HpJktVryzrvvMFsuiFqClpPGP2u4k4/4toc+EIdA6HPQvEw5rihFRZiaoUROkZBTkZJSoHRCFQ5dDoSwwZiI1pF2f8V+e8l81lDWS4QqEWoqpKbMg7asSMIgiDTnOmvdKDkcAs5fcLXp6fuAtYmNDXz5TaQbs2mXKSoWJzXNbM18saKqZ2g5uRorlTf7cdruqpQbl8lsU5BuZSJZqfLbw3bW3GmUFMSQiLLD9dcctl9g+wOEt5AiEIUkiOnPT1skkshaPwA8MbSk0KMrctNiitvNUvKWON23KeYEI6UKtCoQIns2qGNihVDZ4MoH4mRCJI8eEFPBjCnrCo/VTnBMjnqdvZBjF6U4OijDMWlCiEkikY7vw+QrkSIieNxhz/NPPuaX//4/8PEHH1JV0O0P3DlZYq0nUhLNGV9dKK4PnqiWxLRgs4XBjpSm4MIC4h5SKuzBsdnABs1qUdHM3mJ5vkIlRxJxSnGIGAPtpsQ6Sbs/IFjx+NEyH/4ErvcHymZJPb+DlIIQA3Fs6Z2gblaclDMgcjUkNq4g2IAQM5rZAjVbELaJbgwENPvB0o8Ddbnie4/PefPBfUqdePLNl4y2o1lUvPneG5zKRP/sKbNKMluccDYuCSkDrPfur9hu4XDokHKBIGEKj3M7rjaSqpDcv7dCKklZlKQkMEXNtrVEIm/fX6Mqz2Wf2I6RUYKNiSAgOIsUgros0RNDK4npM5uaG5GYcrDFtM3JkgSl1BQvm1Aq0lSGs5MZy1rRR8+6FJwsDCr0nMxgPYNdnViaRGcEQ7vDtwvi0KNcBt4qWXJnXiP6nmUpMv2z9axmhnGwdJuRUkqSNLhpkyV04mymOVlqHp3c5bOvn9O2NzQqb3SVEqxnNWezGuEchVHcWczZ73uSIA9D0SM9FCbLlIxIVIXB+4SIERUT56v1ty2rv7v+rl3JYlTCh4CWESUDCo/RBbM6g+1WCaoym4NGH4lJIMh+DKYoEDJm74XR5ycj5XM0JOhGTzcEhp0jyYreTbR+uyP4rNGPEeazkpgU0hRUdYVSicJADEP2bEFM7IKIMjLff0qgTZklq2OPEAGjJXVjckpVH0hUbA4OG/J4FWXKFHJk9mwQkqRLorT0fqR3HhsFLuYKhpjkCxy3rjn2WpDZqNoolosF9XyBKqpbEDqRz6jgI9Z6nHc45+mDJ3mfJ74YscOItWOuI/JVIoBM2eU8Hb2Ggs8MV++JMW8utdGURUHQEWctdrTM5wuUMlRFmQ3KQ6ZF/+C9N7l/Z5V9nUSc+iwm8D7dDpzHbbJUmU3RLDRlfUozl5yvCt56eM7XT6/ZD57z0znvvzFDhRtiGKjKBYmKzz7/kk8+f8byZI0uDUMHhQxoGbh374w//NEfUNYlSZP7pInFEEZH6EbEGMEmkpfIWGQD7BBIMqB0QBYWG7Zo5SjrOSkZvvz8Gf/r//LnfPXl1/zDP36ff/IvfsidBwtkERDGgK7zMDTV+ySZ5tfE/HRNspJaHUjuBk3NrFqClFTzmnkzY7VcMatmlLrE6Nz7+GlTr4R8Fa995PqkHGeeBBCP95tE4pGqJaYNfrwmjk+Z6WcsijX16T24iAytp90NeMfkVaFJUoM0RKmn5Cs5gWbkDzEIbvY9Lq4ohcz3qcgS01sPqOOrS6/LDRI5Hv6Asxuc3+OxLE7uIIoKUeThVSRF8h5vBwie5CcTyYldIaceE3QGElRe34eQcpz3pBU9LhFfSUqnY+joQ/U6S+FWenp7WL32Y5GfjfTasocjYOIQocW3N1w9ecLf/Me/5lc//YAX31xRoBHFFJd+jMOell7aaFx0OO8w9YxCVFxvXjA6y8PTE87vnSGlISEYXZ4Jy9IQg8N7z6yZZcmELIhJUDUzMBWmKYkpoIzCuREhIs1ijhICFxN+6HGDRURYNDWzxYLnzYyyNmhRsJgtMEqwPl3SrE55+93vYZTi0Ztv4YbAxcUnbPfX1LXl4y+uiCkxm/fcuXM3nxGV5mKfUL1DYDk5XRCUJsbE5UEwjpb5YFmtCuK+R2nF4RDYbTrq0rNaC2KKKHXO/iB5+bInhAOn53OqmcSUBhtq+s6CXKK1wcVAv48cOsnTFwNC7lit5vjkaeqCwz5yeXGgG7PJ5nyZ2G8O6LJhuw04f03Xwbwp8S4hNewOAhc7rI+Iq4iQHS5CjArvJdYZVvMzZuVI60fkmCBItNeEFIgioivNuphzMqvZtztuup65qWjHQIqSrveEkKAqGcbAs8ueZ9cWj+Bmc8kffIuS+q1BhnaKhktkpC2GREyOfEtn6l3bdbRty3K5IITMTDhq6Kc1K9c312xjJE7mbgkBKcdVikmrnyOi1PRjjVQ58iYjexkRVBMdV8hM6c0y4iMamLLRkMmxlaUWlEoxkyYj32QjmZhy3J5S2SG/Eg4hJcWiQimNnl5/CIGy0HnoXyyydANJY0pE06CI1IVAp0BtJPOyoFAZMdNG4+oaHzI9WSpJpQaWq/zWx6g4WZ3wnTfvU5UyI+rH7+MYoyOOuOZx2z2VwWlIFYXIRpdNyWptuHOi2W5aLjcD1/vETevZWkEfBB6FFxllHoKidYGbzvL8auR02fPGWc35smLc9ZDATD4XUgiUCsS4I4UDdVMiVMHHn3zKdtfRzBr63qOVoDCKx2895t6jB0itCfIIMGiE1hDBtz2pzV4MYYiIqFHkgpEfEkFmUIkMMLyWIyw1aOOQZYtLNxQmoDUc9ltuLl8wn9XUzQp5lAfoGck0SFVkWt0xsSAlpFbMzs4wZsbDUTJajXWXRGdZnN5hvcrO1GenZyxX66wJ1QalM5INIFQ2CeJoeCgnn4TjMH1bcP92fTgWv0gSESEDSjqE6kFe48cXjP0Vq7ljNV+hTU9KPVKpCX8/FpL8Y4Ugm3sOjG4P0iNkRZLF9BpAhIjru+yCHAXB5YPH6AIh8/0uhL418xNHaYt6VZCPhe7IVni1FTiaNR5rY0ba49Fo9KiSeI3HdyyuR++GY9mUKSCCwx32PP3oE/7q3/6Ybz7+HDdAXSqUSNSzgu2NRxdrLvc1n78c6YPGyWxSGZUiUaNQ7NqE1opSGWSpKO+cMqbApg8sVysWJ3c5XD+nqioII7os0TqhC03ZFFSNgTBgq4Sz2ZDHFCWF9oBDJCi0ZL0oGH0GeSAblSZKoqx4frnDB0GxjxS15+nVnptdj48w+nyG/Ok//Hu89/gRqd0TXUtc1ESh+f1/9g+5+9Y5/tnX/OTrD7nYX3F+9w7zIhsX+eBx3UsaI6mWGpDY4Kaz0/Hl19vsXxMFhy6zcrwXSF3jk6JzgqtRcHWwXLaerQ1YIYkTg8pZh0SgySZtymjidGZLctqLlOI2nk8JiXUu05/HgNfZcHW5ajifSx48WPPum3fY7Ad++O4Zh8MNTSm4c6LBtyyqgrcf3ufkRPBya0ki5lQiN5JwtN0BbwcWs5KmMhzaPUUyGLWm7SyffP41dgx4Y0BqJBIjFY2WrArJfFYzrJZcXNzQ7ToosiFcGsFQEOzIbntF6DsaCTZFpBC0my3VfE5jNJvdnuQ8xWRe7EbL1fWGdx+ff9uy+rvr79jlUySKbBaLCigdKaVACkeKDmtHvA8oBU1tGHZdfsiJ6FJTVYaYskPxOAwoKSgLgylLQsoDfe8C+y4bYyepcSnS946iFlgbaaqGZBZc70bs0LOoDVVTIlRi9AMiRGQxx0/SuptdT0iJ5WqJUQYfLT5M8i2tSWmkLAuKkD0kmlKhPbiU2Qhp8gAyQuBjYtMO7AdP5yJjAJ/UFHsmbqtaHh+Pm9U81BRFgVKGrm3prKeoGhCaSMrDFRKlTI6BLgqMzr1FVleGbKSdAqN/tZMVxEz4I+E9JKWz2av3MLFbU8yvLnrwQjCK/Dpz6sQ0kOdTispoHpw0/OiH36WuDVEGknz1PRw7LSaw9FWpmojrIisIT+/OqZuHVPOCu3fXSFVwuiq4c+podxdUdYEpG/Zd4Ke/+ITRJx6ezDFaEeosO53PSv7oH/yIO/fPM7AwGWMLIFqHbzvS6FBOkbxCioKEJmRmOUYnMCPOb9HGUpU1pIKvvnzJv/6f/oxvvnnCj/7Bm/zpP/8u5w/myEIgTA2mQqgKVDm5Zue+MjOFc2exOFlhvKG7Gan0jJQ0ypQUZYXWBVoblDIZPJtMWjvgigABAABJREFUCpEJlMgb9mPu8vHdm95PebyLlEcrB/FA9NdYd00YNmix483Hc0rdYFOHCw0XlzvswWYfBGUyMCI0cTIdTuIVu4YEIip8VByGyOgls0qDyEyfJPMdnKY+jVuwLLs/SjzEFjtcMQ4bXOqZnyyRRZ0jJ1WRAZSYCHYgOYsIkRQFMSiUKlCyAVGQhCIJdWt+nT2nMiCWYkII9Vr05PESt//L38+r9/AVBvGanPT2d+Pts5YkKPICkziSXIc93PDk08/46b/7CZ/86lO6zYBK6tVnJBI5NSxlU34tUEVmyyhjqOcL9i8CT5+/zHJlTV5uqWzOOp/NuPfW29TNjMPVJftdBk274ZLNbo/1UA2R/TAgRp8Xy0SkgJPVkvYQ2G6uWS96Hj16ACJxdX1D2w2cixLvNb5NtH1L22f5w8FpLltwtubRg/sElngipnnIsG3xg6ZcvkE39OxsRBwky+WcYdAEc84QAvv9gRf7nrNDhdaazp8TRGS39TzfO+azBm00MRSEYsFl1/LV5ZazszVmIRlcQzG7hxcFV9tI73Y0TcnFiy1D17E+lxhTMnYd25cDNxs4tCUvrwLdaNkcYL1ODH1k3xuCgLYD//kNzllOThvaXmCDw9Oh9YD3lvlyxdVeMouKfet4eXmBKitM2RCiIAQYRs++s5TKUJgCEoTg0aKm7XdApCkVs6Q4Kyr8bsvoE856hn7yI9QFyUi8C9T1CqUUn3+zoe8bRuu/VU391iCDz1AfTGhhSHGKpgscLVPGceDps2+oZ3O0KhjH7MYttUZNVLVhHBjdmNnRt5vSPMhIJTFGY3Sm1wIoJQnREyPEkI0fpXoNiBBMcTgx69ljjpjM4INHJ0Gjs9nYqp5TG42S6fZrM3Y56VMmlDSkNA2imR7ovEfI7LQZypIYJ8Qw5iKoCRgZKaSi1oJKCRpz9NUJoHKyQIKsxQ8DlTmyOwxv3z/nwZ1TJp+/jMcwbad5NdQdj5NjwfutQ0bmIqkqxaxsqOYF6zPL7qrj8mrg5S5wcfDsXGCMBpckMeWCNnhBbwOHbqTfjfi7DY2KFMdUiiPhUVrccIMSjsLU7A8jv/71J0ShKGudXX5V4vRsyTvvv0uzWEz+FxPIIFTWefcDftfBEEgWZNAkDAhDSjLnKcepzZhMPqXKJj9CJKSxRLXFhy1VGTGFpD+0PP/6G+bLisX6/BZgQBega0SRD/5ssBNyI3jUJSlNOZuzPjllNd/xnUcVPG5oqiVNM6Oq69sG6tZNP01OQhyNFrktoPnnchrUs2Tl1eSdEz+OxQSRECqi5IgQLYg91l/h7QYVLaWMmEWJUBXOHwi+zVTU3wItXmuPkiOEPSl0FIVB6JKkqqmZgOAHbH9AB08KguQlWlVIVeYCLs30tTLr6jP/YnrfjhyFV59NkrwCFl5j6b1KhpjuH8Hk0TA1a8DRGDJGJu+SDETK/EJxuy0vP/+CX/z5X/DFrz/HjREpNDEkzu+e4PwG7w1Or/j4Sc+1rUmqwiYQx9g1aYgiP9M6QO8cPnQUKlElRwgDwz7y5PMdbbslRo82ibfffUTTKFSlGdot28tLnj/9EusS1kbClJ7THvR0dmXGj4gCGwIha8hIaJIs2e8dh/0BG0HbET32jGNPwhNjwAhBqQSP76+plOV68wyjIvNGsDxZcrqW3Hz5AYdnL/n62UuevdjRxRJjFG4cQEBT1wghic4jhcqJEVNUbD9aDt3I6KAdPCFl0zlVWJAFm9bSJs314Ni5RBcTVhzpmFlDTQRNZqNJpaZ0iZx1b4RkVldoUWJjxCdwzmWDURK+Ulifgc4hCHrnmS+zWd1qbnj+5HlO54hLPv3sKS9f7vGx5PT+YzAjCYnvZvQHx27T8dXTpwilKcsK6bPzfxWg9YJvLq7pXaS1HhssohBomZAyMHQd6mRJt2vxY6AxKw6Dz2CL8zz94opTWXC+qIiMnJQl5UnJxc02a8e9RzeCQpRI36FCQjlLgaSSkpvrLVfX3bcrqr+7/s5dg/OklPAxYN2A1oblfMHY9ew3W6Qy+ChwNjJvGtpuJIRsdG10iRTkgTpKnAcXPVIqZlMijRA5sjImlfW2CcbgSdFwcRhJMdFHTxd7fNeh08jJeo0qTI7ODDLrl+OIMiVCOS4vb9BFxWyp6cdADIGiKKmbGucs1gbkBEa6MYPSSmcpiMj7kqNaDR/getfl5IsQ8dOGNCUmGZ5HpQwiVkbnxA3ywuZsuUIX2UW/Dwnvsz97mhhzxIDDk+gnynzKhplH81gB1tvJ3yHXHsgbVq0khVHUVZXTHfYtkizbiiLLSrXRmKKgKoppkRSJIW/PrXN5cxws7735Dt/5zqOsO5fH+j3VJHnsq26RcuC47JiYiTJ7KlXrhoflPYoSxv2Be+cKKW5w9sBsVoMwfPnVC371m29YnywoS4NRCVkYpMiRke99932UMVlOqqeewAf8viP2I8JLRMzb8RA1wcsJPEnIwtOPG7QZKcsZgoLnT274n/+nP+Pzz77ij/7B2/zzf/Uj7j1aI43I3gu6yZHUk68SCPCeo3Qzsz0EKSRS8iyXDaIugIKEyYaFKJTMEdZiMvKWUuRQLJWZJ/E28SpNbJfMdpQyIrGEtCVyYLRXeLtFpkBRKhQNRmY/NWE7ViHx8BS+vhixopp8F7IHw9EjSr0m18x9eSKhGSy0Q+Jkbrh1KojylYNiOjJJ5dSnREQa8OMNdrhhsHtm6wVFswQ9A9OAys9NdCN+6BE+gAeCRIgSJWYIWd8mQ+TYbTEBBJmFnI6zj5wCLo73229BB+LWq+r253+LufC3kYbbhaQEGQMpDoR+i93e8OVvPuY//tu/5LPffIntAiIppMisiqOPF7cgQ17OGpNnBJKmKmsOh2tEEpwsFygSRgrKQuNjwLue4bClKDSmLpnLJUM/gM7SZjd6xkPLzeEAUuFjIARPCh4/eJqy5OuvnvBcCc5OTtncbPjrX3yAkIrDIdK2idE7XAq8vLH5nHmym5JnJDd9hQobusM4RY2HLDmv7tA0eTEui5JD1PSdox9rButoRxjGkWdDwBQaqRpSTDg7QkzMR5NTbIRAiQI3kPumvmLRapJPvPHO71NoQdtvsf2e9gCmOGU2u0N/UBycpd06xk6gqPnue9+nH0YGO4D0lBYCBc1yMs1XcHnTUtcFbe9Ynp6ThKbzkevLLd5Hqh0U5Slnb7xL9+Q5l1/tKEVFKSu8CyQf8DahUPz+u4+5c/ecX//614zRMT9bMvpzxsOGxycr5lowHrY0SkFp0AiqlJMLV6ennJ3f4+OPPpuS5xTBWjYbj9L/hUGGCFPTnh9ShUChEDHdHiaJiLMjZ2dnaF0ybzTWB6wP+BQZRod1I85aUojT1lfdejN4J3FWouRRDy7w3pNiptjlxl3cPgTejYyjzjGUzmGMIcSIMbk8KSEotaZSknlhuLOYMS8VSgEpUxpjyjS/mERGR4UkkJcTMQlciPnQTFkSUhidkfEjPT2J/F7IiFIiI3xaE1XW1yVxi93esg+iSEihEDJRVpK7d89YLhe3yPlxFk0y3dLvb4+ZPEn+9vkip+lOTgeYBFUqZkVJXUtWS83y5Z71DTzbei562PuES9mUKRAyHTIJfNQ4m6DMhyApIVJAykTigPMbiiJr8T/++BO2uz2IKWpPKwqdeOPtxzx48yHSaJKRE1Vsil60DndzIPWeOAZ0KhCizBTAqIhBTABDHlKP5plCiew4rT2i6NkNL1g02dTTDh1PvvoCiJzfvY8uc7wQqsxAg6pAlBxjFeO0WCfC8cP2fQAbOFuuOZtNcUOTI7BUk1yEiQY4DeFHr4zjcyGOn/X0tRk1klMDE17VBpHHdCEiQjmkGoj+ipRuCGmHiB0yBJSskAq8z4aQRE8IQ6b186p4SSZpAwFSi/e7jESX2YdB6CIX/xSwfQeTj0iMAqVKdDnLkhJlJjAoN44pRpI6shYmKDEdEXmBEJr02vf06gfiNQT+FUj222DZdN2yGzLTQRIQKeB2G55//Ck///Ff8unffILtIwJFUjBvGhbLJRdXlzTrN3i2MwwpMF8tEKpkiNlwLT8VkZQsyQdG7+i7Ayl5HpwveXhnwXjd0++vEa5CKkk/jKTWMvSOm5fPabdX4AdKpSlkjTIJo0Q+A4RAGYNUksIYFAaimNJ33BT9pEhCUxeB02WDi3kAKKuaflgxuMC+60hCMHQtTz7/Desffh/rDyxWK+6fnlApx7Of/xW77QZSgZY1ZQldr3CtI4WQ/SmainG0BJeoSolWBaUuMGXBallwc+imolUgdImLibLKxmTF1YGbEYQDOovyAZsCMWQAN5qpgQuJZDP4oMm3eIFgZjSPTlfMmhlPLi44OJcHBaWZNTVVabje93zByG4P192AtZ6bmy3CfMmXX77kbNVQzm/Y7fqsjSSwdh1zDcSRd85muJXiiXAM7ZJdl1F9UVakI9ugNEQl0GVFTYkbXY7+1FBUglppVtWcJ09e0u0sfvDoqBFeopNGeIsYPPcfncN8ho+Sbgy8LGeEmHDjwN27Z1SzBcIX7LZPCK1jVlRoIei3e4ZD+E/v899d/1VcLia894zW4kOiqgpI2XC578fJeDebmGlTM5s5xtvUKs/NrsPXhkoW2KCJITDTFQmF9z5voYMgxRwXbJ3HB2iaOb3NoEbwMBwGpHfUOhJldhkPZDMuG9Kth05K2VC2HwNX1/vJwyqxXM4Q0mD9iDQ1o01stx3tkPDJEGXemh+3y4REjPnvHqd0JZ8ExzVMjvyLkAKmMNQGThY5kvOwb0kUCO/Z7Ft0OSfoXHuVNiSgqkqqpkFIyX6/Yxx7lJSkFCbwGZy1hJDfyxRj3jqHaRgLOeEiTMDp0A+5N0z5XIopSwellFR1NW3tAkpIZrM5WilSCDSF5o/+4H1OTua3yMrR8Dpv219Dym//myaAgTwRprwtRgqKWcXdx/cYbxSFaLm6+AatPVobBpv4y598yGAD79w9QxAIwSJVYLWe8/t/9EPq5SLXZpMlZYSE60Z8axFeIlNBTAaCJjiB9wFkRJlASC1CjDSzBiVqnj+75v/9r/8NX3z2OX/4R2/xr/7Hf8SDt04QRk79TAO6uV04pCPA4NxxlUAK2UF+3AzQw2K2IIwK7zVx8hdgSkGQU5a4kNnkXOip51HHhcT0fkpy5KT0CNETwiWbzZeUJZAsKTmijwQqYsiDUfQOoxKzIvHumwue3li+2UWsBJTOwNLUAwl5HOLFrfQhobA+sN9b4llFIiCIr9gAr3/M071Dsni/ZxxuCKHHVAXNcp3fM5PTJBAJoiWME4vBJ1JSIDRaN1OvWeRefGIfiNcAgiPI8J8uiY79XJYu3PJqXmeDTF97K614/Y+TxwBB9o3Dj4R2R3t9zRe//pB//7/9e55/8RzXB1LMi7IQszfYsW+FhFKgs4kFZanpbZavIxTa1Dx6402Kqqaqaup6Qd3UdO0B249cP3tBf+go65rFekWzmPPDZs53ekf0Eh/g0PW0fUcS5Pmtz73u0B5Yzivs2LPfbyEFttst9x+8SYiCYUygSoZxoJzPSLqmGyzeppyk97zDdgOud9TljHFwLNYVRZzkVrrAyIqr6w3Xm10G2JQipIIoClqrkSEzzZWUaFWjC0UnFWhNaSRKgi4sp+f3WdSK8wePefLJh+z2HQ/unFKaGj8OiELQ1CWnZ2tKBf1ecLi6odASqcDcrRgGyRAK6llD3VRZaqZySEL0a6LPElaBompmJGnobKLzhpdXG7wrWDQLDqFk7zWjmmGdoQ+BVV2zmlfcWS8pteB0NWc5n9FdnmAWDd/5ez9kdb6k316Rrl4QDi1XL14y1wvuGJ0lgFKxb1uEkpytJfX7d/BhYoylLI8TsfxWNfXbMxlI03kRkVlYhYx565jBOoFPES0yvdBIgS4M89mciMyFOEW2uy3RBaII+eY+DmeTeU+hzWSaV+RIJusIIVAUeTuYYkQrhTEarVX+cVHS9R1lUdJ12airbhrU9PvL+YyzRU1TKgoNYkKrM5UukQIE5O3GOR1RxDQxwFIus0rmbaxQaoqHeeUtrQQonbOaUYKgBFYKXlOlTUBNppRHKVAyYCrN+uwEXWSq06vDb3K5FUw0/OOmWvwtvXt6ddAkXiuSiSQFsjTMzhRlU1BXW0rTMjtEnm8Grns4oPLxm3LG9qIu0TJym9wrQciAVBbvr3DjzeS8PvKbDz/PB3rKgyHAcrnmve/9HtVsBkaCUdO2A/CR8WZH7EYYEzLpbFKVNEyZsCEm4nTIHin0+d5ICOlRtWMMG8oa6rogBc/XX37BMHS8/7330VWmtaGqTK+b0hKQMsdl+jw4kUBGbot6e3nAto7FbE3wkphMLmTCcEw1Ecfcz8lw51YWoTJdXEx+A8f7OglxrHhTcZuociKCzMwQpXp8vGYYn6KFI4WB7uaasfdYGym1YTab4VOW0bi+p5j5LDuZ2Db5XohEcqZzSgOmmiFUSSrKrMEkJ3n4rkWGCEGQokaaBqFK4rTdOLaUCSYUXk617/hNpAysTBD8bUzSJKX6bfrfK+T9FcHvPyX85Vs83m7K/H7L5Rdf8Isf/yW/+dkH0/ZN51emEnVTsr1piaLiwXvvE7eKP35gsN4glMLFgItuit3MoFL0EduPHHY1WkrWq5q7p3PC/QYm88SmmWOqgs8//Bt+9ZOfUmhYzWvunp6zmNUc2pbRjsQoJyfzSRqjBUYbJDpLt5LBu1ww1PQ5LafDOYSAVhKjJW5m8FGzawOokpdXiW8+/ZzYd1y8+Ia3Hz9g7FYYYZnVmkdvvMNvPvqC/eAJouBwsBRK0OgCgkPFzK7yDkSRzVebsqKqDc18zqypuNrt0FWFKWsSElPN6G3WQmpXUCdFujlQhUhvp2HL++lzlrkJ1Go6ogKVkSyN5r0H5/zhO28xdB2N7ziEQB8T7eA5vXNK1+94enHB8+c9GosxGbAK1vHxFy9YNAWIku6XX1MWAW0khUnsbl7geo/C0bcd73znMSKd0jQlu3ags4GYPLvNFVoXzBYV73/vO8yeb+mCJomE8wERA+vG8M4bDzhfrfFjwqc9zWqFTVmaVkjB/fVjqmhZzWfEEHjx4gY7eBbFjLI0zKuCk7MVSWrGwVMIyVxrVqsVKUY23YB2kd9d/3VeQ4jElMEGbSokiqH3RArMrMSH7IVgqgYXAk1d0ztL2/eMPnGztxxaR1VolNBoEstUYIMiJo02hnqm6HyWXcQkMKZguVzSv+wAQ1kWyOioq7wocD7SDYkYNLubDutgsViSyLGwq3lg9JEwxRzPFg26MrT9yOhyH9ENgdEmrE8EYvZbEoqYBRMYlTfTffB4H/KSRUxJTkhSSreyqLrQnM41pwtD8AK7j/RuYOgOdN2I70YoZqhyhjAuU66FomlGyrpkv98zDn2OXpOCqiyyaZwUWBLEqf+Y+hkx6UZlkpNZpKFcl/R9j3O5PyRlRqJSme0gpMQ5Ow11ETuOFEZx/uge33vvbepSgwiv1r/TWDfBDbwa4o4bidtCx6saBjBFsNcK323o2wvW6walCp483fBXP/+M+bJEF4rgu+yTJBXvffc97r3xCGEKhJJTyoEgjBbXegi5nsRowCu8g+ATiIAxHmEGROqYLyq0Lrm6PvC//a//ng9+8xHvf+8O/93/8I94+PZdhBF5eaBrUA3IclpYWFLIzCyRPCJlVqPvPeN+RIyCQjaIVGTTQnIsY16mTIbkUy8kp8QIoXWW+nJcTASEDAjlUcpC7BnHS/ruBYQOEedsrwdefPUpJo2YIifFrVenGJNTIUrZsCwMv//OCfbzkaeDwMkM3mcmsEQqcTuUh5A/ryTBx0g/OFLKfgi3oAfkuG/xii2Q5a4Waze41JJUYrY6QRSzbJqtj1H1AT+MhL7PeqMoEaJAmQYpJh8GeZRexL/FT5jk3rwybMy/+5ps4zWAIT8C8bdJCxOY8kpaepQxTZ5fKUDo8O2ew+UVH/zsV/z0f/8Jz756QXICkpnAvVeLoWOfHXykKDM7t9A5bjMkqOZL2jHRrM94qM9zqpjRyKYiFQW4AnvosOMeOTHFD9cDUmdwolSK1dk5wUcGV+DDLKct+BEfPF3bsru6Zr0u2e223H10ihst73//LR4+fhc7JqzwJKFQtqCoZ0idvetGl2VW19stbnCUpkELTRc9Ny+uSUpSVDnVJKYIk98ZSqCSzss2cXSVmthPMeKDJPU9IgRcU3K+XhAIXL54QlMq5PmaT3/1K8bhwKcf/5rDd97kez/6AVpHTh7cYWy3gGdzfcHh6oKvPvsEogT01Fd5/DRLaq0RRJRUecGeBHFa8ISYl8Gokt4Lnl1uud71hKQISXJ+7zNudjtuNluE1Dx68JA/+aM/pEwegyfYjv3mJfvLkfVM8p0/fI/f+2d/RHvxlMHU7DvNrz78mi8+/4qynnH33r28JAuBRP6/Cz1a5FhfqbNxPEJPPfl//vrWIIOLYfoDKUcZIihkjuArjCQkQT86RAiMXYfXgetvnhGFIiSRTfG0IpEyjTYE9JTPyUTHSzFrgEmglSY5R3foiCnhg80PbEz5hvEer7Pj8NAPWGu5STlL+u6dO8gpNtMjpvjMRCxy0sAtCynpbCyY/O0AL5koQrdArECgkClbqIgJXMlAhOKV8l6ASASRXTu9zLKSV/rF/DVJZNpYEhnwnc3nnJyd3HpWHG/4dGtM83rx4xZseFUMXyt6R638b2nhMvhsGsndt05YnlQUXzyjFp5aSZ70go3L6Omi0axKQWMyBTrnCSek8EjZ0feXKOnQquGTz5/QDyMInc0eQ0CXmoePH3Lv4f1Mk9JH3Vt+h+yhxe97lIWYJIWZIdC5uIWIDxDCazGIKheQPNBHZOVIukUpx2xWI0g8ffINL54+5e/9/T/E1DOiLFGqJqkadAXaZNSSSPSe6HzeyyQJAfxhpL3Y4NpAKWeIifHgo8wAw5FSOCVjCKmmcIXMsJAqAw0JMjvlaJgoJwdhISHlRk6IhJSRJDxC9yS5xccbnL9B0BEGx5OPPuZwc0lRzAlBESI8fvNNRh/QlQJ1oFlbVFHnz/mYtpEcKVicPyBUQhUlGJNjqmSCYLHbK5IbkDFT4JSpkabK9MMJQLml8sFtLFr+WQQhiSnvtuLfYtPctl0x/2oUR2DsyGyASV/ELWJ/20gKICDw+PbA5Rdf8NMf/yV/89MPcH1AoieCRCAlx2G/5eVLePT+GwzJcufuKfdkkV+pVhODBNCZXUOUBCd49vULvvz0kvOzM9546wG6SMQhJ0zEYQQMUtdcPZthxENOT9acLhoKGUnB4q3NPUpS+OQ4bh+kzPThhCApAImROVNc6gkd15m5hA/5lkhMEb2KGLLG9GQ5Zz5fYqoCWZRYJL1LtCFw8ugN2ghfXh542QUGK4ku8uDuPR6ezBFppF7UPL+4YRw9IWV5UZL5dSkpqEtNU0p0oTCFQiqDKSu0kjS1RSxmlBT0SWGsx/SWFoVXYTLVEoQQkdpgJIjomdcld5uC77/9gIW22GHDe/fXJFPyou358IsnXF+8BC3RhcRFkyOthogBSllhKPkXf/LPeevBOYWI7LZXJOHp2y1FrVmfFwxdy2boGJSiOl9z7/yU8yQYvOfmekPnLeV6xaAUp/fPmT28izQVUkiiD5AiIkVWizkiJb7/pz/gO2GS06c03deRRVPTXt0wrxv6bqCKkdA6hnEyPi0EotK8+eZbtDHwZtfyhtAsFyv6Q8dFoTlbzf+z9fR319/Nq2176mZGXRu0Noz9gHUpG2ohcB66waOTw4eEUIaqaNi3I6OP+JB9nlrnMTIxLyu2babNqWMUcMzMyBAnrwIh6A4dbvQE71nUDUbDybLiZK6JrsWFgq5LvHg5omRB8Ja6zHKDB+d3OPQdbd9jKk0i0LZDJh4kwTgMxGSoqookEjYIfEpUpSGJ3MMpLVnMF2w2Hd47XMhncxJ5zM7HWq6D1vZEr1EYpEqUWjKMDiWyXLKsClRZ4kQiqURVzTCmmEwfe0gRKXNClhTQlAU3NzeZvRAjzo8cE4ey4bRGSjnp2CUpgBSJoe9wzmVG2ZERK0HrvFkfxyxNzGuZiNGC77z9kHv3T7Mp8/FDT8cN8xH6Pv5O5NVXpcnH8NWvZEusRPAD0NO2FyjtqWcVyIKf/fwTdu3ID965S0qeGEZEDDx89Jjf+35OigJ129RF6xl3HckKNBUkRQyK4BIhTFT2MiKLkW68oKoERTnnsLf8+b/5K/76lx/w8PGMf/5/+mPefOdBZjAok5ctuibJMg8H0RFDJLkwxTUKkgd7GLEHh/SaQtWQygyYH/3BBFkaoVTeuk5DevZjODI8jwNyjhLEWJJoCWmLHa5wtkcLQ981fPAXX/HZr35JaTre+c4qB2ApiYqwXK4Ye4vUPbpMnNeKdx/M2X6TfTeYPICOyx1EJqX6ePzkcvCJddkv4RUjQNyyDNJxiUVCEAixw9od1nbMTtYU8zWiaEiqzn1UisRxwO52MGZPKzAoVSPEJMeVakpReQVEidsfve4BkX8tsy+OPZa4fX2/1dvH7CUB02MxzUtxev2ko1Q3ksKAb6/ZvXjOb372IX/xZ3/FzfM9adT5/hbTM3TcDt1uifLwnRlLKXur9JYkC2bz+/zFf/yMTz+9JITM9tZFRTNrKMuC4D1Pnr4ABI/feMzZyRIpAkIltMneHXbsGPqRsqyARLIGKSKru3fQJrF5+ZymKXnwxve4++Yj/NDzzvffB6GxNvD98X2s9fTW4qfYW2cjQpTs9h1j39FuW+pyjqlmhCDy15A5xJEsxbLOUZkq21XELG1zPgPLiewzGKxj6AeCHSEGTssT7p2cEWyPmwu0CATXEpEUGsoCrq5eMMT3ub58yW57weU3X+GGlgJPXSjO5nNilPiQGcdG5WhWVZjMzE+Jwhi00pDIyTkxZllJSkhdYYPASMl6PsdFQZKK07MTThaa3aIkITk7mXO6LKlkSbe9pJkXVPUM2+2ZV5o7pyVf/fonfPbznxD6kTRGXjx/STc4gnI8fXmd5a0pZTauFCzngrFrcxrdazOCmnzb/nPXtwYZks+DUhCgFRgtWZaGulDEkGjHgIiR4ByF0iyXS5TQ2ABJKFzwjMEz2hHv3NERDZly/FeIeYgZx+wK3PeZQjwOQ0beY8g1LkJK9tZRuK5qYsx/V2EKjMnsBQEoozHGTANffralUq8OgDgxC9QrlgBJkgUQ8raIICc33AQihld0peND/toRkXVp3B4U4vVj5nj+SomQ2c18uVxmqUQ6/jtMzrKvMRSmfyMdq+FrZi2vww1HND5Xzde+hCzTEBrqk4r3qgd89dkTChOQu4jYQj861lXBqtHURST5PJggIkJ5hBzw45amNDiX+PzzJ6QgSTESXYAUWCxO+N4f/gGqMqDF7YCeEsTeMlxvETbiHRS6yb4JU6a3tQnnmQ7czDLJRqAi6w9NQKge57c0c4nWgovnT/jkg9/w+3/wfcr5ElSRKW26Rug6+zFkzQfBZoBBJiBks6nQObrLPaFLlHKGlBUxZTReitc0iwJy1nEGyo46uyyTmGiDE7MlD88Tf2VivhBTZmKIiBQDyB4ht4RwRfQt3XbLxZdf43cHxt0e70fm1QJdNlxfH9jdHHLhGhIhZVM63UyJDDGb+wnpCLElMaDLHEWJnvwVYsC3e4bdBh0lRIWQFbqYoUxFPLogHwsgiaOJ5XT33N7Dr27KCTmfmBQpTff68b57Tc9//ONZhpHgb/1N+d9zhO7A5ptv+OmP/5Kf/oefE0eJiHoqpvl1CCTbXc9b5g28L/j4158g5YxCmaMpd35tUmCqKpt0CoUdIr/66As++vhLHj96hHA7VquS4HqUzAyA7mCxFpq64u7J48wmTgHrHckHpMzSiJgy2Hl7r97mdmf0QJCRcgFoo3MqTpHBzaRzko53Od3GR4FQJSGJ7CSvStrBEXXD1sLh5kDfHxjqJb2LPNnDLlWMCUSydFGyvnufe3cX9GPP802HFwNDVISgSBasiDA6BAIXFCJIhGUaGmB0EaENZd3gRcF8CdIGlBoQwuBDnEAolRVGPmBERERPZRSVETy7uOLjy+e8fHHFYCOr+3d48vKKq8OIKmYIoxGTdEqiKFSBVBJdVSSp+PTLF7TbPaVMuLHH2h6JJyZLCiPWD1g78vL6l6jSsD10BCRjCowu022vdgee77Z0v/xFBi1jvN1DZsZdotCKwpjMmpJHdhmZFSfzgNiYkrPFmhTIsbwnNSszo1CSAslsvWIUjtP7J7ynAnWz4HR9hvCBoW9J4dtpFX93/d27xhCplaKsaoahpxtGtKkRwDhYBhtxCfq2x/lIUc+wIeBCwoXMAtP66C8TOIyJhCclTakSoxvphykZIQpcyP4m+64HqXJEeD+QCkHXDVRFQwoK6yLOwUCFjpqr65733zqh31+yms9QqWA9q3DJMQabNbyDJYTIYbennC1ZrxeI7YA/5M2UUSCkJvqBQhmaCsS6gFQRblp6F5jgkSxZSPkZcjGwaR1lqamLgigSVaVZz0uUipiZIUnPTd/hrSH4gTjozBTVGikywKGlQk9mzrNmgfcWZ0ecs7f15NZwMeVB21lLcG46s48RupO/VhQ5tco7QsiAhUhpws8T60XND957k/WimjbErzMS4P8vY+FoViFeT5yYssrJtZ1gCaHDjhuayqCU5uYw8hf/8SPu3FmyWswZxj3RO2ZNxff+4IesTk/zkkpMy4gQcbuB1AWMrJDJ5L/aR4I/JpckiiZh3YHkOkyzYug9P/vJr/irn/yc83PDP/lv/4T3f/AustJkulhmdCILQOWUKOdIbvISiJIYIq4dGfYWI2oKM4OkCU4Rk8oAs8qb/1zj1RRxmMGH28UB0xBLRCiLkAMhbvFhQ3A7CAHpKr765JK/+rOf85Mff8RqLnjzzZrhTmRW12iZEMEytgdSUnTtDmEiQXgerpZ8cx0Z+4BLOi/4Jrkq5BoeJy+zI+DQjRYbAhWvWJa3W/y8SQHhIfZ4u2McDwitMsBQzkCVU5yqAOdw7Q7fd5iUl1BK1QhV35pRHk3FMwNnYlVw+0u3beKxTyGJW7DjVf/Fa/cdk+3Xqx4spsmAdOqLspeEJ/kef7hif/mSj/76b/jJv/s5mxcH8Grq0/LsBGSprpC30t3smUJelkzy8X5wVMt77NrEJx8/5+ImkJfYhqKE3noKA857rjYRozXPX+y5vNgikofkyfaOUDYziAmjNSJFqrqkKDQPH71B2w589OHHDIPj7OyEP4iJxbpEaYWpKpbrmlOd/dTEtKwWQhB9pKzmxKi4eH7JZx98xHJ5wtnDh8xWS5IU2OQZxpHoM1v9sNsxqxcMg8X7SAyCcfR4G/AxErzDjSP2MKBEZq8vlzWPH96jO2zYnCUKpTk5PaWsK66eP4V44GZzzb/9f/4/CLZnVpVURnK2XnD3/C5NpSDk2NZuDGy2LWPIn6EuyswKIkvxs1wuxyOnlPCE7H2lsiH+yWKOi/l7SQLWZ6ccuiVudCQU1gU2V99wfnbCfn/Dejnj7lvv5PSt6ws+//Uv+ebJVzjvWC2WyKRZLdZI3RCEwiXYtTl9cbCOpm44qdccth3CC2Z1xayuUBKM/i/MZFAT4iVTTmNojGJVKdbzitE6ur7NB6JQ7A87hNKMgyNJgyw0WhWkkF2BlZwqFmkyackorZACLSVlYajrGikls7rGBZ+Lj3fEEHJzKhXGGOqqhgh936MnPc3+0DKfz3IhIiC1pmhmiKLIxnYpTgBhnAyI8nBw3Nwm9O3+NnvCiFukS4icKXosT0IcbS+5HcyUSCiRjVHU7YkyjWqTE6+UgsLAermkqAzIyJFdf6TuxGNiwXRAIF7Rwl6dQK/93bxWCKfT7BjJdAtSSIGeV7z1e29SfPUcoXpKkdjuPfeWisVMo3G4NB1gIiCVw/sDgkBRznn+cst2e8CHSHSe6B3aaN557z3uvvEQ1ORFITJKm1xguN4SDhYVBVKUyKIiSY13icF6vBccc6SOGj+kQCrQRUBVHutbBCMyFdxcXPPhX/+au/fuc/bwEUhD0sUELmTJhJAyv4vOTVGjCREk+EjoHeO2I/SBUjUIWZJSkVksZI2deA11Fkq+lnDxaoOdf/6KPXLrlUC+zaKImVmgApIWxI7gN4QwYAfLy6+u+PVPfsXm5QveuDfnwf1TBgskj9GZSrq5uuHs7JxufyBSEMYxv68IYvJTzvcBazeZtVLPprjOCWCLkXG3Q8bp+6dAigap6gzMUEyUwuNLn+7vFLOlxLGhyghYxsyTuEXshchAUpK3T8XfCtN4VXhv2QzTlkyIDFDFoWX37Dm/+Iuf8Vc//jluABEnvfD0NGSQQRFkzfmDH5FMwWoFUEz9YCCmHHcbidhRYkdIMTLYRO/WeDXSxhXPrhPbXU/0w7QRyQ3VYrUiCEWfNDLEDDRGg4gSq2SmkcVEktnU8BWHQ772vAmOJmEohdE5HlQJQRBZsuMI2CRxQTKm7Po+xIT1sBs8+1iz20dCdDgfGZ7u8VFwiA1B1yQSNhx4en3g+baHUtP1HS93liFWhFiho2BwoGNkSAElJcOgkDZlGqvI8Wd9ELQ2N5KDSghdYATUIoN2zqcpVjff60fyqUgeRUQlSx9HrFmgTgrKkLBVQ3FmKHSbN7zOgh8n/xqJLApSYRAxYaRmd3NNOlyyKCSLWc26LjhZnzJrCsoyI/0ierrDgaqq6b0nCIFNkd6O+BDRxlDOKsaxw40DIcYc1RQnA7jop4Yq5MfaZEPQGPI9Gb3PedZAKRMhJVRyaA/C99gYGHygTJbDFraHDlLCtZZDGLP/j1Z5k/m767/Kq66zmeow9iASRWVIImKQSJ9ZjJUuGFzeQhWVwe2noffIWFJTzKPKdSwKycFLDtZBAIHJKRApe0xJY3JLIhQxweAj0pRc7kd2gyfakRQidT3HJ4N0CWU9PkQend9B4LDBcef8Dte7HVqC0BpnPX70GCkolKTUMCiPjENubkXKQ7u3zAvJ2N5w5+yUZbWkEIlvXmwJIW+uj5IFkSIBRTt6Nm3A+UgSGmMihfLMC0shW4JUJJPoXWJ76Dh0FkzJbLliGAe0MpRFTW8HmqbGWoeQAqXyYihGf5tqday52pgp3Unf7lr6YfithVNdFcznS2JKk4wipwoRHG89OOd77z6kNFMBem1wO8b2HXX0TL/Ksb5NcdGvFjjT4iZ5hHC4YUf0I9WyAmn45S8/4/p6z/e+/xY4B86iROSNx494+Nab6KrMywqZ+x7fWkI7UqgSnQwxCJKbzi0Z0SpiakB2SJHd5SMlH/7qc3785z+jnnn+4T/++/z+P/ghoipIRueFi6oRZENtfCCNY44MDZBsyqDM6PBDwlBhVAOpIAZFjIKQjsbPZJkouQ9CTNv16XNIk8GVVB4pewQ9Ph4Y+mtC6CDC/srywU8+4a9+/Gt2Vy0yatp95Ok3A7O5oaxKTk+zsfowDJTlnLKoGEfHaG8w4orHp2fcPO3YJUgUWcZK9kcj5GFZCJmJ3kky+oCNAW47+mmOgVtZjmAgjhtcd8M4diyXd9HlYpLdmtvNfxh6bHuYvA8USldIXYMo4WiKKaZNKNwuuPKsw/R+TTPGEUMQvAIYjgsZjuzl2+3Ma0yMV/lypLx4JTlS6HGHa3YvXvDxLz7gZz/+BbuLPSLmaq1k7ledFyil8D5mpvLUmqVpJmLymvMh0tlEbdb86qMLvr5K9G5JokIlTaWzGaiQJcnA7KRkMV+wmM8gWIiOGDyj7bHW4W2R/w2XSCGhx4DRic5eMlrP04seO474qJl99AVaj2iRgad6ViNVvteUVpiyuH3fjC5AFHz68Rf84ue/4u7du7z7e+/y6J3HzE+WaC1oCAitMGXFeWnQKqLO6wnEzP4UQup8/vrE86+e8PXnXzNvGh4+fkCzLAnDgdNVyb279/B9z3zVUC1WSN/y5JNAUxr0rGQ5e8CD8zNmTcFyVlGoSHA93eGQEw2BpikwMTOAhHo1ghujKYzBqfAK1CXmJEWZHTpjkW79T0SCqhasipoQSkIU7PYd33z5KU8/GtgdDrzx+BE2OaIbkMmhteB7P/gRIXjG0fLkq6cEkRnXQipSgM2uzc9NSnRegt6y3Q1UEpSKzGY54a+uzbeqqd8aZJBTJM1Rpp2ISBmpS0FTlWxbxfV+RBiFdwN9u2O7a/Fo/BSFdhx4vPeoSNadJDvJykX2eFAQnCOYvFHOzqUe54dMjfv/sfdnX7Jkx3kv+LM9uHtEZOYZa0RhBgSOIilqoHTXXX37vvV/3A/q22yKFCmAxEAAVUABKNQ8nCmnGNx9D9YPtj0iD8iW0GvpQVwLXuvUyZMZkRHhvn2b2Wff9xngJTQkuwkYtJJTYtV1qApd7Bu9DkIXqeIsofc92fd4xVC2klBXUTUqHkdkUFrRJcek2gjD7ti1Vk6FvRPBt9jjkOZdYWCD2H3b7l+hNvNAH4Shdzx+dJ/gTmDAcRtsQU2WbUXbhn53g9L2Lpxyqule7hQDFiA5BUh1ghsir3/tdWL3jMFdkc96Hj7oWHVKSRXnzQfB+0wMiXG8oe87VCMff/yE3SGRczXXeYUHDx7wB3/yp7guWo1uZg5ogfl6z/h8i08QfCQOG5yP1OqY5rklKk1agCDeQ2Pf+VDwQ8F1I1FmfDB0+2f/+DbdsOJb/+o7SBgMPIq9ySR8d+ya15TI0wTZCkadK9NuhkNGktCHNSIDqsGkGrrQUJbgKnA0fWwWnmpfLzu0/kaBqdjYRm2FufMjLtzi6jV5umbe79heZn75zqf87Htv88UnX+B9ocuOVx8+4t7FffbjDW7a48Uz7g74x4HoKuPtjml/YFW0GSwBdaSWG8p8YLg4h26FNhMZRMi7PeV2jy8BR4/Q48KAEqH6NjJ1WUGnwGbzpPUOeHUCUrTaYyoVp8uz5LjGLLCeABdcU1IskzWamzGaKeOO20+/4Id/+/f8t//P95h34GpjwSxvS8DGZXXE8y+xk7e42RmYpW65ZguIsWjiLbkgOLJzhDff5PHmln69It87Zw6g1SYTUEximaKDNNPlSlDzAghknBSyT6RQrFNdM4ih9Z56hCUzcqRvileq82jsUW8zxFUStQgFRxbHAUfCM4vjIDCJcPADU+xIeaZoQhm4mQe0KsVt0Gi/OxAZ88SvPr3i+e7AnDJXc4/r1lTf28h1bJxVSXZucjbtrTib6KKzY8Kx14BOSgo2GaLicDEQJOLUqHkWjM2Txy5lNuptdqhGNt05/UMhIWTvGFJhuH/Ldrtjd3tryUeeDeyrMI57tprQtCfUDdJXNCpluuEQHTfPhVXnOVv1rGNkJYqbE7JZIfOMtPGAQ/T0mxUPXnnIq195C62Z/bMnIKcxWdImmXQx4r2Qi1H/5slGc7rGPKklIWKJ9HZ7oB9WBnhHW4g5l9b9mFCMkFdyYRhWDHe6Er87/mUeDqXMM9M0shp6uqgUzeACXbR9rlt1rAtIjDgX8FrIOTHNpZkpmmTLOfMJkODZzYlpTAzBs47xOHWBluCWmq1MqzBnpVdP1o7tzYhmAzOTNyBCtCBz4sXlFb/3+te4ef4FPYpT5bDfM1PJbqLUQgyB1WptzKuSiL7y6P6GLB1jLuR5JjrI457OwVm84Cx6vJ5xe7MnbStZKx5ve5wK6pXKwHZSpnkkiBKlkNNMPtwSdWS1PqeoTavoHRwaRb3kmXkcoTPZ7WF/oOs8t7fXXFycE4On5sBUi00Yq2bQHWPEOzHJGkrOibPzM1IbKaqqBsQMPTnN7LZbaq10MdJtNpyv17zx2kNef/0+4upJUrq0l9vUiLsh60Qlb4871nkGDoiKeRvkPfPhyphOccVhX/gvf/VTLs4HztcDeTwQcuXs3oZv/96/4t7De+aVQMv4UiXdHghq+61WR0mFNBkw6j3EdQV/oJRbYjR/jw8/eM5/+csfsN/f8O/+47f5k3/3R3SbgRpt+oLza0Q6VAOaM3Wc0HnGFahZKVOhTgUtgag9LvSIBrQGG8moeorrTSLqpMmeFy+DBjRQrRnlZEcpzyn1hpwnalIO28L7737OL370Ib9++3OmXca7zpQcTtjt93z04Y71ZmC96llHAa3stjvunz8mrgJFM5dPP+DVV894/cwxbkcyoTEneMk6w7WiveJIVU0yoWG5cncOk9CRR8p4zbS/RMTRn903Ly/fGbtIsRGrt1uYS5ty1oEMiCzggm85p+0i9mZaHnLUZ7vTm6wLu3rJ6+8suSNrRkGbBJdWn9z5dU4Kook6Hyj7a66fPOUf/+6H/PjvfsR0m3HVshKVihNHcKAh2tc+Wm0jtb3e4upgkpVUMhJXjJzx8w+/4Ma/weTOKRrM+NqDqpKKt086nDPFDVSPo8e5HvEw1YFDmtDsTG5ounhCcURgvDV25BwuGNOOEs/xm/to2TLXiU23IWuHJPBOTc6Op5ZCLongK8JMPiQ2/Rk1KTfPbwjuE3bPXgBKrhlwhNCRs3li9b2NGi4lEYeeflgjriMnz09/+DN++vY7PHp4n5r/gLe+/jqat8wkYhyoaeLFZ7cc3q+UA3z5y18mDB2rVY+rhbOhQ3RGqGQt1KqoOCqKeEccemPkY0Bd0bZObPiesVS8TaUThSBiAEC1CVkxOpt256CLZpafCtRSkU3P8PWvsTuMPHn+nGFYE8PA9jBxux/50pff5N5rb/Hee+8x7jO+v+DBynORM1OF3ZgYP/qYrIGpwJwc7BKpBLxzVBfBR1yAELrfIqL+/zXCsuJopiSlsJsyN4fKejDKXBBh1Qk+CK9erDg7W7MKlUNx7DPM1W76ZZRQqcXGAcHphJPbPWiBZens51rI1dzKzaBEyVohKynMVFW6LhJDIKst/hg67t+/x+uPHrJZrZEYqd059Cu0Kp5MmrbUYtpq9QuKKKf/1LAuhxLULjLqjhKEYxdTjWYkahpCGuvgSKqTZeNoK8kHnFQ2q46HD84xtWdtL3/aBl+aYHPsKJ9KPTtr9c537gTF43tsb3A52u9UB74LPH7jAdFD2VeGzsZ+VrE54CKFEIvR3uZbVmc9N7c7njy9Iic1ulGt9F3Pd/7wj3j0xmvmROvshVTFGAOXO0J2zaRzjY8Dqp6cKtOUqLXR3rDCXbwhrD5mYp9xcYJ6S9eZ8/f7v3qfaZz5sz/5Y+L6zKhqy7hK34x31ACGdJggVXwBTZV5n8hTIWjAEVHvbTRUdXalxXOSO7Txqs5ZYGkIm2sjV/XOyV5gHrPWqIhknEu4MAGX1GLshf1uzyfvPuGn//ARv/rJx0y7hHcdNU+8eD5zfTVy7+FDtN5SslKraXnnKTGPM7f7iXF3a0WuCzgq6EiabxCnhNUZ4rtj4NOcmF48w+eKp0Nch+vO8HGNuoguMom7bIPj0jnJdhYtoDQ5xVHO0h5710RpOTEOU7UeAfrW71j4OKKFMu65/fwJb3/vh/y3v/oeu+sJV2zcqd3/cmQEgCfJwE1e8cFN4FY71HtUok17oR7/WO5Y2x5jG/YsA+nsjNl59mMkisMJtq+pUgWkKh2VSCWQ8ZLpKAQpqMuozziXjVJYZ6QWApXoTufNWFngvELwzD5CMG+MXGx8U8qVWczEasKT8OQopCJUWYM7o+aRWmaomeQ7G502TLYvKXQrZe0dcjYw94HcKV4mchFD5TH3aO+hiJ2XIoWFi6AEskSSOCYVahsLtnRcBEdwjorRFI/gSTu3UgtSEypGmVQVMpARsjjUFdYbIfiBdbfC1UTUTO8dSuF2e02g4p2yOxws2Y7KbqygCXKh8wY0DB42Dl45v+Br9+/xi3feRqKHYOD1sO559sUZTz/9AJFKHQ+EKPjQRt+2cX2umW6WkvHB3Nr9sg5EycXmqQ/DimlOzF3PGAOht0kqJVdKdey2O5x4itpc6S72xL4nhIigPOR3x7/EY7PakFJimqbGmjRmXXA2QltLIU8HUAhSSKUwhMC9VcetTtZB10Iu2r6uzNWbaTUG3GYRDjlD8EQX8SEyt2k1KkIqhTEV6+hLprTxfFdjAQe9F4I4nl5fc5hmhuDxmxXTNPPk+SUyRNQJ1ZuppM4mQ/DBE4Pn9Tfe5Go78cv3P6KUhO+gH4QHm56OA+Ijmx5efTAw5ZHbcW7SP0cRk4epCPtccEUJKJvoyNmx2xlo268d+92O3TyTtI0Q1EKeR5sSJRUnFeeU4LUV084aGbWSSjJfLinkbCCDc47tdsuKFUXNVG+cJ+bJ5LOH8cBqNZCmiSdPnlBypu87vvrlL/Pw/jnf+PLrXJyZJhw45mm6xKPTT9rPf9OeuGWH7XlSC1osN9I80ncd3q/45Tuf8MGHT/j6N1/DY3tK9MJbX3mLt772FqFrOSIOipB3I2QbkUcNaIKSDS/3ToiD4vxMmm/oewfqefb0mv/61//A02ef8wd/8nX+zV/8G84f3zeJZBgsD/IdaICs1ClRDxmaFj1P1RoMVcyg1HegkaqOUhpJD4fzJgEwINbZexZ3BF3EGQsZnxC5YU5PmKcXiGZSCTz5aM9PfvghP/reLyn7DMkhYmVHKUoXe0QrtzcHvvh0y8V5R786x3tjcRwOB1bDOXmauH72nIerV3nzwVd4MhZ2qgZ4N6o/xxDcGiDimJIypdLyHGNNs8gE1OJMTXvSYctut6N/+Ih+fQ9xHeqC5TS5kLdb6n6kI+CkMWZ9T5XOzB4XQ8f2DnQBGNq/78AIoAsD+i6i1VaZ+8024V3Ww6nV48R8JOq4Y7665OrpE376D//IP/zV3zPfZnztzZSS5gUmNu7V1WXZL3UKIEvtYWye2Ef2+8zmwZt8+rzyYjpDzi7wMhhTu7nh51CZ/cLkVcYYm6G9I4g14ioZZKa2+q9WKGqmh0mMnZjLzBQuSH1kJ2ueHRxlFsb9zPPdjiEWggsmdewcfe9xEoh+oHeBLna88uYZw/mX8H3PxfkZZ2drhr6nVmz0dGPkuDRbU7gLSJ0peiAlNeaQVqZZmXJAuofMDDy7zoTPrknzNVpmvIvksUAVYhwQNxDDusmROqokxtIAOVUoULPlrUqlOJBQkLJUa9pyVqFKJFfX2EO81Hit4sFXm8bjA65f4YMlYyrFvCRqxsfAuu9Q3/GgOgiBm9uZy9vEk+c3zGFNjk/49YdPKVPi4dkFb732Ohfna7b7xK8+/oTieqr0qDhK6NhrBx6Sc2Q6Ug14HFn+J0+XsA3YbpyqylTg+XYmxsj9TQAfWA/RqKZMPOjX+FzZNqftMoNIh/dCjcEMhVSNOttm6oqYU2+MgdUwmOO887gQmNKBWiulVLouMsSeoesZ+hUi1mHSWo92CDEEHt5/wIMHDyhzNiMjF80HIJu2qOA5SSecoUhqJb857QtKwok5gJZxNpO/0vTnx03NwAXfJmQsrCkVM2ehfW3nsTEZnPLw3hnn56s7nVf7bUvwO3HOF7370is+LdDTz+9cqOMF05e/x52NrU04CFG4/+gec5cokwEHtO6fc5m+V6Z0hQ/2Gb/4/Bk3V1tEHTUrXoRHjx/xh3/6J/gu3AE0BJ0L0+UNekhEFwmhs7GKEihZmcaZkipCMAZIcy3GOSQUXFeQfqbWa0KYqXXmkw8/4ukXz/jGv/oW5w8fQdejvjOphO8MUVah5kTeH2Ay7WGZC3lKkCHS4cRmU+cklOqoVQxAapMhAHywcY7OG8iwdPcX9HkZSbpcFTnW6hUvGR/3qFxS5heUlLh8fuCdH77Hj/7ml1x+fsu8V4bY2+zZotzejDx9csujV84JrkkAUBuls98xTpn9YWK/vaGmPRIEdKLmW+bplu7iDOl6cNE0kzjm/Z683dE1Z1sfV/h+Db5rgdfdCWp3A15bZXonemOUO6ose9+d+HnnuS8FUD0Gr3qE7NUStHlk+/SSn/79j/nuX/8DN5cHKA3sQRoTw3adTCCpJ2vPVFe8mCN7P9hUGGm60dY9UNpklqYHVPHtvlY0GIjk1ONxHK1bRXAoXmEWJTTgz0rximvj28RVPNlGzlTLBINWvNTWO6kEUZxUgofqoTiozhGcGnQRlBzMgyTVwCSOSQLZeWoxnpyLBZ8nKAnVQmnNDBcLQZwxp2pFnGeMgYSBPLpWtJSjjLOqmRonqagz9+SWHVLVkTVQsOKhek+VBpjp6X7wzeyLo/4VqKX9WXguuU2pWfApAQpOAgHzpQg1syZzHj19dLyI9twuOFzNnHXCxTrgXGGaJtOmayU5UC3kkrl/9oDVl7/Bx3/73TZay0EtBL9jc7nj8e1EaaOxxBv9WrDH1JJBIAZvRU0wimBwHoc24KjivGe1WlFVCcvM8D6Yh0auiAQOh9louVlJc8I5ZxNGglFTf4/fHf8Sj/2szHNhP5lRcQiezWbDKgQzSsWR0oRWiN6Tp4kYhD46UnDMySj6C41bam3Uf4eLNkVgqpnUEv+FjePyTCTQxdCmTjQzMvHgnUl+2h7rxOHCwPW05dnVDd957QLxgc/3iUP1OA3EYAV6dZaEppqROeIkYKOHZ1JOlDqycp77D+9xce6AkZIncqqsV54H54GUZ+YyodXjQ2zTZWyspjqTvFZ1pBxQZ6bLmYG5jExzZXaWRKOKVCU4aygEHJvVuo2Z3BBCwPvAsFohXkzzqxVVZVitCMETu471Zk1uBrqL0bIPwSZgOEcIga7vyc2kbBgGHl6c8+U3X2nj+ZaY1OI2HDF2KwLtK7kb0o6xzAoqqRXNo5lyjteIJrpuoJbI9777LptVx4Pzc5veIJXz+xd84/e+w71HD1r3X9AilHGmjDNBOqjRRv2lbEaPDuLg8f3MPG0J0SEucnN54Hvf/Sm//NUv+co3X+HP/9O/4dUvv2kStNChYUAkmMQlZ8qYqIcZnap5UFcQjWZU7RWnDtVg8ogijYRoMhUzdGwMXpFTt14UfAGXcZJA9uT8gnm8QgjcXmV+/vOP+ekPP+STD6+4ejbyYHNOqpPJ7JyjkpnTSBeEUgJPn46sN7dszgYePOwJvjLNByCQZxuzOW1fcH7+Oq9d9Hx6W6yJ0dq/xgyS4x9VYZozhylRaVM11Bpzqq2xVw7M0w3b3TVzqTx88Cours0M21nQzeOB6foaV8H7AR/X4M1I05o5SzJ0F05Y8p+Xc3uhTR2785yXjju51F3p7SLZEWlMzZrRNDLfXvH0k0/58X/7B9794TukbTJDVozhbT2/k1CkhXVb5k5YjEelMVR8LMQhUkZPP7zC+++OaP8GQ28eezizW1sk4UuOhio1eJKHKoGCxwtUXyGUIwhkiiNrGBetUAspjZRVT/EjO9fx+T6gqedwWNPnjs2woguR4D39HBiySdqCg5iELnpiWBPuP8KFyBQDafbI7Kltep44azSpGnumI+J8RVYGdNZ23nUQ7n/1Hm+t3yIER//qQ/Ssp047a/rgmMlEH/CrFdNUGHOl10BKnuAKcymIJLwmpJoRb2nXrWLxQBuDSpucE4w9qngKxrTSNrLY2LqnJmAJgewHJNgI4OQyE8KsQm1eYcV11DbmczvtudnPXM+BejUzuhc8vUzcvLjk9QfK2f3HJB3ZT4nPn9/gujMbaS+RJM2jzg1UgVkC+xzAO8L8P9n40bWZdUvxUBGyCjeHSqkTVDtJZZ6Y9js4j8SyZxM7bvcz+VAJ/QW9j0itzEUpaSZ25gBeqrnWO2c0rXE+mKauwBtf+hL7Z1sUZTocbINPhVXXkeaRw37P/nAgl8L9+4+4d3FhSBqQxolxv2cYOjSPOO0oObHfb82kCwtMZpJnG1Ot4LyNmVQMOPBOrBPvo8k27tDBl+DmWxIu0hbtEWhYNp9mPiN2Jh8/vk/XByAfJRXLQnpp/2ldc3GNRLXQa+485mUE/u7feqT8t+jQsAcrOLTd+drMN0ttYI2D4AsSM+n2mtWqY5xmnnzxjMNhomQQVbq+4/f/+I+4/+rjo5EcgFZlvt0zXe9wBSQEwnoDvqMWxzRN5LkYQo6NphLnTf8ZFGIhrgpVbwl+QkR5+uQpv/7V+zx67TVe/+pXcMPKzI18bxIJF0CFmjLlMMKY8RnyWChzQTS0iSYeSmjTLJRS7EI5Z9RVO+VyZDGIuzM1whaphQ6xYlYWGpyr4ArOzYjfIX6L5hv225H3f/6Un/34Q9772Wd89N4z7m02VM3mMF4V5wdyTnz+2ZbHr97wymtrSp3wocMHc7+P3YAbq1FB04HYe7TuSZNRE9erV1sHwxKDOmemF5dIVpBgIFscEB85Sjy4a8Z4XKh3EPnTWjwWmE4bavabCL1yYkSIAWyt8D8mc80QSfPE/vklv/jRO/zD3/yA55/foiW0IOwoND2oCkU8JQeK75npWff32BfPiCOrpzibTXHSO2rDumq7r5Z9iwYEWJejOtemuTTNr5bGvoCEZ5GKtPDcqJmKl9KutwUJpwbViFS8FKJqm8KjdE7xVOZSCbWiauPxRuAgyug8ozomF5h9IDlzfkcrrvZoydQ2naQuuTGCU5OpiHiS8zZiuAVK0TaKqTEeVLBi4/hZMOmWCqU2OqjoUR4DYsjEAkqKNCZD8yNRNRC2ZhsHBah0BvQo7Z5oa8oXxAWc98Q6E/IBR2HTRVIXKaUw9B1SPZsh8MrjM0Lw7MaRm5sDpSpd9LiaIY/s6Hl6m7ipAyUZDdFLIOaCdAN+9QiJM09vPqVMyz3bOjoacA46jWj15MPc5DK1SaFoJksQ9zNzmq2L3YDvRpCiauEwzpTaJDIpEUJg6ATnCl3322kVf3f8r3dc7829fKqOw2x1lEShUCgZnARyyoCQU6FmpTor/4fOIVLpaHGkdRoqiveBKph/SK1UZ5I6amZwig8OvFFSZU6INxrxwhZzzQR5WK3s9qSSa+D2MPHg0SPmXLm9fMZNEopmLmJHF61j6/sVkiemORNC5NmLG7bjTL8aGOdE7Bzd4MEXsmZyNqBFGVitOvpdJo/LHq50IdAPA7c3O4JzxGZ2WSQi/QU1rDjkAboLdD6Qs9qkCyqKMTbQQnIF7wMpV1artRW1CH3XMQw9wYsxJfuFJeR56BzDMHDY7bi6vGJhnKqC94F+WOHEGgS1VLRY6v7grOPVR2enfamBBXfj3cuNm/agO3+x/FyAUqhpT55umA9XxupyG54+3fHuu5/x+qsPCS4wzVu6zvOlr77FW9/4Gn7o2zRFR54T5TC1/KdDS6CkQk72HkIn+KFQ6g4flRh7xkPiH3/0S37wg3d4+MqGP/uLP+HL3/oqYTXYNKnQodJZJZkLZTdSx4zOhZorUsPR1FrUtRaDUNW1brMVn078kckJTQLolu53BSm4MOJkj9YdteyoJZFmz8cfXvPuTz/gnZ98xNVVRiQQhxVhGJim0WKbCCEKqpmUhT52TFPi8892nG16unCfi/sDpcxM84H12Yb7RdmnHbL9nDfOv8bNWNlqpYhaLINjg6I27CFlZZrzkVGJVPNvoKI6kect+/01N7tbugeP2Dx8FcJgclOp6H4kXV5Sx4SnwzkzFVfpQeJvgAWnVcSdvGH52ckk3h1z8OWw93e3OSPGrIRFvdvWbkHqbBKJ2xueffABP/677/PuP77LfJMIRJuS51uLpUlmwab8lQreBXIbdzvlmQ4PLuBjxEVlrhW6C64OG17sV8jqjBC6xjZWFs7GEUxpeYfZPwil+Rx4mrQqGJW/am3eD/Z3rcXGyrsBZYX6mTk6rksA7Zj9ilkC1a3ofSC6QBIPfoUbBkrNzFoYixIkEKQz/6zkMfaqI+PJCLQph5ahKn409qKI4p0eWSKKMoWHyMMzKsplDWx3AnWF14IoVFehwNXBpm0Jjlgdgwi9Kl4z3hWCJrxmoFCctcqgosHoRKqLZ5RNeVExmX51lVxb/txqN11yL+dIztg0lslmMpmJZgmgGa22F+cgjAj7KhycUjrPrvaUnWNX14ySeL5T3vv0kqHzzDnz9HpE4jnqB7wzuRDicWqMpRIcST1zFQ7lf7InQ9/BnDKqsRXShmZsp8z+cGijLRTJSs6F3W5LrSPeO/K4p0wwrO4T+s5c4EtiTAnX94iDkgp469qmlDjsRroYmQ4zIZipWcmFnBIlZbRWht7GDV5dX7Hb76mlcO/+IzabNWlOzCnx+RdfkKcDrzx+yBefT9w+f4JTGMeJw/6AeE/se7qhByeUYll5bTpk5x3RB1bRs/IdfQjMAoeUW8h1VjRJK1gW7Y1YSF6GICmLn4Ut9ODgwcOLI9XouEcdizVOe9cdxsQRhT9a4snxJ3eAT47F3XFja2VeVVRL68K2WaxzpuZKrRgShrEYuk6ZDpdQZ7xb8fzqhhcvrtBqjAcnwsPHj/n2H/0hEhyNe24TI/Yz44tbdKogEddvcN3KkLo5kWYbieq99ZOd83jvTc4aCn5dULnFccAH2N7e8t577+P6ga9861t0Zxdo7G1+cRhsvCQOTTNlN6JjRrKQp8I8V4J0xNghBNM8Fo4FkjRw6Dj7uX2NnCZc2ElZ0iyOHV5tBSA1AwnnJlzYorolTSNPP73k7R98yH/7y5+RxsI4WccHIqh1wlNSVquOUiNX1xNPntxw7+G62SQUnBRCUJzryPMNh+stdR6hRGq+JU17Uq2EzQUEkxCIKun2mrzdGk9EI77bNAZD08cdJR4WMhTrtGlDVhdQXdxpSYIxkJZic/n24lViDIIl8Cwovp4AGa1ImRlvb3jv7Xf5h7/5B55+eoWWAGrgjapv3TYz4FQfDRnvz3Cux/veEkivpGoTbwwsk4YAL7dPS6G00TyP36EBdw1EbLQ0J5BVF7WPvXMBRI9aQntqQJe+otRTU0wqXiBWu9ejKFErvpammbZ7rFRlLsoskLx5GCRxZCJJoDSQgBoQb0ny0XSsGojpVBvW4ygN5JJ23gWO7uzLdlDkzjVddgfVJhMSyl3fFl2CW3u+LNIhbyBp6y5K63CpC1R/kr5J2//E23pwvsOFHq8zrnhEZrITXIiUYuc7eCE26YMPAXGm+3M+ELqA5kSZAocSeHo9MbFmqoWqDu+UKBlfep7tQAs825v0RLH34QS8eKL3rKSjlsI825graoPcBFIVfHD4WWxMaK0EV8z1fPFnCZHDHEnJTEBLheiVGAoxCKvVb6dV/N3xv96RqzCmypQqlYiTjkzHtM+UeaYLnpotmu7rCOLwtbT7MxG9EqInt4kTioHUPlqHPamtt4KybJVVS4ubxoxzwe6riu1xzW4PFSH2HVUL8wGKd+xLZn3vgt2LG3716RNeTJWShdorazVpkPeR7GAuGVcLWWacD1xc3MNtCzEUG+VWIHh77+oiRU3ra3ujUXVJha5E7vUd2s8M3QZP0+t6a5Mm6bk9FLYjjFnIVVsXcTGfBrRymCZEEmHOxgQqhRoC1ZvmWELAe0+MHTHaqOEYAyEEpsMICkM3gJpcy3sLVHMyUzPNjTWREo8uBjYrY3DIXXB8ybm4+/fpOLIWF/CVxpIrBc0zabxl3F2x7j2VyI9+/C6lwIP79ygpoTVz8eABX/+9b3P28N7Rq6pWpYwJquBdh9RASuZxVWvBh4rvKrlc492BEDtqdrzz01/zd3/3A4aV8m/+/R/z7T/4Pfr1uY1887ZvogHNhbwdKbsZKcaaEO3stQjGkmmpi1aTR+gxDra8ZymgF4BhmTZGRtwBuKHWG0rZU7Ly7MmBd3/6Md/7258xHwK7Ww/SG6jkGkNYbDqTSUEs58tzRjGwe3s78tmn15yfdQxDhwtCLjPD6ozNWc/u2Q3bZx9z8eVXeLA+43BIFELzEXq50aA4ShXmVClVUb/EdivWySN5sulN+5R57Utfxg8bajCJiOaZ6fqSvNubJ0kYwPUmz3XxWG4fUYCX0ag7x5JnngAEPcZ0Ts87LsuWUDgD5qQxmoWC6AxpT9le8+T9D/jhX3+XD9/9gLI3U1nXvOR8AynNC6CxdSUwl0rnO8ZpJPqeMWU67UjaUYhUV7kZZxge88llx1bOmIcVJbS3xV2BNpZzQWP4Wm9CxfjkroGsCtRWVKtWpC7sJ2MXqhsQmXGhoF5I0SG+UH2mOE/qrHFmMk7PJD2eAXXmieUArwFfIqIR9cEYDOpabiVAoDQ6uYjiaoNzxAAGA50WmUukuN7YjtnhivGufPOSWvJASmvsiCOo0FfoGgs2aCFQiK0KrM7ABicFSOBOdYPWjFtyaRWy1Db4pQm52ihymldU9RGViMchOlMWkAHHrJmq1kobXcccIjlmVDLiM9lHDtKRQiScb/B9YHQ9uRrT1W0UUZsqViVa09d5Fo/a6hwlOGr0TP5/MpPhjVfOefL0BeNUcdIbmFuVXK1YEyl4ga6aY2YqpvOpuTDliotrfLRCz3mPiiNlWG3O8SEQ40SIgbOzDWlO9H3HxfkF29tt04LBPE+ICKWW4z3pQwcu0q82IEK/WrWRMwUVx26aydPMZs58/vQZadrTtRMXCHTDQK+VkJONvhRt85wLeJvxOjSjkM3FPXoytc5GFXadbb4GbB03BotK7cQ1in1LvalOqFroo3BxvjJkX+8ConfR0DvH3SLg+OnldLfLnccdXZCXDbf9rJbmLGyiuzwnypQhQWutWBGj4DQRu5mrzz9ms4E8F54+vWS7PRhS2qjE3/793+filUccs3SRZvZ4IO+SdQ/jQOg34CJUIc1TY4KcWAImkwB1BbfKqLuBekOMyjzPvPfe+1xtd/zBv/5TLh6/ZuaGoYMQcb4HHJoz6fYA+wkpkOdKmhXvB2IccGLc9XnOaGngi5jWUJaOpxOcs7ncJymBHuUuC8BwCsAgklAZEbdHZIvoxLgfee+Xn/O9v3mbD3/xnA/fu+TRg0dkHMUtRS8NZKqUmgmxY04zz57veetQWJ8H5nkyjVtjLgZR5sMOHSdYd+R55Ha7JT56gOs3jebnqePIfHWJlIKoR7oVYTgzaUnT1C5a1GVtntaUcGyB/zPrcRlPacaYJ0BsmdlMQ9zvJnK2oVcombLb8fG77/Hdv/keH73/OSUJqpbUViDGHhd7hIhzZqwUXThq+3WcKFeXxEeByamNuXQNNDmudY4JrS6JEliyi7KMq1U9mUvO7arYLXSn44U2B3PXTHzA2A8WxA2HMEaLOUc4Ao4ogqcStTKrHhkGipJdJauQxVgYR1mHlzaarWL+L0eE8cjoFbQV88cF2b57AsGOOuMj6KPHgH+EK9XMzuz81CNc6Rr9atGQv8TqWX63VltXFarzuGrbjuryCCucpFZz2a8djh6nPUhizDO5V5CJ4sH7RBwiLpjLs7iIBMzLIkScD0wpcUjC5S5bl1SKjaqkUn3lQM/zg5Kmwm3pOcxGOccZvBQcdDEwEChJSEmZs0MXEbIoSCD0PVI841jJdbkrLEF1Qei6wFw90zRTcrX54gLeVbroOdPfLvj+7vhf7wh9hDRT1QqTnJU6ZrpgYGf1Qhe7JpnIZtpMxofWeTYzI8ZDYU4JdR7VypgKgQG8PUbQ4x5jxpKtx+YMxDQSmOJCu7cb/XtOs42N1koVx6FkZi/sBZ7sRibXU5zjcqxsGyhytj7jNnnmXKBkzn3zRSmVgE3qOhyACmfna9v2nYEduSQqBeeMibUJkVfPBh4/OOfNRw+ZkuNmOzOlwiElcnXs9zOHeWaaZ3Jp+4u2uOnM22gB87UqWRPOCaVmJmzP6rpIP3R450mpErpI1/WEaPfWzdU1++0OVTFJllO0VnbbWxt1OSWCi0QfiE559eE5wbWd8eiP9U+PfxLtlrh2zLUcUmxst5RMHndQM11csd1O/OiH7/HowT1iiOx3O3wQ3vzKm3zpm1/FxQDirNM4TmhS676qjdRNc6HmpamQmKZrxN/QbzoE4eOPn/J//efvMs07/vw//AF/9Gd/zNn9+zZFwi3jEx0kpd7sKPsJTfaevesQHxH1NlmiFAMZDEE5xvqjD9WiBW/fQ2geGhPKjlyuqGmLdzDu4RfvfsJPfvQhH394yUcfXHHv3qto7JnHZI75OjHOGe+DvVdVahVi9HS9a80vazDcXM88f7bn/v0NZxceRybnPWebM3a7wHR7i8yXPFyveTFmUi2UpeG3SPpUjVldhWmu5AJ02vLsZjg9H8jTgRfPr1g9fszm4ato86AQraTtlvHmFpcroVubzFRMZtqiAXd0Nsf1c/rqDujQmhp303W989C7tmnIqT1j/S27/6TMMN1Sdre8+Ohj3v7b7/H+T35JGSuuWE0ljV3hfEfNI7lCVkcqGDtKOqoMxuLUFdU5Eh37uedqF1k9CGzTSI2Pef96xba7YJJAdZYXHJtsumQSRwjFyp+WE4GjNmazsf+0NSesiaW1+fK5QnXFvMJKMRDKS8txrIar0VMFsoA468ynKeGD4JyNKPdqo7nN4DOaBJRmQt3yv6WptBBsT58GrLt2+jSqoAScGsAr+Gbob596KdUMg7RG01SF6JSglaBKFCuwnSzgRTWzTp9RKk6N+Sw1N2ZqY6BKJbccrTb2gjYfOCce58xLK6ggRIomZjyzBGbNZkaNYxYldYMBpLngqlKdIzsPrJGY0c5zCILDJpWEsxVBDije6qUmE3ci+KW+6zylO5m//o+O3xpkeLDp0LThi2c702W51iX0HYg5dS4jEjX0ZAdjKhQcfnXBOl7geqMh+dgxhIHV2T1i35lByDzTDx2r9YB3AYej63p2Z1sb41MMacop4brIZnNG3/eGJopntVkdKe1zyjYNwkfCsCaXylzg5jCRc6aPFuiHYEYonXi62PHg4oxVF9nt9pZcentvG+dYS+U8VEreU/cHxkOihBVh2OAXj4mGei/dv7aS2w3oTuosUc42PZuzDl3cXUVe2pOgbTSu5R/Hny3b0+km/2d3rqMJnwV4zYmSUpOaVPKYGA8TmqELnenySoVq3d7gM/P0gpq29PE+2+3I558+JSfrejiBh48f880//AN8DG1Pdag68n5kvj7AbC723foMH3vAU2umzKWBgc1IVMScnX3F9xkNe2q+ZFiZedann3zG++9/xFe++S3e+MrX8cMZNXRWgEjAbFmVdLunbEdcKmgbAOB8T9dvTAqhjjwXUjJlgwXQxligAQze4Vw4ntS7nXldgJsj4FAQP+NlB2yBHaozl893/OwfP+R7/+0XvPerJ4iuKBLIRaihdaiaPtA565zmkggxEnzk9jZzc3tgWK9RhRg94pQYhbNNb5quaYQpUFLi+fWOb3/nD8EP0EbipO2WvN/hc0XFJnpI7IFwR8bw0sJi+VSnnMo1O9I28s8Zhi3/bBG1sGlkwenvxFnblJ0q+bDn+Yef8f2/+T4f/PIT5tmMcarzqOvw3uP7lSGoeNOqqnVXvApRHNN0ID//nK7vGO4/YNcsBOoCfKic2CecGvPAUY94LMB1uYdq+6zte1qP50Fp4LWqgQzQpDWnG07U/ByKmE/JkoSUaoyovAAv7dxX10wSa6OqotQG/lhxbmvyLoXkBAOZtvnlROZE26yLlGWZUKPWaVhMZ3T50FKPxbVrkhKjELtTouPaXuKa9lSAqkc2hWYQKagzcJLqGiqfDa1XEHXt8ygqayoFYsaFDVIStRayHEhdpYTB3DC84KIgIdD3Zg5WU2I+TDx7cUNxsc2Bb2JPr2QX2BfHIcHk1uxJR28KL8b8mGskl45SC3PxJM2UagiJw8Z6dW5AxDE5z0wxM2GM8RN8pPcrioNZO7JUChnVjBehlwi6+mfuj98d/xKOUhN9Hyg1ULG84TAlxDmGGK2Z0jmcj6QxIZIRMOPpKMTO/IZQR9GAiz37ubAdR0gVnCd0rdFicyuBJhN1yx3s0eYJYw0WY0TWCrmkJgEAvDDVzPU0MnlHDhG6DpwwaWbKmZArxVUORSjq8UDGsfGBwmxbQImkFDhbr4jdOUhBx5F1H/H6jHuryPrehgerFY8vLnj1lVfY3H/INsMvPnjKOCX2c2HKMynnJgM0V3XEmhy1JaocJYhNwuYW4NUv1QhVC9Ns0k0XAmPKR+ageNOaz+NEzeWYAHln12YaR6rCqh/oYs+mCzx8cMZrr95vcsblKQuIbGdcj/t4Wwh3YsYxTmqLicn8nXSeyPPI0EVit+KDn3/K1eUtb77xOjWZYe/Z+Rnf+P3vsLl3YcW6eupcKfuKl94oyclRJhvPLqr4UBnzNeP0jIv7DnEdl89u+M//z//Ki8vn/PG/+QZ/+hd/xr1XH+O6zuSizqjLOiXSzY5yO7b92LxEfOgRgvlTlUKj/7VNXqDlP8d9vkkjTtMPKs4dKHrFdHiGkxnvI8+fTrz9o/f4u7/9KTe3BdUVc40kPEWUqWRC15FUyVVZ+wgutQIvgXqQio+YnANHysKTL255+OCMPp7Rrz3zuGcY1jy4f8Z+vkanF9xbv8p58OzH0uKtNyPIpRjCUVWaCetyMQtCQdOBMu/Y3d5wfbvlj//Nv8V3A7RxqTpNTNfXaM543xG6NRJ6lLsMhiZzaP86ykl/AzAATk3E2hoYSou3y+PkGF/rS2xMNYChztTxhnp7xYtPPuVn3/0hn/3sfcpuJvgVRRwSzIByzpWcK4dkAIOEaIxf17HyHvGRlV9R8QxhQHEc5jXPLiOr+4HZw4vre3yR77EPPdoM8E9MjFMT46WPKdJyLfssIksDR01OX1uJ7ipVaPmOazVcwbepLyoOGj1f2r/t8Up2lsYUIOBwGggutEyosSZ0EWaZCfWpJb3kN5YfnSacnT6Dv/OpTOhq97ywsJ0thzpdtgU6qhQx1kQ4NpvEPAKP1A89GnY2SgSOik3sskknqoVKpZzcPg3/cKc44cUTNODFGBaqgRlHITC7RFYo6pidkKKxZUs5SewVkFDNr8UJs8PeB3aOO5/RJiXW2l5XhSCCdwIxUIKNif1tjt/e+FErZ5s1L64nQ0/a29psLphTYZwOBA8BpYYVO608u7pFY0bDBvDUYk7iXsAFM9cy9Ms0v2fnZ3S9IdbRx6YBgqsXl2xWa6jKWHd4EaL3eHGMUyKnRNeZdMImFswopmEO0bT6h3FGXCB2AR8D3gVCF/Eh0MeBBxfnXKwHpGb6iwsqntxe/zwGYp4o04HDPCPiGbdbXhyuiOs1F/cf0K/XSLQiYTFo0uMG3VA+Go3LVR7eP6OLHiTfKVz/f120E1hx93tHytWxGroTONvil+ZRkMcZLRmpZoK4v51sVGUzWlSsqyBa8a4QB+HF8ydsNgM+BG5vb3j27BJwFgyd51t/8Pvcf+2VFqMcqEcTjFcHyqGYc3HfEWNviFiFtE+UVFu1Z52NJaA5n3FhYr/7gvMLCxtXt7e8/dN3GDbnfOM7f0h/dr/JJEyuIzgolbzdk68PuERzZ1ZwHV23IsSeZU50zqbJK2pmN0eAoSVBDn863bRz7lpHl/Y5ASQhkvD+lvHwOT7MeOf44smWv/2rn/D2Dz/k+fOZ7d6xWXc2C12XU9U2ufYKseuYU2Uumb7rGeeJZ093vPL4HO+tIFJNdBvH+dmKm9sd+bCjHCK3hx3Vd/QXDw1wUQfzyHx9jSSbJBD7tRmeStv6nG2lJo1RbF5oA1WWQNmCtFvW2csnpS3VE9hl37PfpXfW8hJInVYzenzyjH/83g/5xU/fI41CyQGlAz/guwEffDMQFHzbY5xFDkCp00jXK3l3S3r6Od1qRV5Ho/vLEkiWPWvJo/R4fZdrqixjm7CC3TWKP75pB+90HZyDNrLJ6m9pv+N0ny0glAAFxSNkhYB5LJxGoTaQoX3fWAyOKkpxS9CFZQ7H3YC2HI6FWXMXZJBTN2rZMuTlAKqLpIWW6NRqjznCn6drLzRzzxbgVPzp/LkTS0JE0FyOzAoVZ10LWd5LS2Br695iQIxqtTFhjT6eZM/BZ26r4ihkJ2hnBnax76FkYpfIs7I9zBQNZEB9Y2AIFAJTDczacVDlUO2+PaLwWokaKMXG1WYVDtUKOCfmmm2+cD00kCFRjOjV7o3OR9QNVBEKZneZXAK1xMl1HaP/7VyXf3f8r3fkeWKz3rS7u0Cydeyd+S1oKRx2B87O1tRgmti+71gNDu8rzmWyZjMZBZxEBOtS5zriYjQDSdeS6TbVapmcZXuJa2H9pNF22rTDCOIdXefYiLmtj7VCiJxt1qTeJFe1ZipDk4IZazOGSi+OVRd4+PABaTpwGFbgzXzy7OI+zgd02iO18vjhY3rpOBtWPLq44GK9IvrAVCuXuy2fXx747OkzXtzMjLmxKzA5prY9VxYmZ8vtDaNUpBRw5kuQc6brIt6HlteZTMJ5o13XqtSaSJi8UpsbvjhvDDPxdDEQo40NFeeIPtKHjvNe+PqXXuXh/Q0izXDtTgkHHOWqd1WrynJNbFcXq/6oc2EeE5IzpYxoOdAP1uz62TsfsNn09H3HPO+J0fHW177Mm1//muU5eGoS8i4jxdsIuOKoCfJsFHIfFcLMYfucvldiHJhG4S//X9/nF794j69961X+/D/+Oa9/5Uu4vofY2RQJPDon0vWBtD1AtlzG+w4f+mNuUGulNGyZ5Y87ySJkcbR3voHPiriMY6TUS3b7z9A6MQwrPv7kkr/9q5/w7juf8OL5TDfcM2NjDaj3HA4H5pwYtJJbV1pEKKXgfbRr25pgIUYcSm5sh5ubmc8+uebsbGUSIUb2+1vOzh9x73xiO16z4cDFMPBimhqVePFBsYupGNs6NbaZGaVlkIky3lCmA5999gkPX3uD88evtoYTaElM19fk/UggEruNsWUXCeeRvaAtv19y/N9YQMejxcClHlBafsXLD1zCefOTswwsIyR02lNvrrj+4gve+d73+eidX1MOFcHM7HNRUlFqqRzmYr3FuLI6ygWkjUb1LpBV6XvHnGCIPakqh6nj2VWHPAkMjx/y2X7FjjW5eVP55vH0Ur5xPE653hE8WT5WtZzJDKGXE9P8M8TKel1AIXcy7lRdZrwZ6FJdw8XEGgYiDqrHN+BusZxTbIJFFZPRNp9TjoafxxxnKbnuZPqC5X0vbw8n9oKc8oC7Z2Axra/2cQ3wQRoLol1SbTXgIslQ8/C6Y+8IKFXM9Ff93f6Snkq7ihl/V8HXxi5RkyAVF0g+NOcHx9zqnNIuQFtado2CTfQpx89g50iDIr2djIXVu/SrHCBOqD5QfLuov8XxW4MMh3Fmlyqp0lgLdhKit6IsuA1935HzTPYr9tOBy1EgFXyckCS4kHHeRik579CSCcG1JNAz5wTe/B5EPaUkXry4ZL/dtUtpwUNEmOeJaZ6YpplaCtN+35zWI1cvXlC1kuaJ4IMBEMCDR68RvCc4YZ5G1kPPWRdZx8C9szNczUwpc3F+ZnNH20mcUmHOhl5PpbIvhTEnihYONzdM08yDx4+5uHffKGnLImw3zHGhYFfNS+Xhg3ND5RcWAydy/vL4I4L+m1XGbz7mqLfQ4x/XQLoyZvKYqHOmzBmKUlM53rGiDtFWSjYHfecSPkzM4zUP7z2iVs/TZ5dMU6FWo8+c37/PV7/zbWIfLFESh1bHfHNgvNxTZ3Au0q3OcT4CQhpnGylZwUhIZkIkDnwo+D5zffMRXb8nug1pmnn7Jz/n+mbHf/q3/5GLB6+YRMJHCL7dvJBvd+TLHTIrmoWSHUZ7XuH7ASQgFaaUyLN1j7RRwI0O7loyJkfK4bK+j67Kd1hxIgmREceOml9QyhXiIh99eMNf/9Xb/Ojvf800RWqNttmrOdfjbFOJi8+Ds82h1krX94zzAa8VkcizZzvGUTnfeKZ5JEYBKush8uL5xH57S3/vjPc/+pgv/ekf4ULHQuWcr26ot3tchhDXxNUFHM0emyliA9AWB+U7uy539mGODsfNjFSX1rD+9zaY04JVrYiafnW6ueXnP/4ZP/7+O2x3mVw6JPR4P1DbZBBtMgfXQoLx8JvUoWacg6gVrRN5e0V6+jnDWwOTt+L+5IFyMnu0kZaLnOIuAmGfe1lHsrAwlgK+/eCoAhFpIMFyTuyaHJPVO0+rx3tajhIC01bb1wXzSSgIpVEpi7hjF6M2b5e7t7/hOndYInpalMfv4U6+nMefnJKDZT2CNGDp9Fn0pc/RXrhN/jmCaw1AUGtOGIPES5tcsXxiu7+NklFacPN2HwgUMXNfaSZQdu47DlJxkvEU8AXnjMI3Y++5uAE3BNARt1KYk0E0WlrXpLMxUM4xFSVLsXnTwdunap1R9TYmai5iiL1f7nFFnafGvkmKckvKTfrjg4cYyc5MLqsohYKG2K451NhZR/l3x7/Io8wzUwP6HNY0iUPPxWaDlJE8Z8YxQZ0JoRAcbFaeMDhqMbNQHwL9EJhrYkp79odqPlLSoUVxtZjZWjuWxkRpcgKW1xcI3tEF64w5tXHGnXN0opwHxyvrgA89Oo+88XDNI78iqZjG1ntSgRfXtybnULg3rOhU2fRrNHR0sWc/zUw58+Rqhrwn7bacrQYebO7x6sUDLtZnOOe4vt3x2dUtT65u+ezymhfbietDYsptdxM59jvsT3Otx+Kt9+YPIe4EhE75gNZ63ONC1xHdktYvv8PhomXdzjlC7HFiBfM0jgiOe5szm9LhIDjXxgrDvZXwjS+/Rtc1BuGdt/dP8irhCBwf490CNlehTsp0mMjTTOdnatoiJPrujMvrA5989pyz87MGghTW9zZ851//Mf3mzHTqajlQzZUYGjCQrbAuKSOuENeVVHZ0A6w3K5zr+MmPf8l3//5HPHxlzZ/9xZ/z9d//jnmZdQP4tlfNmXSzJd8BGJzv8N2A89FiyqzkVKltjKUA4k8TJJamy9Haz2WCH1HdcTg8ZZquCV6Q7oyfvfMJf/fXP+EXP/uUWnpSCpYLthjn1NgZkk2KpqVNN0LJKZsBvCyyAygqeN9RXWYuhUjk8y92PHx0YHM24PvIPI+oJh4+OGf3+RXlcMXF8IBwnXF0Vowuv7EV8qicvLdQ0ESdd6Rpx+X1Jc+vrvkP/+l/x3XmyaYU8n7HfHODq46+PyPEDYtMAvH/FBeQ5uF1uqPRondisByNhZeWzbEeOL4zbYXnceU3gCHDeKDeXrN7+pxf/uCnfPyzD5hvE6od1UW2CaasHMaJPq5QFwixR/xgxW89gSDOV1ypUCpRnMVa8Uxz5moXSS861vGC59qTnbfcRI0JIss7/Y3Pf6zKl7zibj4pTYXdPpY1Pq22OqoTtAGuLedQaWauy48dR5aXtLyw1FZz6Ik9Yfvo0pN35Du/w873krO1/eWOPGKR+p5aOqcPaQMbmjNO2zjubh1VF7l1a6gdk7a7QMPiB7bUewrcAfLaySxabXKZLhKTk+G4NeSXRrZSKQQaY8cLhUx11vwoeJOMNFPcIyN7uW4ORAOqVgrWJs3VepJpHPuNrQ6u7XnFN8btf7cGOB2/PcgwV9RFJEbm3diQGofkidfu3Sf2K7b7AwdxXO8Th0MmyWALqghOCy4nVFIrsITDbm8TGZzJF7b7PX3fEX2Hd4GSM89fvGCek40XA1yMBv6rsN/tSamAFmp1dMFG3Tx/8oSspgWOIbIa1jgfWA1rMxByMO5vOV91DF65WPW8/sojttc3xObDkAU0NTNENc38IcH2MPHsxRU3+wMPXn2diDCNE3mcqIONNfOxaVmAReevLOZiSueVRw/OrOgU4TTCUjlx1fXOYr0DOJyWCUeIaSl2FmMTbUn5ITNuRzRVdDaU0xWsq1+NAhNdh0PIOaMl46QQu8I0v6DrlBA7dofMp589aWOqbGze1779Le6/8oil66nq0KwcLq+ps1Krpz87o9ucQQxoUtKYqAmjZktgcZ13oeD7QtIrar3k7PwcRfjo489492e/4vf+5E/50te/jfQD6k0mIc4hFeabHdOzG2SsSLGRNWiH73rCMOC6ANmTZptmAc3/YeneO2lGR/5UaB3R8Pa1b+cVRSTj/AHlObvdp/SDJ8YV7779EX/9lz/hvV8+Z9rZhuPbdIEomZVToihePCVEgvNkZwZMU8pcbM5bUjpztu65vd3x7OkNfXdhM3wDlJyI/YpcE4dxT7ffcbU/8KdvftmuKQppYr6+ouxnkEBcXeCGdTMqaqBK08c5J1aWL8vqJVCs/a22nQnF1rLdfLYBwktI8EsZ5nHdZsgT8+2W93/+a/7r3/wDnz/dkssa320QCVT1FG2bmQhhmcsttka9LMWvghNySXgHpD3p5hnh5oLh4iGzRps0IaZJs835mC8eO/G1mjbTCD/1GLxc1TZO9/RZXjo1jQmgzSxRWoA0mqCBCEtiXNtzs0KhtgkUp1OkzhDm0h67oPpgrtLmRnz3mugR9DweL3UW9JQA6JLQuBY4T3uGqh7BAVk0lo2mefIdcUeg5e5zq57OiLLoBwX1vk2+WFgdth+IOFw1wGDRNApCdfampDYpmCoiHQnYS8U5o4N7bW70oYLMlHqAoHRdoa+R+fbaOrztOmcVqkZLGH3Fa4IQDBxQGwnnYjRTrDCDTKaLrM5kOtERfPN4EUFLppRMXwzE8CEQuh6TvplRme8KRQ2EcNgUARd+67D6u+N/scOJ4+bq0oBB11unZ6rk6Kh5Qmpi6BzBV4iO6IVpvsV1fZMJJILzdF1gReTqyRW1msEdVXGlEktl5QyArRnAo8FTgkedSQ8dHZ0X7p+vOFsFnGYclegcXpVIZe2Eh2crak6kw5bHZx7fB7ZjZs4CvmNXYPTBqkpVtFqsv7ne4gR2+x03+z0pV2KY6UNkCCvED1zvEqu+YzftuNrt+fzZc15c77jeHbidEwVHaj46NCDXextD7b1ruYF1fkWEruvpulMTZhonone4GImxJ8TI2frMCjIRSvNzibHD+zYFSaGLkarKfrcj+o7zzYaLzRnBmQnsKjjWPvHqeceqV77x5cc4ErOzBN+33XFJ3Jc9rmGkxwJBpBluViGPhcP2gOZKEEF0ZDpcmt+i7/nwgw+Y5pn12TlznfG958vf/AZvfuPrLFz5PGbKbJNovLNOYJqTjdYmE/sCcUTLns1Zjw+Rjz7+gr/+L9+lX8O/+uPv8K//3Z8Tz+7Z+F5nDAqdE/OLG8ptAxhih/MdYehtkkT1aK7klNp6A9r4X2NYNB8CFxq4JTbeL0yIu2bafs44XdGHNWl0/PDv3+FH3/s5n39xDXNg6FZkN0Odid7T6UyXJ9bWpaFH6Z0weNcYZSB4K3hdm3pgRiCEfkNJB3KFw5z54OPnrO8NvPrqCqEyHrac33vM+dBxffWMzRtfZRU8sZj8UI8CBsu1URsbaPlxQcvIfLghzSPvffA+D7/0JuevvYr4NmVpTkyX19QxMcQN3XAPdSYp0KVcusP2XDJD+2djKpclN2rsNu76gNRjbL2b01s/u8VEVzFZR0bTDt1dM11f8osf/pgP3v4laQ+qK8bi2OXMdp4JoUN8T+xW9FjxWHM1U1l14Kt1oUvBiZCyEl0gzwdiXJGyMo6evLvH7jYwDQ7x2dhCCHlpYrY3ecz6WtzXBSi5e0baY5U7HhONuVJbriJmANfWxFEvxiIdsYKX5vVwAgFUTxPFLMezgeP56HfWwARpIFNrIGqT0+gi5WrX0mPskdrykcW/StDjfrB8/rtZkbZ/e5U2rUI4vYWFTW4MBjmug6Nw/vj+XUvAVL3BIc4eZwyCNhJdaFMM7fb1CFK1sVGFogbGGIvDocU+vzW6Wmauxn4obXSt5aONDYE0oOEEENm+fuKgWJOI5hHEb3X89iDDJHS956tvfIkP3v+A4ODV+/f55le/zL17D3h+s2e73zHPM9u9jR7TI1rfdIWqzdXWkjJVxYmj6ztKrpRc2e8n7nb8TOOnOO+bBsW0K0CTQDhC19N1Pf1qIDYDnCDedJI+mIYlRLoQGUJHHz0PVpFVgJKueXCv53zdsb9WvPd0Q09JlXF7zZwqtVTGac/N9RXPLy+53u5QhH6aWK03rDc2w3k6jMQYqd4hzUjhxKCyCyeiRG+mj+Lukrbb449Fnt3UL1sU/cbXx8Vv/za6kum402FmuhnJYzV3/arUZAR41JDlLnYG8hw3CMW5Quzh8vo56/UAKlw9v+Xq6tbylAoXD+/x1d/7Nt1mjfhTh3i+PZBuZ2oF3/UMZxtcsJGRNY2YX2dzL5bQ3OlBfAU/kqdLHjzcEJ3n6sU13//BT7h4+Aq/96f/lrg+h9Ab8u49VCHvRqZnt5RdIVRjL+AiEjxh3eO6gIiNR53H2RgUYgwKRY5g12J0tBRIyD+lAolThIxzI+JuKekF3iWm28wPv/czfvi9d/j4o1tc3bB2hgIGL6RcOXPCeujxAqsuIl0kstSnza+jKuvVmu0uNWQ08OSLG1579cLMkoCUE8NwjvNCRvnlhx/y+te/Tr/Z2DKolfl2x3y7oxYYVhtcHMxQcNH7I0fK6JGlwG+yGez9az1tqkcdoeixKD3uXHAHbFjOW7WxtyWR9zuefPgp/+U//w0fvf8MdIMPK8RFymLM3u6R5RosdjsKxzvEeU9pPha1JoKDsr+lvnhGP6xZRUsfi5hD76K9qwsQBkdwRMRbcKv1zuc+UZZPusKWIyJWFNTTHanLu1a7jnXRELa9rWHVZBwOGzh0RJR16eg3kAFnTIb2PkWWv5f/0faUl1/7GNQb6v3yoZyuxwL+nAznDHBo32N5W6d1sQTg5f932REWhy3xN7/YBor65Rct+x2NMbF0gBZWkLbuTpuUIZZklBaE7aFKciYv82FFdT3zNFGkENaVvlTqbNpju0eEKWUQT7c5x6OUFuidE4shIaLY1A/CTMnmMSPeEbpACEYvVQVqIaiezDdl6cSaZ5AKSF30lDTzR7uff3f8yzzunZ1DKdzebvGdmV/N454X84HoKqvBsd4MhOhYrWz89Pb2lsNY8METfERwzKmg1dtYRR8tnGSFnKlpwpVg91+tOA+oUKvggza2gnA2RN64v2HwZgwWvaP3QkmJoNAJBB158eRTVOHRWWfMyGlGajU5zy7hckanwjhmppsDUjL76Hn14QUX64HojXX46NGr7HYHm9sunue7xO7ZLYdx4mZr4MKUC6nWZrR3vM1ZIgoCIbZpEwvgKTZZYzUMzYsCQOl7oYuh/TziYySE2F5fiMFYD1034EQoWk3n7Ay89ArDesWD83OGriOIEL2wjnDm4I17K87PA6+/eg+kNEBhKYX+mQxZ7n4pOPVoFuYxM+0n0piJzmMY4kxJO9ZDoJTKe+992mJAJZfE5uKM7/zJHxGHNlErVeZxxokjOGN21gRlrmYgGgo+zKR0TYiFEAM31wf++q++z9XNFd/8ztf4d//7/8b6wUOTijpjHGhS5ss9+Xo0gMEFvO/xQ29+DdWhtVKy+ZotSIprTM5jReT8scPqZEbciOoV6fAUrXvWq3O2z2d++Nff5+0f/IL54LjnN9TewKO+QlxFcsnQeS6cIqESQiQEoXaOsy5ANg8TM2hshb2a9ruIIl6IXW/jzX3k8mri04+f8/DBW3S9p84zWibuna+4vN3hy5Z7wwNezDBVK+qX/KWFbWq1RqSQqfOeMo3cXF9zeb3lD//v/6d5+wgG+mx35P2BSCQ2BgMSjInX7H9sgchp0Tg5/vvY7z+16Y9xlCUW1gXkWh57WnpHe0LNUA6U/Q3p6gW/+MlP+OXb73LYJqZZ2I3Kfs7M1TEMZ1bvrM1Y3eNIqRg1vxqab8WqSUHFBxoGSHCgdcb7zqRLhxF2e6JfcxY6RoQk5m0AjaH5UuOCY7PDovmSwy1Ai8VCJ3o8Ze32P+ZatoEYe2Hh5x/rJYyBIcKd3GxJ051d5CYzXRihd6WkKs6kFGIdfptyseSAHNd81bq880V1e1KiV1g8FLjz3rlzWRdswkyyLQ8S1/K9qgRVoDTW952Npj1nYd+cVoXcWWa6kHIMEDg+1bGwrhYvC3XBZGvaZE8L4HLM12s717Q8RZuX2fI4OX5/mahTteV27VIbk4KX9sv/3vFbgwzzWImifO3xKzx0ldXQ8ZU33+L87B7Pbw/c3F6z3W253e3IuRGexR0hKKNhmKHFOI4GDrgWXIJn0fPUthGaUZCN7AghcnZ+xu3trX2uWsjNF2CzOSN2Xfs9Ae+8Idpi7u6uZqIoq+iIrtC5wmYYiMETSexTYT5s2W+vyTWTVLi9esHz6y3Pr66ZpsQ8Zbb7W3a7LfvxwDCsubh3z0yZxPRkU5p5fnnJV9YDoc1JX7RJblmsmDyh61esVrEFvFO5YLW6nnjIR97X6cb7532R2w2ioAWmfeZwPVJHc4A/ud3ZotRaiTHgg2umSy1ZpuAkgZup047V4zNqVT799DNyqpaMO8+XvvFVHr/1Oq5rO1URypjZPb06yhG6zUBcdUiAWgopJTMcolVtzplG3BdcTOR6TRereRIcRr7/D//I7e2e/9v/4//k/uPXITRTQ2/rqe4nxidX1F1Gsse5CGK6Tj90hL4zAKRCTgmtBrC4Rv/WRpuSVtCeKqJ25xzpb9U2aZ8Q2aJ6DexBlN2LmZ/87dv8/Ae/5nA98ziubP5wFDvHXWCbK+dBqKtASpl1J6QgSDVDOaNgeVIqDOuB2A3M88Rm6Lm9OXC7PfDKKz3iCqUmfBQ2ZxsQx35OfOf3/8hQeJQ6z0xXV5TdRHQDcdiABAvi2FqShuYuhe6p+JTjl4sK1TmjMVrueNpRTuMcl+31N9aiYOhqzdRp5OrTp/z9X32PT371hJW/ALeilGAjAPUUOJxffBOk1alyNB80s1k5brLGdEp4HLq9IlxvePDIMakV7lk8VTxJfaP9WdKb1RIq52zt4+TIMLB4JaCLftIdz4cCWl0b37kAgO0wRMbOi5OjcsnkAQsgY2tMGzJs3RuOoELFUdS9JGfQ06ngaDradgLg5BOyoBG17RHLpdV6CvbcAXNaUFnWxAn3+A1AgtMyuUOWbMaaemRM2bZiwVSOEXoJgksi0eZSs1Cq7TyKnvY2J64ZI5npHaI2bUOEghU+QqROI76vrMVBTpScKNX0ls5n81uJ8TgBZzGZkxZPVCxwEjJRpOGJtt6dM3p3VbWRVS042/pY9q7Qoq3i69KDaslCS4Z+d/zLPNZdzxw7ctcROhsHPM4zOU0MvZk7OlcJQeiigYL9ZqDkjPndBJDIYTxQ6Qk+MmXoo8k0cx0RLczTwUYm+0DOiXmamVWJq4Gh63B9oBdPLBN9mbi/9mxWgRiEecRko1W4ublGuoH1eg1ALpXJZcY8MWsm7SbyPlMmyIdkXeqaQSJnnefeWY/WSIgdF/fO+CQnnt/skb5ne0h89vSGwzi1kZzYKDzg6JaOazTz02blnNHHtVR88ETfEbuOLgYDVVVRdXRdJAQbX+18+z2q1KlSa2XV9XhZmhLS6N2WAjjvCGdrog90XvDNcNdh08H2ZL54fsWjx6+z2qxO8e+UTrPsc8vduoAfNAlpzUIaC/NYKAmkBpwD7wqpHhCdiN05Ly63fP7FpZl+1oT6ype/9TVe+9rXbLpIhXSY0FyJvRmUazZ/By2K8+BjpegOrQf6oWM+ZP7xhz/jgw8+5q2vvMFf/B//B2989asQgzFDVQxguNmRrg641NzgQzCAIdqUgVqhpkrNteXgYuu0FRNm4L4UK5b/VdlS8iUlXeLqBCXw4buf8M73fs6zj58hY+H+sCFncJ2ni56QlPW643Y70Q+Rs96TxsoqCvjK7AqDr+RinmyCdWGra6MDm5l0rRC7SAgdRWd6H/jiyQ2vfHHJG2+e0XWOedyxWt3n7Mwj5YZHFw/5bDuzLYF6lDMs1qlCycXM49NE2m0p48iv3n2PN7/5Te698eYxpyiHienqBknQdWu8H0CbB4MsylJ3iq8se/2deInFQ0pt6622YlhPjZiX0qYlui4x0AwxtYyU3TXj9RW/+NGP+fn33+bmemK/q+x2ypghdgOr2LPuBvKc6Hwgl2x5jRhjQppCVFpe4tRR1GqWUiouePJc8b7iS6YetujNJV3Xc77ZEO/fY6yFQ82kZGN5c60UhVIth7HmzXI+7APedZKSux/1br1z5/yJ3M0wl+edGj7S1gwL2LAU8QvisEyrWV5frZbSxqaxMZ61jQ62JrUcEVJlIZ8IxuRZ5BrH/1kC2CQM9tpuKcqhmSBafUGtTWrtbI9UpcrSSmoj0+8cS4V3yrGWz05bmwYEVCxvtATOTCQXHm4VZ7IHwZjBmOR28U5c9j9roN593aPj2ukCLmANbc2K3vGiUEpjR/y2ac5vDTJEH1m5yBtn53znlXOGGHFx4IvrHb/+6FM+/uI5V7sDU7K+HW1RuIZiO3EUVXKaqbW2jo91mGIIVugdgSLB+9BoZUKIHZvzTWM0WGKa5kKpynpzRt+vDGAQZ+PjROlEGKTi8ozUkYtg2ve0F8LKIVnohkCNPWlMPL+84vL2QPWBp5dXPLu85up2y+Ewg8J+vyeXTAiBi3v3uP/gHrHrKbWS0kyaMjfbW9NCiv+NzWe5XRRRz/l6TRg8LIn0nQtod8dvdMJaoaCtKD2RjzgtEXXUDONuZryZqbPgMHOdstDAPVTN4JUuREppBDNdKDyJ0FUOh0u6PhBix2Ff+Pizz80Bv1bO7p/x1re+yfr8nCNzojr2z6+Yrke0CKGP9GcDrneoKGXMpFbo2xQHk5BIAOkrxd+C7ui7QM7Kuz9/n/fe+5A/+LN/x1e++R2IA+qjsRgE6jhzeHpJujngSzCAwdnoRhc9YdWZhlNM/3fXaNLJUuyddFTHW/p46heQrKHAbgRuqXqJllvKlPni/Se883c/5YtffYE7VM59R+gC0zTjgyflQh8j1WVWvti6yJlApUOtI982TN9mkucinG3ucX31BOcCpSqXlze8+urrOFepmqgkHr3yEN/3fPnVNzh/5VELYJV0e8t0eY1U6M7OCd0agq2Bo+7tJTBFjvFu+ZZNvlj2Vj1OYDBEV60Sr8t4ylZQtpV4unsVqYWaJm6fXfKj7/6Ij375Bat4QSeRcbYRRl7dUa+r2Lg2LUopBfUBodq4N4fRZZ0jiqNUiJ15tkgtuGmPu37KahDuP7zARU/GAMN9hpFAlp4kkVkDxTmKA9QSsyM2vBTaC+OqSZAWYEq8mVDaJ69HMMBu3RYMdKG+1QZI3E1Ejif2GGoXoNE8DgyRbo2OO7nI3cAsx8L/7lk/wu4L0q0toCy/QetSF5+C5xLsjm/vZb3ly3tQu84LQ+FOkLHwCirl+Hq1GhhhR2imqi2AL0ZrR5BkSX6d3W+yBPv2xzm7MZ3ggsMjlNICbOhNVlEyWitdV+37c+Iwz4CB1Mv6V2+mcqKK+orzzSysgQiqbY9qSYXDZnvT2HR2fl3DHpe9V0/JjxzTg98d/wKPeRyJzrNZren6vnmSZLLzDKuO4DIpj0jy9EVAMt47avUmGSSgaiNYpzETfcf+sDPDaecY/JohONI8WQwISs4w5sxcEkOt+FoZs7DPB3bF8/jBwNcfv85mbc+7zgdqyaSUKYc9PgTm7Q0+eNbrDQdROrECYhUcufPNOBVWQw8z3D/rWUnBz6MVJArjfkdOMzkXMonbMbGdM3Ne9sOTHMxox2rAcDAWUVUDsV0zcIxB6LrOJEg0UFddKyq1KThiG2soR81vjFYk9KEnekdwxl4ITgxgcIAW6AO0kOTFinWkEc1r5MU0c/7gESEG9Mi9aIfcKX7uQA6oIMVRZyWNmTQXtAiitmd4MuIm5t2VjZV2gY8//oL9OBH6SCGzuXfO7//Zn9CtV2gRyjxRxvk4VcRytUxOBbQaM8In8nTLsA7UKrz//qf85Cc/5/zeij//T3/B17/zHWMmOI9qQHMl3exJl3vzO5MOiYEwmKR5YXvWlKmp2MjKatfQLSMqF80gAi7j3ETOl0yHLxA5EAT21xO/fucTfv73v+T5J8+5d/6QQqUXG6ftpBKlEOvMIJVDSbgA0YM0Zbg1FCu+dV3FqfmEq+UBZqgsx3iUi+3LJScygs6VD95/woP7A+u+I88T/SrzyuNzdpJxq8I6ZnwKlDq07VxbXIGaMzonqu4p44FnXzzh9mbLv/6jP8H1na2KVEi3W+phpgtrQjyDlj8fgXg4MpSXY/EPOK6kY2w2gGGZzKXN7BjlDu6/xHWL9NImD2gZqdM18/aK9378E37+/bfZvdgzj8o02xSq9aqn8x0ej2bzJtKcEW26fhqm0aYA2qQArOPfCugqQi4F5x0lF0L0pGlPGDvcdqBbbXj06mPivQsOWphKJuXKPOc2srYyp8I8V+ZcSdWmxOjRx0pObIBjrrOAkaeGxm9OQlxy8ZYF2PVcQAPuSC0WMKE1Owq0+G3wZ21xubLkZnebKHdzXE7sTk75sNB8wVgaS0tDoj1n2TcEinJqMCzNoCUl8+7Evljuuzuvv3x915jcjqVWWT53PZ6XRe4q7XEFG3tZxJHFtakarnlHNWC35bfGetAj2/au2EU5MVjtdbTJXE+57Ilt8dsdvzXIsBlWnA0r5jGRu469wGdPvuC9z57x0dMrbg4zqTaDkPbqzgkhBPquxzWULZdC1/cE74kh0i3zj50/mX2I6fAsWDl8CORS6Vcro185R86ZeUwEbxS7s/Uacia6QKCyicJaEivXoWmi72cOY2WPIx12xK6jdp6chZodzy9v+fTpc3PrjR2b+/epoadyyf1797jd7kipsBo6Hj16TGzve7vbsd2at8S9iwtzO22fX2Shz7ZlpEaHenBvhQ8O2iz543E3wX/pCspLX8txEVt6b2OVlGk7M24zlECwNrQl+22BqNrolBgc3gspF9PlV20UxYIflNsXz7l33iPiubx6wdXVLVojzntefetLvPLWmzbzWR1aPPPtzPb5lloE7wL9ZkVcdeDtd+eUbNoDyw3nzLzYZ8TtKcVYDCKeTz78mB/96G1e+/LX+MN/++8J6zM0GICAOOqYGJ9eM1/t8Ck0+nJvQdULfhVxvU2eqKWSpkItIPh2lzewQeUlmhu0TbkVzmZklXHuQMpfUOsLYsiM1zs+/OkH/PpH7/Pkw+cEBoKzYjyKbXS+dWs85lYfoHXM7d++YTPOCSLGwhBx5KycbVZsNmektGPV9WxvZ6YxsdqYw/acDty7/4BdFV55602T3GihpsR4eUk9LFrCM5ufLdE2Q7Vu7slMZtnA7qws18rf5j1w9DQ8FsZLOdk2m0W7xelxTtSMHueZ8XrLT//hp/zkR79Ey8qcnotSmv7LOmA0LSWknKFCKoXgTMPce9vsowjRGUIbRJhzxodggKVmdHdNufSEs557r7yO6yOpJKYpsZ9nUhmZS+JQPAf1TG3MlhIQOXW3TcLVinVtG7DeKSDvBJDlnCzMD/TOBn0s6C3SqBjqbzT7ZeM29lFtbCKW17lztttOyhIKjgv1pST5n+E33ekMGIhxYkEJ3Bm3ewq4C9Cp+k9+2+nXvvQS8tLfuGVPsiSyqhyD/gIwLOF1STyABmS17scdBgkiWIOzASsqiDdvB6KNR67FEnX1Ga1tBJw405uKM7oo3p7nrFtmEk2jWlY1iZyIja/VJt81/wmP1mxX14M2vbmoaRi1LoairZ8gv2kA9rvjX9pxc3NtkseaEbGRusFDF3vWQ0+tlZyVkidydngPOc2kVCk5I6IUzaCRmkZWmx6nWzRP9N3KTK8RCsL+MFEpNuK2GlNw9pnOZVQ9VTKuBM77M846x8opt3kkH7YcDpngI/c3a6QWUpkZujVnUShr81kqbmA+zHQOziKIBtarFV4H1l3kfL1inis3+4nZwU4yz24OXB0mtvOefVZSKc1M1xJ2FxY2kOUONr3LpmGknHHOE2Nk1VmOV8pCM64NjzsBjN6JeTKI2EQI53DeEYdge7/zDN4xeGsceW9U5CpCVpsssXQWje7vqV6OpqydD7zy5pdYTOHkKAuEu3vmAjEsTNA8FvKo5KRo8W3Pal4xMlNlz+7mCy7OA6kIn37+wuKGd6iHr37j67z65bdQHGXOzLsJVOhCb3tKsQlf5EyQjPMz43hJ1wnBe55+ccmPfvg28zzxp//h3/OtP/pjwrBCfEAlognm6wPz5Q6ZFCGAD7iuww/RKnwcNRekVANDVZrhpj+Ziy6hymWQkVSes739GCcH+tjx/PNr3vn7d3n66+fsX8z4GogSSW4yTyQxPbhoAc1Nu20SaH80tKuNfWJeS1WKrYElPt75eslHaq2m4AiRXGcQz25XePL5DUN3wXmMpPnAvfvnuAwxTjzYOJ7uElWH1rnXYxFXciHPI1kP5MPIZ59+zlvf+DYXr73WKsRM2u6Ybm8JRLpug7RpEjTgWdvI6OruxC65EztfWlXLn4VLsQBswss+SncWYAMY0BHNW+bdFe+9/TN++r1/ZPdijysDAVj3nr4zr7kyJ7xzHOaJ2A2UVBHfGbAk3rr2rclYsUoh10IcBg5zInaReZ6JbdqYB4JWJE1w2JGvX3DzZMWjs2+yOj8jNMbSUi+UXEk5cdhP7A+J7X5kLMqcK0u7Ybk95c7XDXVpcVMbs5bfyGmWs9iaFa7lAKhN61I1fzl/Ai2LNgbBMX21wtq6+xxlWAayteJ9+f7ymm1/sik2J2BCFu+7dglPjZVTFr1cY3dsZurLn0MWpuqpubdMvri7I0kDXo6+X00NsLBUl6ecZA1mcpmUo4FwwVHbxAszdrTfbWBDy/MaQ0H1JNk/AcnHhWlr8zdyTnd62//D47cGGV57eJ8u9jzfz3yxH9nPE589u+TZ1ZbDnCmqzd3VTohvei9xDheMlYBzDMCAGQTFGIkh0MUe5zy0hbska94btVkaBcpJwIlZdEQvuN7btAgMc0SUKNUQ1hYUJXbE7gJFqb7g+8hhmhnWZ+z2M/vJZsreHmam0l43BmIXefhwjfeex49f4UHOzJPRDVf9Cu8dm7MNirDfjW0sj+fm9hocrIahIdccddiCIe6P7q9NmnUn6bcvTsn6AnkuN+eSdB+XYjPKkSrkVBh3iXyoSFmmtJqGXNAWXMxcyDuIMaBqI6ccvnUVzM1dmajlQOw2QOCjDz8lz/ZOV2cb3vz617h48MACmzryWLl5ck05KJ5IHAZW5xt8Zx3CnKzjQkvavfONVVGRMJLrC0QOxBC4vdrx4x/9FFzkz/7iP3Hv8WttRJPpXHXKjE+vmJ7d4JMn+gFcZ13vILjB41cdEhp6mwuaq81Odp4jf0yWKh9rixwLHG1/Kl5KA0CeME2fETtl+2Lk/e//il//6D12l6NN0OjMud45o0I1mytj86iY54W0iyru2Dm1wuyIk+K9US1zyjx+9Jgnn+0Q8YyHytXzPevhDB8qtIksab/HdQqaoRbmm1umq1s8kWF9Dx9XWGV0l953Mge1b5jFtNHJ29Jr50fUzoUu5+TORnpatS+naRYHio2A2u1596e/4Lt/+yOePN3Rdz27MduYJV0ot8ZuqkDObUKACs4HUq5tPJEQcUhtEzkQS0CWe8oJVQuSlcPVFbUfiA8fc/bgAV30dFVZp0xNlZwq45zYz5lDmdklZSyBrB2zeGpt10Rs31oogFnbzHpnsq4lETqFBttvVLXpIO2caPteUKO7CTSPAFt3i4zluLHfBb0WgOK4kS8Jzct98lPCcwIHjo9YkqBmcrRc9iNYsQTIOwneXeDh1JVZgAT7EE7vGBe9dDT2TwMRTP63rI0T+i1tbz4edzpBi8+DMTqMmrcAJmZAVVEf0TggrkNKscktWqBmbL62w7mOgKfWxhhaxrRBG7XXDH2zdTidd81B2zqAuhRXx7Xf5CXiiN7oyrV5lujyeZF2L/0OaPiXegyrnv1uy3a3Qymc3bswOnhwTSedOd+sTLkXjBnovTD4jsOhkpIyjjPzPKHVQakMweNDYGid95wqpVam0szL6mLqahOatDq8CcHoQySIcP3iOXtvTYLNaoXmA5vNhugi8zw1sBrS4YZQlYvOM1O41xe8Jl5ZDwwxcLHZ4IA5Ve7fe8AXz3c82yZKt+LDL17wxeU1+1SZi7ZOWOugieL9InEI4KoxTX1kmmdyNkdBVTOYFhEDAWoxw1Tv2/5tcdHer6fvOkIMhMZcDcFb0VMroZjU8I3zc/a3V9x/9IAnNzdsK9QGKqCV3JzqtXk+WfyqPNz0PHztMdroZ8v0oGMRKLQutOUDZS6kMVNGoNhoPLQVylJwrduf0g3j4YqHj15lu515fnlz3Dfv3b/PH/zJn9H1K+qU2d9s0VQYVoMZK1ZHTYWaKkLGx5nD4RnUHd35mvEw8s7bv+SjTz7jG9/5Nn/w53/O5v59JATUBbQK0+Ut85UxGEQM5HG9x6+CiezFQVZ0LtTc4sKRueCbhM+1/DKB7Jnn50zTc4KvBFnz8a8+5xc/+AUf/+JTBgZ6P1BIjb3SZHGumjzIzGha/LLruvwt3mQbToIBsAt4fwostm4AoTbwu3XAne3j85wpNfDBB89Zrx3D2X1CSTgp9D4z12tee/iYXz9JzLWNgm77clUouXDY3ZCGHc9eXHIoyh/863+Nix0UpWxHxstryEo3rNvIz8XH6i4sxTFWnmK1nctaaRLCVhguwByNBfnPhsyWcy7ywDKjZU857Pjg5+/xg//6A55+ckXvzxj8GnWKk0BtOVTRipNIqonoO+Z5xktgVFD1zNXk6Yowp0KMjmnKXPSOOStJM94FplQYuoFUKl3XmBE5kccdV0+fUNcbLvqvkpt8TKKNs+6AHticFzQXDuPIfkxsDyO7ceYwzqRSjnmMXZMlx9ITCNEK3yU/OBa57RqWdt4XWetpCkKm4I3BtDTExB2Z2Sa3rZRSSaW2OC246o7NAHv99rMT3HiHpdpApiXrbe9Lljx5ecOtUGuvzOIjYc2rluMtXgjLitJTBUD7CIo2SZutDSd3gak7IzHlJElRbHKLnWNpxuTWTD3udq2BqFUNTVKOeZfS2J93QJHlEpzW6ekeEMvy7zzyv3/81iDDWQxsp4nqBp5c3fLs9obdOJLm2pLzu7QOWkFtWKYPHhcCXpTBB0QghEjf9UddTa1QspqjsIoFGWeLwcYMBryaqYlv2tqFNOBVKePB9lcUnDN3WjzTrHjX6HzOUK3r2ytW9wSVyKF4xrmQpWc4i0w5cxgz5TATusiwtkkHIZgBTEmZabYxVW6c8N7kE+M4kdPM1fULdvst6/Wa87Mz1qsVXejMxMh7olPuX6yM4v7ShXz5gp623va1crwxDX8weUSeKuM+k2eQGvBqVN9SmsbL2x1YawFXiN4SpnnKJ/pRqdSSiF1lPFzRdQ7vO6ap8MEHn1rXToQHjx/z5le+Quh7wFES7J7fMl2POI2EGOnWK+J6QLxtvHnOaLbN1zUDHXGK+IS6HaXcsF4ZUPLuz37Ji8sb/uQ//W+8/tVvGMDgAjbqqXJ4fsP49Ao/C9GvEFlTnVADuOgIq94YFk7QXClzQWorHGQBE+yGd4vp0RGxazeMVLwrqNyS63Oq3hCiZ/vkhl997+d8/u4nbJ/vESINUsO7Ey3sWIstTIGGKhZR8KF1rdqaV7thvQ8E5yll5vb2li+98WX2mw3jwaad3FzNvPqKsOpah80rVefWgajUcWb/+XMYlWFzTlidWQbckFdaLIRWLOrCRqAZX5869LbW2nanBa0J8222LW7Rs1kheCqmXFukNSXy4cCvfv5r/vL/+i6//ug5+4OgXDHNpsksC6jjl1FZjnIc6yX0vqMWNeZCQ8VDLRSs0+PVJk4UxCi02L5Q5kx9fsXVx1/gNuf0jx6gsUM7wauwqpWhZjYlkUtmnGZux8qT28ScjOoHHNkeVU9FsYJ1ru+cU2mPXcYfLfhAbUGwtseLnLr4ZspjZ3MJKqUFqYVgyR1WxZIcn5boKXU57g56SmhowMby8+Vpxz93rrMef9td8OFlGcSJ6CPHz3liSZzAjJOZ5JJWnc7Rsu6Oe9od9tByWDKhy4I8FveJpQtp560AxXlq6GwPlSaVWIyZVO06uUL00Yq3WlqxYB2C3PwzTN7o8TEYo6uUpve295cXk09nUpna1oK0rqB9Xj1qxln2ld+BDP9ij1deecgzqdxsb5jTSK0rhqGjpJnDfk/fCX0XWoFlCaxJKpyN4/Ydc5qY00gfBqZxzxAi/dC3pLMe8W0IqIvtfmiyHLEmQa0wjYnx4DhMmZ1Uhk5ZrQdWMVJSZYiBMo94MkMf0VKY5kznItE7OoR9hMFHht7TB8+Dex3zOLMtlT56o1aHwKTKp89fsJ0radHuL4WUs/u3GzqGvjPdsat4v8gxK3NK5Jzx4kgIKQTi8idGAxO8scZsvKJ5pQx9T2wghGu+Lc7ZpB9fEmdeeOPBOZ/ur3jz1Vd4crslaSVB69gJWRY5hxWXohDJPLh3wcW9NbiZhZ59J9K3gtBYSXkqzPtMngoeG6GudYF/C0LCuYTzM9PuOTEK3nVcXV6zvd2x0KS//s1v8cZXvgZZOVzvmA4jZ2fnzQDSWY6bzEHbh0wut+xuP+fh43vUqnz04ef87Ge/5MErD/njf//vePTGm0jX8iD15NtDAxisqSPe43uPXwdc3zbcXChTpc7VRilWgSUHclYU2/6cQfZstx+jZUvfefI08N47H/CT773N4cWBuveEzcoYqD60emdpIBrbQFzEhc5AEOetiSCBLIHiIwXHXIVcPamaVPQI0os7xo667N9LHlWVLnhcHNgdDuQ58+nnt9x/ZU23CuR0YFgP7MZbXrn/Gqs4MpZCUb+MCQCUNGW2Nwd2+YaPP/2cR1/7OhdvvGGvnRLT5Q31kFgNZ4RujbgOFY+6pXN8KjhbZwowRo0JPlsRt4Rp0TZxYanj2gdSOBnvtRgiikhG80yZD+Rp5P2ff8D3/+5tfvWr58wH4eIskHqT4JaqlGL+Qbl4nArb4jgcMvOskAtTSyhSMdaeD4E5V6ITEkLdHui6jt12y72LcwMbGpgmav4mZR5RF5gy3PSf4TdnuIcPST6azNCZBN47Yw2GQTgbVqxr4V7NHMaJ2+2O3W7PYZyYpkTGJl3okiG0E2YU/Zab3Klbl38uYBGyyFUXHyvFZKnSUlxpzVuOybiq5WO1mDGv81ZTOo6DI9u+UY5SyaNngpzi/NLgcmLNBefcSd6w+HFhef9iA3D0F0NBfGto/vMAgwEuJzmIY5FkLDVKazi3taTL+1PFJsv5I4hq5pOtMa8ni7/FM8xesK3Bxjy9K9t4CVGwlX8nl1s+z53H/Q+O3xpkqNUxpUodYKyVm725ENsFbd1csS7SQq3zzrMeVnRx0cQuiI51+5y4lsybWU+t1eQPXTB6qxj9Xrx1M4OPdF7ovDfgFKXW3BCv2pBSbDycM7rIsepToynXmplcx9PdRN8PbAskzF9CqOS8w3eGkKszWvHNdk+IEVWYpol5msg5Ebyj7wdiiLjgqckW9Jhmbr+44epFYDWsWPUr1usN682GYVXYrPySjf+T459Lv49/L/l59eS5GLgwVrQ4nIamLTLkFjh2XqEivuB9M9IVbbPfLfZqNefd0MH+9oqzTUBc5LPPn3D54oZSPN0q8sqbb/DwtVcR8Wh17C+37F/soDh8iPg+0p8PSGwEw1ypSZFmpOe8t3vfF8SPTPNzQl/woefDDz/mF798j69869t8+4//lDCcIT5at6LCeLVl//klYXZ0bk1wK9R1qLfPFtcR11lgEBULsLlN1jh6EXAqqN2iR4QFjhSpODdR6xWH6WObg+47nnx8xS+/+w5Pf/EFZVJEe9M9ZrtprQkVUHFUZ8S0jNU2M46SlN04E3zgxe2e7WFis15TqxKCjc87HPaM446qI1oLjx7e55OPrqlZ2d4mxn1iczaQU26dWo7F03h5w3S5ZQgb+s19JA7t/TSKfyuajgGPBTVeirIZbfpBrYmazegsl5ngha5b4XyHaDgmnVSorRBcJsGghXG/5aP3PuH//Zf/jX98533GOTBlz5wO1KKkrOTaHG2dgYXLhJNQIbpAcgnE4UvGFU8osxm6FiW6SgzeqLbVta8dsTGcZC68+Pw5nD3kXtxQVh5tI8OCeJsf3UHnYKiFVVGup8/Z/X/Z+69n2bbsvBP7jWmWSbPdcdeVuWVRVWiABAiyQTbZLTLU0nP/ga0HhUKhaIUU0SF2iGKzQQJCkWgABZSvW+76Y7fJnW6Z6fQw58rMUwVSl29dEZWFi3PvPrkzV65ca84xvvEZP5Z7bUKoFVOKjZqm4NP5nGhzITvB51phYhLIYcFWKl/vqSBQk7Qilo1vAmQPrf60qU4bAwfYoGzKryPHE8p+ujMf97B0aKyn9zlq7f6O5eXveIhwvEd+7S8P/+/kvdOvP+Xv/IG8duzp5O+OUVcZUGACS8r5C2VSEcQQVCLpWFyl4+F388YZc+HkYza1FcrJiRSuBNrb7OivSjER/AGPq63CRHAxG2lpa8v3UbKyJU94T4HFY2Hx28dv7ENFEp6qUtS1JRFwzpOiy1IwBd24J6VA3VZ5uUhC1w1o3WDEUNWJqopUpqbv9ti6ySZ3+MNarCVLgZwPiJrMSRXWVKXOGImhY1NFhn6O04raGmJSjOOIkgQhx05XRjLgIIngyn4kELynMopKNMZq2qairSv8GDBWoW1FQFjv9rzYr9gPA6FQbJnkfEqVuiNLX5VS2cSsxB/HEIneQcweRE3d0NQtTdVQVxVaZ+ZDZbPJo9EGo0qKiyjMJJc41OuZeZlUIipDlyLP9x0bUXxwu2ITE6Mo3MSyEEXU+YCnSamkiJbI40fn2Goa2x3NsafdT9AQBDc4xs4TfMpDCaWYiMMZWvSIOLQeEePYb69ZLuaQFDfX9/S7Hq0Mi/mCr3zzm9iqYdhs2G922KqmbovUwU/sSo9IRKrI5sVzrM0GtXerDd/77k8IwB/8g3/AO1/+Crpp8u9Ghdv29Dc7xAtaTAZqrEHNDKqRrMOMZLDdpRzdFzKQipg8EhGFkohSjsSezeZThmHF+WLGsHN8/y9+zPs/+IC7F2taOyeGxBgsWmmCmeNUg5MAUeOSQkeNJMXOC4yR7ZgY/Uiwkfs+0hMwWrjrHFYHhiHQ+8Q8ZSM+Vc51SrFMJ6U0i6V2VyrLbmLAR1itHDfXe87OW5wbaC1YN2LNyHkDm25kTAISD8zKfrfn5noN1Q2DC7zzta+j6gqIuN2efrOjtTOqepFlEsrk86QnTf7UjE5t3hHSVzEgRJLKcctlWkqKjomJnC+30zo+SyjyvhGBgThucV3P+z/7lH/zP/0lzz+8ZrupCEHoosOYXfZgCWSGlFLEFAHD6APSBQLZl8SFiK0ULmY/DBULkDgGmqZlvd3woF2QlKV3YGzDfvS0taH3nlopGAdiAFUl/N0d2+fPaUyNr2cEpUkqgjbZaN0IVmUjXFEJsTXzuqJZzrgcHF3fs9nuWN1v6HZ9HqqkYy2Vy6wJaJoecpQjCEfG0VTDK/LapPN9rJTJ6YNKFY/NvG7l9B7BkL2YjC2MmiJdmYY503ebPbqmvoBD/TP5mEzxlxlkUKW+CyW1gsNnOpVNTmlTkxxVlyGSFHnH8WI61g6HOk6OcehSXiOW480gRKkXk5AKRSF7LkwMjNMST07K07IGSgFSVWHingBip1Lh01NC6SlPCKj/ycdnBhlc3dDvB+5X9+z6Hj9RZssNJAjaWGydaTskWM5nuZlKJSKk0EtjofGEkLXoIWbnW2NNjsNp6pwrDagkaK2KPs9QKUVtBEtAxUASTR9TdpFPwjhJJcoCJlEd0KF8HIrYtGwjbLuRKAaq4j4aAkFVmKrGmHwhpxgIzoGp8msa0DFrFfejw4WIEsGFQBgddV1hK8v9rmMfHXtjqHRFXc9omgbzIFLZf1hYBMKBk12uyUIOOnlM2FLJOw3C2Hm6zYAfExLzpp0XLSFIIOCzXEWEnKEa0EBVGcTkmWEILjdlZdKnxCPKEeNAVeXYpY/e/wTn8iGeL5a8/e4XqWYzQOG7wPZ6S+gTBpvdnmcWO7dQPlssZkMKU6bWhXqoR8Z0zzismC/mbNdbfvyTn6Hqlm/8wR8xu3hQWAwZNXbbPbtn1+gRajNHSUvSWXsoBkyjMK0tdDJILjtU41WGmMuCM90/THE6kiUlqIQmoGRPkGvuNx/StoI1C57++FN+9O3vs/70njDkHPOgAiKWUSI+aYYQUEbnqKsxU9OSgPMdqDxhv1/vefDgivu7NYwDzawhxIBEz67bsdtvibFDi8f1HecXC5qmZhgGFjNDv3cIsxwRRcqN0ehIu4Hti2uiTzTn55i6nJuySUd1vI4kRFIYc+EUHdH34Edcv8f1O1y3Y3O3YnWz4eXtDtGGP/zjb2AfPULaZV6wjMmLWYliUhRqWIq4oePpx8/41//6L/n3f/kzru/Gg7s/qkh0xBMk4Ms9KiSiFK17HCBFgpeix/T5u4p5sVMxszJtyEaQRmtMFLTSJS3FopNBOSGuO9gOSNAEE0s8WnZnN1plrwylCSGbkspJg5ymlTtNCLY6eCROSx6cbICqpCSkiRdQNKnTlCYFZEIAp9eQUxBBDu97mH4UsOz47aUjiDwh2eVgDqyGeET+/65jTSc/O42xPIUGXkOyjz84MF1Orqbjax8AluPjqGWU14/38Fu54E1TLlhGEkgpR5lRpkRBMtNDJr8Llb1kfDmGdKDgFuV4ygybA4imigFnmnSEsRiXJpRp0EmOAJEqTuDFmEGlQJXyFqm1zvdemtyhJadKHLqjdGCt/Pbxm/tIccQauLiYc3ZxSZLI3e0t1mqamc1xywpiCHRDR11XpBjzpEzl/bsyGmMVPrgcwV3nGzeGQGVq0FBXiWpMeTKJR1Ke5i/rGgkjjCNWTayJbNYWo2QafAyYMnGrbUVdGyqj8QmcSohYQlKs9nuMMtTtjAS07RlJNdxt1qy3Izf7F7y43XB7v2G1HzNpO1HMGxXK6Pzv5KSIBLgUSTGgUTmeuOuJfU+lLItZy3x5QdO2aG2p6hplLFbbnOalDZXOflFyKH0mAKBoh5kA1nyPeRLdpsdLzcv1llHZbOqGylprOS6aqjQOGk+t4M3HD0BSfo4ceFrkSltIITHuBtwYSVGhUvFuSTkvJksgEyl5REUQR5KBsd/w+OICN0bu7zYQItrAO1/4PFdP3iR2I9ubDSlE2ss5Yqu8N4QcVwkj2nq2u5ek1LE8W+BD5L33fsHTZy/46u/+Ll/51u/RLJa5QUmC3/f0qy34hFVVBmKMQs9MNhHX5P0+FIlEAGIZ7pSkJSF7y2g9kuKG3fYZKXVcXD2ku9vznT/7Pr/47i9wuwTB4rRhRNFvPabSbAbFCs845CY+hkhKAWsq1tuRGXuGCD5ks8ZV7zBBmM/nbAfPos7GgT5EgkR8kRIGyj6RCutCl+tPFM47hjBSa0sIgdEJtzcdT97w2KZn5vbUVjOGDW9cLnl2u2NiAuYSw7PpNtyoe6qLgbe+8kXO37gCiSTn2d+tkKio6wWisg8ZcNjvlUwDlERKrgx2IkIoIJ8jxUCKI6Mb8GNgNlsgpkJMHpKlMsA6bsDFy4IIsccPK7r7W149vePP/+1f8/3vfYh3LaLmWdLXe0LYMYyeEGI2TJWJZZHj2MvcB2sdPiWa5IkxHUz1jW3ookOJJdqK3eioZmd0fc+iMrjoMDH7EvWDZ9nWeNdjUMh+h7vboGdrwlJwpiKaDKwom9A+MmrNEBRKR7SWksKiMa3hbDZjfn5OM1vz0cefEodTNmZhelIAnROmZQab8hBg6r8FioQ3//4kIEgUQFSKqXypvXJipMIYMot6GiBNL1jAgZTU8e+YarFfqWlK3LkIxBjybJ0jUBHzV5vrPk6Ot9QGIYZcv2nDNJ4rqGd5bjo09FPtl+AQy3ocUmVZ7nSuUoqH442JkvwxSUKP9eU0sDmBuw6Dk8P6OL3mVMlmhCf/9gQ+pHyDfNZS5zODDJ/c71hvO/p+YAyxVNf5/Bid86CrumYy+go+YI3Be19oZ5NWabpgJpMYSCo7FFtrqG1F2zTYyuYJZwIh+ywYwBBpEBYp8vB8ydmjR3zv/Y/ZeiEqjajIGH2WXRSNbqaYFGdwETBVvkGTlOY3a1hER6zSoNXReFcltGnKxSFUJlI1c2YLR993+LEnhJh9IVqNUQaNZt62jN3Ag2XFsl2yuhvYjfeEmdBU2VTk4FAydQ+vff0nDyFvfA7GLhut+C4gFJph0a6nmDcXdYgoKptBdBidMDpvsC74csyFUZEi2iSC31NZhTYV4xD55JMXhJCpN8urK9549wuI1kQnbO92+C5kgEEpxGqqWYWYsukHwKfshyBlAk7EmACmp7t/SVtnzepHv/yYly9v+d0/+mMevP05xNblRlTEwbN7eo3qI021QKkGxJCMgElgwc6qAxuQAMlHCIKUyztNJiwKJsf96SaKkjDKo1VH4o5u/5TmrKFRLe//zc/54Z99l9un96TYMATFLkQGNN12pPeJwIjznroShv2IMRV911M3NcPgaWvN4AL7MXKehD56std1KnoxT7f3jH5Eq4i1in4ceWwvuLp8wPXLF2hd5C1TV5sS54szdEj0T6/pbrfMZ5fUs0W5L4vJVgikNBL9gOv3MA50mw2+3zHutuzvV3SrHeNuh7g9Ognr9Z7VTvjFJ1vU2ZLPv/OEqplh6haMycwSSdk7gcJYT5HkOtYvX/GXf/Ej/vTPf8KnLztcVDQJ5o3OXhVaUymFCgEdJ9DPowhYSSg8MQZiyGkouvixJKVBGbzoHPkmNdpWWFNj6gZbVej5HDufoyqLaVv8csnaG2QUkhd0SOgqZTaEDlQ+oVLg+uaGXT+SxCJCMSySA3gAQijrbF7yCjocjhKFSV5xBIizyc8E0OemdKKHSp6QJCDG8lNVUHVIk2HV9Ian60JBzmVCySegoNACf31b/LWV5LTFPxzPhGqfvmZerOW138rvLUA2rzuyEY5Aw9Gr4ig7mYyWpndNHBGWY3k0IfMChByNJdl7IwMH5RdSwlHaEjm8CCRNTGHaVZj0S7nYmD5aBDSUyNcD0pPh26xXLAZTJfsNos9SF51p3ilGUKZoJcNhepFfbgI8fvWs//bxm/Lo1itqBWfn51SNxcdAbw11U6MqjaiArhTBK5wfIYJGU2nw3hGjMPaRvhsP+17f96iUTSOtzte6IsdqKyUMLjPSKiXgOipJLGYVF23LG1cLlrMGKw6rM3PLiib5gDEW7yG4rDnWYpjZWZY6xghhw2zeom3NbHmBMjUffvgpH35yTe8Erzruh0jvsrlpNuzLtF7RiqrOen8fAt67PPAJ4EeXfWrGER0j51rTNDNm7YKqqlHKYquGqmnz4Mlk7y0jhWJcJE3T6henkiHliOFUjM6QItSLkaAyyJJUBhNSoRAdGFupNCSisNFj8Tx+dJGbuwlhnCgaCaJL+CHgOk9MqqxRxYdBsquVkpjvcXxmjqgR5zZAxNQ19/cd67s7rNFUsxnv/s7vUDcN++sVfdexOD+nmrXZN8hlZqcEj1Ijzq/o1i+5WLQYW/Ps5R3f/dsfcvXkMd/6oz/i/PETUDVJIOwd/WoHDiwWo2xOVqwFPQEMJJJPhDEiUYihACmiSWIKQS+izAiyZrf/BG0HFssLbp/t+ff/73/PJz/9iDRoatUCHmta1t3Iy9WOaqm52zvizueUgpjp98TEvE7sx0joc5zwOASa2SIPFiXPiiqTm8WoJRuPCxijSMSc2CC5PrDGoI0hpcDoRpwbCWHEKUVbGfb9yGo1sLrbsThTeNexuLiiT5G3H9X88IMVKjUH02HvRrptx9NhxXKm+dY3v4qZaZDAcHeH33Ys5xco04Bo0sRcCenISnBD3qt9T+j3+GHP0O3ptlvu7+7xfZ/fZ7/DucQf/rN/zPzJGySzgFhz2Puner/U3KSR5Lf41UtuP/yEH333fX7+4w/Y7IQRTVCRyWsphpwW4EhlQJtlpMgI5GEsMaGHETSMvkcrhfM5VlenvKeu+55KLNvBU1Pho8DgUKLZjoGmrRmHEUzu35wbqYcev9nDaotLljCDYBKYhIoJUTb7rquIMjEnQCkwSrBasFojKIbBZXPKAuSkAmqGNEnOTs2pC4Qw+WJIrjFiChCLRFGZY/VQ+jytYq7xJfuNqUnuIFlbL2pqpCd2wTQcmKZIR+lwCIEQQmZvxanRzs8JsXjaKVVYDOnAgIoxkkI8gAaqMBtiOsbIHiEEXqujKJKGRMreUMVrYYJSAgkdp9fmMMBJQjb4LOELk9dZKvVRSqVO5PAx85pRqBRCZnMejkN4jWE2/ezAvPisNAb+M0CGp89fEUI4icLIH7CyhqZpy+ai2G23OflhGFmtV1TGYm2F0VWeNGpd6KmKqqoL+pQL57qumLUzapuZDLpsSMSilQUkRnwIDN5jTJ2NZ7A571cUIXn8ie45U1yK/maScqRjUXyqsUbp4jR+8gVMQOb0RcjkBVERPLjBE6IjeDB5lEGKiaaqsFHzB7/3JRrb8oMffkifEu88ntFO+szDtz1tqsXNQo4XIEkyGyMK3WZg3DtiECQZNAqVMhsh9ywBpMRKTWZ+MTMZjJ0aJ8DF3BxKviBjDBgjjOOOprYoZVmvNqxXG5KPzC7nvPPld5ktzkhR0W97dneZtpf1lYKpNdWiYhJFxRjzghIzGyHfmxGxgb5/hVIDbTvn7nbNL3/5IQ/f+hzvfuN3MbN5aQI0+MT6+Q1+O7KolmjVZt2fCOhIVCEbTBoBVajhMZGGeChE4iTSUtOdUxqQ8qdIROmRwIoQbqnnc7Q0/PDf/hW//KsfsXnVEVNF1A3XneeT+57R1GyHMVP+vcMAS58R50WrD1IUqzRGFM1c08iMNgXeujwnDCMi2eU5AiEGogS0EXRl2Q17PInF+Rn7bsvgRroxZmptnZeEs8UFjIG71StIwvJsCWnE9yNxFxj7gWG7Y313w83zl1w/f0W/HRnv9zTWcz6rkRTotx3ROyT4kukumPoBUXuevtrx01+84Ortx8xCMdUjZtSXmB1+UyJFx+7ulh9//z3+4i9+xIu7PUOyuAgqJM6rFhUzlVDKYqxjbtK9z/RNXZDUGAMpeKIaEW0xohBdQTXHtAuUrbH1HG0bmtkCbRtUXWObGqnrDITUNU4btrGGQaFsvi9tAmsiFk8XBtbrO1b3axJZkhVVyTsuXi9qAg5SARTKMaqy0CqVJ+8pTqKHvI7lezswbWUCxQZDTYsJp0AXyAEomGqQcvMXTGnajKYG/djETqBuefavwAyTO0J+4dNt4WA2yWGJK+8xbbJ/B8BQ/k5e+/nxz2k9PaytJ67NR4Q8v/bEFhGmadF0PHkuM8VcpljAuMKWSwWMOWFYlvM3fSY5vscBnOEgWckPzYG+OgEdAqJPfSNAdIUU87cAKFuV456mSGUvY/JkKb/9W4zhN/axXd0jojEXhu3oWO+2jM4xm88xRjO4ARUM1lbl2p3SkyIxGcYxsN70uCEymy+ojBDGnq7r0QIj+TpTCKawbKJERBKNsjQm8uR8wecfnrOsAssaLucVtVbUOmGILGYz3ODQoonjQPQeVde50S7Fn1aKWWWoq4qrx4/BNrz3s/d5+vQ5u11PkBrVNtStofKJRZX39VSAMqU0jc0+DziPhIAZRyQFbHC0VUW1XDCzVTFtbFBVi2nnmLpB1Q3GWpTOUjVRlAK4NAtxKoLzz0fvicXoe1qrJoxVTlzzhOxzozlq+VPKskiJiegGvN8S9YaH53Pk8C7k6WAUogsM3eQVlRugFMsaUHBLpfKkJEoWZigVEAL9/p6mqUlRs9l03NzcoZXmzbff5vFbbxP2I912S2UqFudzxBYZR4DkAxId2gburp+zmFnqumZ0kb/9zg8IKH73D/8Bb737ZVTVQlL4/ciw6kiDQqcMsovWYFOWSJhsokeS4n8l5JhQKSdQlclvRHTH4F7g3Su0SszaS1784po/+5/+nE/e+4RGN1SqwihDEKG2LYLPIJSP+OSRpKi0RRuNJqGIzBuFFcty1tD3EIzivDGMyxl13eahoZ/TWoOJls6azC7RQkiRKMU4uwyKQswAQwi+eBtlCbKWCoLixcueh4/WPH6rZXR7FvqKuVE8QHMxh9Wmg2TwLuH2O7r1ljHd862/92XmDx5mYmu3Y//qBXM9o1YCfiD5MdceY4/ve3arFdvrFZvVCt87dusN+/strt+jyJKX1XqPBE9KAa0CUmnWX3mXdnmGqluSir/CcoScIhFIfo+7v+b+2TN+8Fc/5Ps/eM6rV3v6uGQfwSWPyaFpWQIsGqU1kcymmK790i6DJEIKqJDweERpgtKIzoypJDZPuXU2ls3pfAbvs38eAqNXhGTo9p52ltkvdnTQddS7PVTZUDxVZCmOnxr3iKmkpH0UiYtEApGdG7m7u2W1uidE8rpV/NFCLD58oo8zV17fr+NrU/0iR9AWLebwc601xujDf782N5nWiOl/kgcCp6kSE7hwmtw3SR6meii3DflnWh97yGngnhkNkFJhSkpOQksFFJp6SudcWaNfl1XEUmemkCXmh65++iypyIxRRWoRmZy3smQiXxZTWtlUD064wBSNfvKR8zN+tV6ZWlCmMVr5M528wGfHGD47yDCWEzMdMEXHaqqKuq6x1uJ9oB8Ghq4nkSUF1tr8XKPRVUVlLEbnL1MXUyBrDFVtqWw2SFSiEVH55knFOROVXTuVZtSaaAwfdAPp05dskjCIHMyAEENJK4PXCuZjQZ0pHxMIIcURtBTS5VudUCumE35yflMMGGtp2hk2eLyruF/dsmhbYiqbsrEghmH02FpjFDw4W+Z4vuSPje/UXcgpmjdtForYR7ptj98HYsjonlLqkFwQYiif51iwa5UNXKLErP/URccExOCZJncpBZIaUcYRhz77YSjNs6fPcaNDxHB2dcW73/gGKEMcYXe7JXYeS5UjrWpFczFDVQZR+XW9D+T7arLGye7MMW3ZbV9ycd7gXOT9Dz5hP8Af/ZM/5PzRE6QYbBKF9ctbxtucrGD0DJGqfL+A9qgqYZq8iEqZUKQhEENEpeIyG6eif2J2FIqSSkBEy0BIa4KssbXBbR3f/ZM/5YPvvEfcBVrb5kVCG8AxuBFXJrlLa6kak6OFKo2ZLbBaSHOL1TnuaRwG6sbgJIHzoDW7lBgTjGXR9yJECRiJOEnsx5Hr+xXJO4LAMDqSbum9o20MznuSCH4Y6Pb3tIs5brxmfx94/vSO5x/d8OrTV2xv9/S7HjeMBD8iEawkzs809sGSeVOhUPQu4bxgbY7CiSjWLvHxiw0///AV31x3tMstM5PlTUkyk0lSguhw2z2ffPCMP/uzH/H+BzekpJn0ZyEEnAvM24YUPEQPUfL0IiaUzQtm9oeIaKMIpdA0MTOYmsZQny8x528RzCL7p9SZjihVTUDoVHaw1smgQ743dNkATQKtI971EEf6MbvHD8GTjEFsjqua7vzpRp/ovEfvgMgkTRCkyBPStMfk5jNbnGeK/mGdzHeAVkVHHOOBzjZpATP1/wg8TPdz3hRPXILLX54e57QoTa91+pheZ1q8T3H0w3NOgILjn9Nrna6FqRThec2Kv7I7nfpV5DVIHfekUxreAbydNnZee7/pv8uwElXmFbk5KX9HMYecGBSpgAVMxfW0hk4AzfQe8BpLI39Jx2OcvvOyAefuKF+rWTKcyoT69Gzn9/wttvCb//BU7Lc7PBO4OzCb17joM+BZLrHdfosbB5bzGYlI0zTsemGz7+iGiLWZ/TilY4UYsFajraKqKiQJzq9JzmFFmFUtTaVojTBTnodnhvPGIOOOCkVtFEwMRKUJaSQlj9EKbRuMNnmtd4G6naGU4uHVOYvzK+7u77jb7Nje3fLo6oyHDx5zu+mhWbBxkT5GGizrbswFrs5yr8p7khuxKVA1lnlTMWuqLDuzNcoYlGTDv6gsaIuqqiw50LpMH3M0ZwpHGUT2lylAXUq4sefVs2ecL2Y8fPyY9W7LMDpcTLTNjKqy6LIO6onVKoIPGYQcnSOOgTA4wjigQ8fDtwyXF/OchCNkLUZQ+DHk+tRLBhhOJLWHpSOX5IBHicOaBMVwediumbcN0Sdurzes1z3L5SXvfv0btNWM7e2KJIqzB2eotpjT+gghIiF3Zf32FoNn1swgGX723s95/8OP+frf/0O+8vt/gKnnEBVuPzKuJ4AhG+0po8AopEpQATrXjtFl+QJBZaBhWrdVIklA9EjknrG/wViFSS0/+V9/wff/7Lu8+uQVygnW1sUjI0cQaxIzq3jzak41qzgfIstZS3CeurJE74kpUllNpxSLmSG1S5zLPgytyVGkEoGYDUxTEoYQUTHhIiSl8DEgKeLjiNYeyNLlCW1K5MHf6CJSWfptz3YXGMfAOHREP1DpQK0cyxbU3Q7nLWHw7Hb33N3f8caDmre+/F9gq0uS69g//xg9dNjGMt7fsN04NuuO65fX3Dy7ZvXijvXdmmHTE9xIDnYJaInMG83ZeUM9s9RREYKm7waaZU2KiZefPOfync/Rnl2gVDxKoiePJyKEjnF7zfXHH/Oj7/yc7/7tU/o4Y0yBqKvst+Fd2fsyeK+nLUsZjM4T8xjy8CaVvSqmIjSaGmAoAEfIwHwk1zq6mCSKgMom2spUBNUgrUEqjbQNTbMg2Aa9uMTNlvk+96VOCCA6kVRAJOAiGA0paDyJ6Ad29yv2uy0++OI9Y3ittZcsqzj899QKHUCyw0cAAaNt9j4qPU42PcwpDIcUrVKDpDwJwqgSTV3MdUOJqzjE68qxTuCEeQAnflxM9devMB8mLOTwEqUGStOoA/iVOunQe04nQKaOMx0BgokykKZ7gHId5Hot98ZyeEo66U+zj8Oxzz2ajR9lItOw/XC8h+/j5JyX4zjUOOXPdPq5P8PjM4MMr7nPF9BAKNM8KAZA2dTGiDBbLDg7P6epW7StMKbCmgqtTNFT59zcyX3YFHM2JccIsFSQG9AHs7TpZCWlWI+BmAJBcvpBlGwCYpRmMqB8XZ+c0b6Y4uHKENFlYplOCvDpKz+e2wxYlW+UokO3GqObfLN7j3cDi3mbDcdCjUotT59vi55RA4H5vCZ7FqTjN/UamFH+PSmIitCnEk8ZkKgye4Hs6qqL+ZLzvoAjuQjSapqwjURGZNIaq6x5y1o6ITuzOlCeKEOmgmoLET764BNSAltb3vz857l8/ASi0K87+vselTTGWLRVmLmhXjQlVjjTx6MLhXs+leIBVY2sbj9iPrNoU/HixQ0ffviUt7/0dd780tfQzSwnMARhf7NmuN3R2jlW18XxdzIeDKAFXRtUBZNDTHKR2OdEiZSON9OxpZDD9Yt4lB6I6Z6+f0lVQX8/8oM//Ws+/ttf4NcjlWqwyuDFEyVyXgmfX+Zp+ayp0Ams1XiffQ60MYScZZTztschT1aAfTegtCKMkZ3PPh6jbQlRaNslWjlU2mEqRxDF3WZP8gPEgLUGdGbjVFVNCPm68S6hxbC6vub6aeBnP37JD7/3Kf3WE53DxnwtVMZiTIOIR3TEVA1iLFFX7MaBZ7cDu6HICWwFY8/t1tGPkdvrDTdPr2Hc8mAYqNo2fz4l2MpCCrz89IZv//lP+Iu//piX68QQFS7fJfgA+/2e88Ui54iHhA+SAahDoHHJOz9spjnuTKtIozyN6mjsQKprRntGNA3e5EzyaRNWKXulILFQG8t0gTwJT76n29wx7u5QCszyAtMu8ZUlKUuOZI1o4bA5H72G1MkEPzeckwYPjmyFCWgQJpZUeXoMgOQ4xqk5T5NXgRxfS4pHCFKopuq4acbTlexX1uQCMIgq68YBqJCTP6eYvKMfyWlDf7r2HPfE19H9Izhw3Jhz9vTJ5vT6jps3xFPwdFprE+UnRU95OGfq5LVyUzLduSd4QFnDyuVT3ktN+0Di0MCUb++wEhx2MTmeTWGKKitMjMO3WsqKQrc+bOLT9VCaqOPpOoIUv338Zj6Wl28whFfcbnvQirpdUi8qXIrUilKQR/q+Z7GY5dQoAFXhtwPdfiCmYlqXsgdODKEU04IxenIUoCn04rqqubq84Gy2YPXqJTMl+O4OXc9IYWCz2iJn86wO1Jpu27NarYDAfDbjbHlOZSrmTYM2Fe1sRrfvWK03qOSoDdRGePcL71A1c+63IyFFnILOZ+NIULTWIpL9heZtzaOLS1SKqJSYNTV1GQiBxqFxKDyGKMX0WIRY9qk8PE+5YS1rzIEBe1h3QBEQP3JRw5OZ4c1W852ff0wwhs2+Rz94yPziCi2aJBGTIg2BZdty3w3ZBNN7kvOo4EhxRMWBJw/OaaxGJZd1/kkIQ6Df9XgXsWoaZukM7sYAhdGUIQdHSj0iLmuvpaSJuJ7qrGUYA6+uVwQPlw8f8fjtzxGGAe88s+WcejmHChIRkhCdR1IgpZ5he8Ni3qDEcnt7z1/91d9w+fgJf+8f/1MW5w8zwNB53GYkjaCSLfTwsrxXIDWgS+xzBFxEvCIFVfaWwszAI2okyQ7v7mmqmhQN3/nT7/PDb3+XsPHM7ZLejJn9WiyskIAbOwye81pTWYUaHRWKMY5EN5Cr7YTVDcFovA+Aptv3OfkqKvphxI9CCFkGqo1GzAxPg6oNGE0MIzGOjCH7kAh5uq2YavYshfaSm09la/ajxhX8yA09VRPRsee8BfF7cA3Dfsf9/R2vVve8/fl32Y0LXj0NMNxje0d/v2GQPR/98pa//dv3ub3Zs1l3uGFEfMz3jYJaQClNpYSmMpwtK87OWpStGHzifrtls4lIbdAp8fzpDe+sdqh2gz23WXajiomoCKSB2K/ZvHrJz378Cf/2T37Bq1tPdVaxjyOmrZFkUD3kLnLyAsg9jS7YdwbucuR2DDGbnscCEAkFyCv9heQaPDe/Ot/BWlAqZv+LqkXPLqC9wszPEGtRdQ1VSzIN3tRQVTkNJypwgo4JCT57IChPGD2EEe97us2avtsSSFSzM2zdgrZllT1US7kEPDCSprWhmH9yrGUOT0gps5FSzMaWqCIXhxBLP3ry2UU46LFSGR4wJUlM8sZpfSr1UZajTENndajjJvbnVLHEePQxmNa0I1ZRKpapZiq/I8hh8PSarLIM7UxhaIUyrJreDzjUb0lNlZM6gACTLOJoDFn65VJXodQhxvLkhJKgsN7zsaaTf5hat5PB+/Q9/KpH13/q8ZlBhsk1W8iRe2idC+IkuHEEBDf0MI5USlNFqKNirmtM1RbjtbrEGAm2KvoroRgBHQ21phOafZ7Jpkcp68AnY694ii5J3iRjjGglBO9zxGSJYMqsiFyET/EgE5KTL+XpypjeoxiJTRqamKeYKZUoyJQdy7MDSzg872zeYkym8qElu+72Pt/4JFTwzGfN6zX90RP9+E/SxCC4vcP1njgWCnExHZtiWHJKRDxe6CpPD5USRHJSAASUnhqBAn5FOEQRJo/SiVQWHRHNftfz4sULINIsZnz5d76B0hbfBfarHeKE2jRUVUWygWpeIxZQmUo/eUNIiUpEIso6+vEGVE87n9HtHT/72QdgW770rd+jXZ6DGFIyjJs9u1f3tKrFaovCEtFEKdQ/QFtB1wKmUIuSEFzWC0os0YjkFIY0IcnlrhFJKDWAXkNaU1XC9sU97/2H7/P0Jx+SOgXREkXnIURKhHFgJsIb8yYnR6Qxu/0mjQuOqq6gIJGu5FPHkBh9YeKIoR8DuraI0YxRSKaFpGnmlyxnhuDXWNUhaciXVipRONoQEtRNjTEmbzbRst97fviDp3zy6Q27XvHDnzxjvc0xYiYpFlaYVRobEiaRI/oIjDZwH3q09qzWAy9XiZ2P6CqwXGri2BOtcPWwRcXIq49eovYNu5d3KKUxVYWuLVXTELzj2Ys93/lf32O3T2hbY5CDuV4FEAZq7bhYtPgx4ZzPU4yYaZ0hTE3jhBWDqERVJdraYdIaxgofB4LWjOVZEhNJ/EnDl4jBQ+jRKeDDSBx3dLsb+ruXpN0di8by5AtfJraWe1MRxRCmckamxn/S9VHWmBPZAeSG83SDEXnNQmHaO5SSkjCRN5aMtE/gQ5nUp9cX/jyVL57WU2Rm4rUN6ddMGAuKlhkTWe982CQOi01hj/zao3yyA0ZQANYJJJkAgdemCkeE/eSgeG3yUJ73Gko+/Zxp/ZFDQpE+0AZzJLKIEEJk0kweNtHJZPOQs52KnpMjwDEVFoefnb77KXg0/fy4lxz/vVwHEzQhpxyzIzh0QkI7XB6/Rj/87eM35rHddey6kWH0XD24Yr5sSDKw3+9o5xXKZGDVGE2lFRJ8WeMixjkqYAiBMHZIlbCVpa4NyUdMHFFjpKoMjTU0jUZmFcv5GfNZS2s19VnF59+8wu3v2K06Fm2FNRo3DJh6hneJu25NiIGHjy95+83HNG2FKjpg72NOBLIzru9eodSCdtbiUFTtjNEnYhwRsumwVUJbVRAsF4sFLoKtK84Wcy6WS1LRpovKEYZBZYhkTDnhwSedweQSRyeJg4RIyeSTUIDGqRjPtyykDELMaoN5cMa7b73B9YtXmOTYbDbsth0XZ3MsHk0s/heJxsJla+m6DTaOGYO1QhKD6ITynrceX6AkG/UlEm4M+C4SHTmJS/ShiaAU9GoCGCRAGkiMGBWLb1jM3kbDQPAt++3Azct7Foslb3/xy8zmc/a7Pdoa6kWLNJqkY661gielEVGOfnOfB2y2wvnIX/3V39KHxB//k/+aB2+8Dcni9x63daRRYaJGFU+FZCJUCalATCwsBslG154sTY3ZYywvSB5UD7IlhQ1WBYZ95G+//bf8+3/1F+gucjG7REuFELLcmLx0+pTYrNcoXbHrR2oSnQt0cX/wvqptDeQ0i9EluvUaa1v63uPCgFEVbvQUr+o8+NCGWrekWGGbOaquScHjg0cxEFMPaURpnSWUKeHVSPIBj+C9R6VI58l+A7om+JCBidBx1lpsGtl7YbfvWK13DKlC2sf89P0tn358gxluuawdzz74mLaq+Yv/8BOevlzjvBCiYBTMjObCGqxWBAIuhezT4BLGRbQTgotcbyLXm8R6K2wYMRVcpA3vvrhn3zkWDzvq83PE5IhbXVuS6+nXd3z0y5f8u3/3M37y844+1aT9nj6OfP7JgtrWvPj0RRnGwLRhTQPuCUlXZXgpOotXdGWB7IUWYzhUFmUUXPyvhNZAVYXMaLAGszhHlg/w7RvEek4wimg0XlXk+EWDToIKCYkBUvYpESKEAe/3hP6efvWS2G/QEjg7XzK7esJYKwad43oPck85QPi/hslL+ay5M4tlnSAn85TtWk3eaiVdcNJQ+ghKl0GFUqU3E1KIRInHyHEME9HyUN9MYENKpbctng7l76c+b1ospiSGFI8seMjDnCl5AiagIDMKpoHVFIf+OnPgeC6m4IPjKItDemLpdpEk+FNJKiencsJjypu/dorLkCgd3vVEcnsKMpSTLdPaLqUunKT4n/Hx2ZkM5IVOiaKqGxKpuG1DHB3jOBD7HUuBxloanc0ZZ+RiWYvKEUamRBpVuVmampGUhMJiIaSMwgTAIwdNzjQVLymiwOS8mX9xt91SWcNmfc/5+RltWyiLFKJzPBaLAkVLHZjQUsh04BByooQLDu9GgvNFv1OMR1I8mAwdStSyIEYnTOaCkh1y0AlUiug4Mp+3f8fZnVaODFWHMTLsA773SFAF0QNtCvqXd+hCKSu0ZJWbltzY5M+FeLROaFMaDJnOX1n1C2hjrCKbG2Vs+vr6Bbt9RxTNk3c+xxtf/ALJJfZ3O8bNQCU1lalRRhGrYrxYzHsIieRyrCZRl0bMIWrHbv2MywczEoqnn77kk09f8jt/9F/y6HOfyzpDMkVw92pFnWqMalBoBEOczPMkZsOZWqEs+b+VEH0guIRK2QNiogapNDmq5kUWRigAQ0wbiI71izXf/5Pv8OwnH8OQ0FLhkyEUJkk3JkbvcBE6n3AIQ4wobWlNlQuvqDNlsVADx9EhCUbnEfFoAasEI0JVNQxeiKoiJI3yAUvN5fISY2YM3QotQ06JiAljE6ayaFsh2iKqwkfN6n7gP/zNx3zyauTj647btStsnlxAzjQsrKGxmsWsgWgJAerRQxoREtZatF0gNYgRVN2CH1k2ioWtMH5ke7vhQVsR9jtCCIjOPiugeP78BZ3XnFvh73/1Maqe4VOkG0fudztUUqiUaGXLsLmnMoaqyWwQJQbvFW6MOJ/XvryoJ2J01HWibUIxBMs6xhAcQSUkCEpNzKMcHxVjIPoB+h1xvyL2a/SwRfpb5uJ453LG22+9RfOo5mMlrFNhMhVwU2GAkBMLTu7M4xJ9bFynohmlirlpnjgkKBuMYGzW+2b0miL7yq8SwkRhK9N7IKY88VQTdj1B4xOLIp1uOfDaLgbFbKisdwW8KHLdXPQXMPd0I5mAgAMAUDbY/LzpEI6tdTxhVEyrCeXYTs/UcTPngPgzAUFlsjBtzKcb1qmRJIUlMfnAhUguhilsqekzCNOKfmQbHLr/abPMB3BgLBzAHcl7yFRgwEF7mAoa8Rr4kDia15XzcgL/nJ6K3z5+Ax+3dy8Zhh5jNHWdqBtNxLLrA3e3Gy4vFiilmVUN4iO4kcdn55zPr5CNI9gRbzTD6JmpyKzWSEqMIZB8zyJaHjQVZ/Oabp8d4M9nirqOpLBH2cB5lXh+vWZ0wpPPfY2rswtevHyRjRmN5uLqgouLJRcXM4Lb4cc186bGBYcbevqkcVET0ARVsd077veOuNuArhi8YnCJqASjK+YziziFqpa5mdAGYwwxmczckDIBLIVpwJTY2FzrCIJlMuvN/xidhy1KZftlLaDV0S0lYwxlaGMFiZb1Zst2dMwvL+nXG3Zjjo+NwR3qLa3Ie3zMBmgVOdI56XwsRisqk3jj6hzCCHjCODL2AUKWd0wDCJhiACdJmxRQMxCiQyTkNBEiIp5+v8F1I9u7jt0uodB88Utf5s13PkdwHkSo5zPsssk+UaWhSt4hyRHGNaHf0LYVIor3f/lLfvqL9/n9f/Lf8O43fxcRi+8cYTuiBsn1k+RBAzoSbUJXgPGgC//LQxoTKkhmvqJAKcREREacuye5e6yG/bbnO//f7/O33/4+w2bgsjnLEcxkKwxJEFzChRGfEkMIWJ3wyaFjRVL6sH6OPrLr9/jR5UGQKPre0aSagMpeC95DCig04zhk+YXRmKAYA+iYhzl1NS8MBSGEHTFuUbFHpSGnnllwboQQcUPAD467bc+rleex19RJCMmDbrA60urAs27D9WbNzbbj7PINzPINPr2LhP2esNuxX73g+uk12/WWZy9XJAwhCaOfWEaes13gYWM4azR1ApsCtVVUyVP1O0ISXIRYLQi1YRcV2nnivePZJ7ecn/esru8xtUHXhqadUc9aul1PCsKf/9uf8MsP7tg7oYuJGHvEeC7mhicPztDDPX3n8SHhYsCHiI/pwNrOtUA2kFIpu0ClAjAF0mFQcohOVGA1NI1i3oBVI+gI1iK14Osab2uCWKKkkuiV+wiJfY5F9QG8z6lk44C4jnG/Ig1rdNiihnsu55q33njI5eNz3Ex4GoSBYn+ZO2Rea3vTJDEog4lyh05SUWIon0MdYqKzpKcwjyQzGXKUfC4Y8jqVB8YhRRKT6SJleHesH5BJnkNp4qeBU64Lfy2aexpeTz1ojKUnO8oupQCt8VDPlAHJlHTHsaHPpyAd20CRQ40ST6oPUcfBFOlYd+TPUNj/J/9Q+sHJgPXwKWTqIyd65gnn/zX04AQAOTlX07H8ypP/o4/PDDKgslZPKUVlDc4NqOgRl2AYscmznNUsm4bFbI4xDeiKZDSqqrBNg61qlLbFL0GyzjVlesqUVZrKhRFFDpFGUxFZTg0HBYtwyBqVFAnjSEwRmwJzFVjoyG4cS0Re1kFKoWP1XUffd6QUmM+zhjH4gHeOcewYhiFHULlQqMrHjTERCjo/NSf5mxcyyqMkY3A25cbdakWlIjZ2LGcVcpiQnV68xQxpDAw7z9glNDobO5J9BJTOjbZASe0oX3YBrnOCy9RMB7IPQtFoTXseGfjI7wioiOiccmBISBI+/fgZMSW0NXz1934XO58zbBz7ux0q6GziqTVRxyxZaAxoV1gSkThG8Jl5kWUEI7v9M9o5VEZzd7Plhz96j/PHT/jK7/0+tp2BKIKLdDcbtBMq1aCULWkGJetageiI2IRYyQBDeYTRFzZVWRjKapZKg5fTZj2wx6VrKpWlDDcfvuK9//Aen/74E8IuoMWQJOJTliPse8cQYo5JBUYEjMFpEMnmQn4csdpkV2cfsdrgXM7DNdpADLnJT8I4elQy1Kpi8HkRkWEgdGDaGbOq4Wx+BWlPio6h36LEMY6JVy/3DEPkXAJmJuy6xPOV5/1Xe55uBoZoSFPuLZr7CNWYa5LZlICiay6M4bwWLmaaBxcNy3mdzXpUorIaYsC5TPO1kmgrCNGhVb5Osm+RsN8PrG42+Ahff+eKdrFkvlyQgHXX894vPyWiqOoaNzpe3VwzOsEw5qmL6KzfCxBCWYSVzqBZiFTGYLRC0EQUdRxQaQNjnrQIAT/24HuC2xOHHcr1GL9nJiOt8ixN4upK8ebVBY+vFlxeLukaxW3naPCkqPCiiUoBOaXloOI4jA1+fT09NOzlr0SEHJ2YmByFMzaaCix6ZAwIBQw8+IXk95l+dyrm854ph00h/cphnDa3KUKIPr+2HPlR0yY8AR8TyBBjPPx7fto0p580h5w8/+QzTvB42exFqwPqP8UEQykWUk4IQU4kGJLZRUwsjZPzl3/vCDRMIEsgs8nC4TXkCIjAAVCZ2Fl53Tk5SRNQMn0f6URCJYmQ/AEMkvK9nR7v9DqpHM+pF8Vho4+pyFV+izD8Jj9S6DlbVFSVhTgicQSJRJ+4W28QH7m4WGSH/2GPCpFHsyXvPHrE/maN8ZGLqyd8+NGnLJZNbigTrP2es/MlV2ctbzy4YN5U3N4mtvuOq7Ma7wOd72kaYWZy064TzJuG5dmSru+o65az5RnGKLr9DiLs9nuIA7WxJDReDEPU3NyPdGnOq2dr7rcDgxeGMdE0FlSD02dE0SSpUKKptEGSISkDxdE9ToWFwCTxTBQmgOIggxImEEGw6iR2jlSkIZnCrQ4eKrnQj7GAxj6nCnXjjiEJQVeIrlAq++SQIhEpUrZsFjcMA6TC0EuRJBqlFZWChsiDy7Ps/8NIdJNJdTYeT+jDPZ5r+sxnVSJZUw5FUhsz1R0H4tne39DWLUa1+LHj0eO3ePML73J19YDoA7apM4uh1uQ6TZGKhFjFgd3qBb67Z2bPWG/3fO+7P+KtL36Z3/vH/xVVOycMHr91MAoSp1hyQKVca1UJsTHLRUWy9NVFkhPEZzYnWhATUWogscEPN6gUuHk58tPv/oIf//VP6TeOWT3HmCqzYnWuG1UpFH1MWKU4XzSEGDGLmiSZlbEdRrZdj4tCiHli3piEUaClwY3ZTFzEEvyAJKEyNm/wwVEJWGsIwRPCgIoNIhYxFdFapJ5jWWLjHvH3WNkSSQxj3ixtbdiryGoz8v6HKxZnlidOIWbEznL8a6sS9+trbrc7+pT4/JMnUM+53e0Zt4H7657NnePTpyN3q47Ba0SXJi3m+t2kyG0XuRsGHjrLeZVodaKNiSYIbcigvbaaVlcoMaWRG9EqsFltWLY1YRzo1h4xmo1Zk1B8/MlL2tkZLz654fHVGedngtgGH8F5jxlWsB95+8oQfIUPCh+zx52LiX6MdJ2nHxzeRWLxH6P4aVE6DNS0x5bOSbIpY2V1rqvEFSm1Jgo4ibjkiL6kRpReIYUeP3b4fkvo9yTXI27ABIfFo8KeswYuWsXlQ8vDs4YHl5ZqGXmJ51mgMNJj7pnKoDef8Hx4UmgFUxLh5C2nStqTlHriCADkGmySYE1DizQ9X+X+I8RjcwxlIHuyrk3eCulY5BwGO6/tC+mkNjocd+nZSgymUunAXD3ILicjfBJ6GkJIlka81t6XxWgCJjLYkg7MbRJInM4LJTyjwDEpD3JSASdyHVb64hJ8wMkgBo61HUxjklJ3lutIkmKKqcyfdzraqU6XXztH/7HHZwYZtASUZGcBFUeU76mjZ2Yts+Wci1nDxWxOZXOkYsDixJLqGaadlQnsZFSYo+sml/A4fYbpgxaU/FDAHb/VfFJRBc2J5eRl1sDZrEXhuNA1jxrFxdzw05efousFTbskFTPJlGC329Bv11zf3vCFL3yetp2RnCe6AcKATi439qZElMR8DIdJWXHLzxO1cKAoa5UJTEsrPJwpWonUWtEYRaOFs5mGolg/fcSo8H3C94noNGpy7pC86BWAuiB2sSQ8lUtAcUC8VTkX2RhqzJQmOV5cUwY8hYqU83tH+m6LiQMSNNfPr5k3M5YP3+BzX/0a0UW61ZY4Bmo9yxu1AjFCNa/AgOjc6EQXcozmZMiiIz5t8HHD1bwlRsVPfvQz1tuOf/q/+285e/g4u+C6SH+7R7pQIpSqAhgU+p+K+V816EqByaaWCiH5SBwDFPr9a5PHct2ICmjT4+OK4G8hVVx/+Izv/9vvcfPLO2KXUDLFsEpZvLMxTtJZ3iPFBM6FiBsdLoyYpEk+okxuboJPiM3gqzGas7M5Q7fDiKKua5Lb5Zzo6Ijeoaua6AJuH+grmDUz6qrJhqgaxnHOOO7Z92s++PCOpjU8WAsX64rVXrGYz5i3I8sQESf4lJuxlIQgCqcUKM0oBq2FWWN44+GMr7695HOPKh6cKdpGUVuhqmxZZAIh5gxso6cNKS+EIRjGPnF/v6Xv9tS1yjnpRhOHe3p6ktL4IbJbbXAJrh5coJVQGYX3geh7tKpAAsELfkwMLoI2xCEeJvKz0FI5DSlf/ya+D+0tsfcMwwhhJPQdihGrPHMrLGphUUUuF4bHlwuWteVyZrhc1pzPK6paeL67Jby8QcsVYs6gnmPmi6wtTiDJTDDm62gxZXGe1qmT9WkCFsrCVNaysviTMmHrBESY5DDTMj0xIpUqEpIJJjvEP3KQKfyadELyRjZRCEUfTaIOz0kc3JT/rojFSR4hk3Tt1963vJYiu4zDYT2PiUyrTulwbqZf+FXJxGvHfHzzAj5MDJIjCHCIxUzHLfKQC13+jCe/lVJCHSCdX/2Qp+BR0a1O3/HJ9zv9eYJpHM/DhORLlnocNoQTwOa3j9/cx6Pzc+bzvMft9jvE5aHComppkkFcyr4/VlDR0GjL0Af2245KCY+WLV945xH3L17wzuNLlIK2qulmhrcfX3A2r1hUhqaueHK+pHcRU8/4q+98l7ppePPtt3l4dUVlG3bbe4yZ0bRLzs8y62G3HdjtdgxDB2jWG8/oAnsf8UGx7RV9gNVO03vLej/S+yYz7dAMvkEpTawqokAsDu0aC+icLqF0BhPK/Z0HXik3HCmR0mR8WorjEqmmATM1AjEd7t9UwMdIlhCGlJlWsSSFxRAOMZZeVJFFSvbq0tl1XlKWS+QlLjMFU8oTRFsGCioJlUBrNGcXS0gOYocKeyqxudYRAaXLdJODjDbf04kslfAolT0YRDKfljTSbe55sFwSQ8X7P3+fj5+NfO6rC4yucERsazEzmzsAEskl0uBRIRCGHbv7G2K/pkrw/sfPqNsFf/Av/luWF4/xXWDcedSgUMHktIwSyR4lX29iUq6DykIYXSK6XHfkyWZCGYeqHSJbUlxT28irl1v+5P/1N9x8sgJnMWpBcBEfs+Gfd54gEWsEIxqXsjExKaJSoKlqNps1i2ZODBqnsvFnP4xocuqFTnmg4lwB1dUkixHmtaUxwn6XpcOV0cws3I092jYkXyOqJiRNUhVaW2qpaUXTaEOMO3xw+JB9M7p5zbjfcrPqee+n19zc9dxvGuYLzfNXHt8NdOsVQz+yWFzx4MEFw7Bnt7lnc33NfrPl5m7DzXrPGAUnJoNY5OZVa8k+HRLpdGJQmuWDOY8XikfLiotFQ12bHAmfyEwNF3JKWHQYDW0jRN+DxMz+DNDvPZtdz92rO+7VljeulizOclTu4nyOj5G71ZafvPchzz98SdvW1PUMa2ZURkGtsXWND8Locs8QAuwHx2a3Y7Pf43wRRqRjdDOlEVdasFWFMqVmKd51WiqC96T9miAR7z3ih+wnM+YBThh36NhjcNQEGiMsjGLRGJat5vHVnLOl5XJRs6g0s8bgTWLrEmqYaqLJw2ky9U9F+jgZOB7ZAa9PyvMFL4c5QV5LZPLWm3r0wgaYvKhyyyZMPk/H9Ksi1ypgKhR5wgSolvdM4XQYE0sdUHpQmXrBdPy1Upv9ap0VY74GotaFjTAhI4cqiam31FNdUd7qoPQun5/pLB0AgsLwPpV2nr7qyWcuKMbhvycgYwJophN8rOGm98meZ/mrkgNT5LNBDP8ZIEOrEkois6bmycMrwjBggXnd0NY1dZVjKrOzpSaJxqiKYAxeHVwPmFy640RFLdpa1FGzd9okqnIhyoRmUcyCUkKk5IWqCApmWmGxzETx5ScPuH31Arb31NpSxRqUQRL4mNjcvOJ+u+b+9gb3xmPmbUNlFEbX+QaNFSGSXeBDIJTiNaVQYvZC1hMnjxvjodCtJNIkz1evFnz5UQvDBokBaxLWKGZ1IDHkLTlbWuTFwkVcD3iNwZL0UVsdiTlrViI+eA7xKicFsVJSghMy3U+iB0LeOA83r8qOqin/e0pZThFjz7BbEyQxdp6mavnCF7/MW1/7JrPlBf39jn6zx6ps3ClGkwyYRjBzWza+0oS63OyLQCpeDOvb59Q2Ij5yfb3mxz/+GV//wz/i7a9+DWUs0cNw3xF2I5YakTyVKZhmufkFJCKG4v8QskQjgu9dUYdkND8VTXcetyQEh1R7krlHwhaD4qOffspP/8OP+ejHT4k7ctSqMZiqwkdIweGJ2XuAPKVUuso0wW4giWLvPXVVk1I25DTKMhIJzkEKVNZyvpixiR4fAo0yRAzOw+AGhsGxsCpTU6Nns9lwdl6TvAFjMVrRns0I6Yz7jeX25gXjLiC3O/bDUx48fJv/5o++zu99vefpzYqbzcDgEqOHfojsh8zECS6QfKBWivOZ5tFcePethq9/qWU5cxgV0UpTV6Y0YBqlhBA81tpCa42EmCBpxkGoamhnirvVDrtLKGXYdwPdfkdSNS5mz5XNesfoAnVd43zEGou2Bm2zXGofPTvX4caIsoBS9ONA8IHe+7xWuIgRg1mtUFVTWPSRZVtTa2HWaBqbmDeaq7OWi0XN2cJydTFnMauY15raKmrR7HcjH/3il3z4y3vuOGdoH2Ievs3yrS9Au8BT6KMFkT5O1o8L+7SWHTeTjKBLkTGlmBF7VWKSUn4yIpnRkKd4ZAnQCRJ/BMbyi04N/6lGLvvFTM8STlf7SVcYCxghhcqc8dhSOJ7sDAfd4GHdPTbJp/FOcjiuVCLF4uG9ypJekmoSU961TKh6Kg1C4tiQp+yLcqAblkblsCHCAfDIB6qOvzvhOBTk/pSVVTbYsmVP5Ur+Ux1lU4fDyC9eGpepWDkBk06AkAnAyPrLwlQpb/nrQMpn3X5/+/jf2uOyrXjy6EGWC/Y7dHRogeWiJdQVrh9gDAxDQnzg7OKcu3VgHFZsNyMQCW7k8qzBEmnrmicPHxAvL7g6X1ApYBwZBxidwQXFbp1wYY6KM7Z9jawFbS5QtWY3CNe3O1Z3HfvtgBsc/TCiKsNqWDM4RWCGbA1DFIYgjBEGXxPF4GVOsrneErLUQZTOEsMESRUTacn7beTkvitNV5YHFcpxMY0+FNjIpA4toN/EVCAj7YcJYSzxlfl1I8V3Kx5B2pQEnYRaa84Xc2aVpakrVCpeCSmQkuBSYjN0meIsk/RBSn0YaapJHuhIYUP094y9omlblJlzOHI5NggZRHdI6InSoScquY6kkP2t/NBRPbjg5dMV3/ubn+HUQxbzc1II2NZi2xoxEwKQWalpdIgfGbYrdHRUxjL2HpGGv/9f/XPeeOcrhK3D7z0pGCTmWPZc1ybIcnikFjCxMFWF5IU0qiyXSFkum3RA2w5Vr4FbGBN3L7f8z//jd/jOt3/Cg/MH2Mqy248kH9GmxqbEOGbWrpcMFQ1j/q7GccQoTSuJXee4aAyKSNtYTDPH+xWqMA7rkiSByrT8zLC16BSpdCBqRT9ouuDReoa1mrBdge7QpkalBlL25IpkCaqtoNYRq1ROh4oeVzxHOuWojTCOis1W+PTje0LqEHPOrG741uff5I29p5qd83gOm+6G7f0LxtuX+H3H/uYZ2u2pyUlUIWX/JG0UWgvWKCoLSwMXC8XX333C73xuxuMzzbxVGAPalgFYEkJIuOALI6TU2ymRorDbjdyv9/ghS4lbY3Cj47ytMOJQPuD3HlNbZjahUygRtZLTFBgYnGf0nqqpMCb3MpVtMG3N2dmcy/OGwZ3Tj4GuG+l7Tz+OuBgJpZbQViEmS4RGn/BF4qkGR7e5Zpc2jEnj3YCJDisJq6FVEasjsyYxs0JrhUWjeXi24GzWMGsMF8sZi4VlZhXW5PMy6MRVY2hGi/ZF4ihlSp9KDOPE+El5XZGUpVnZKzuzgUlZJh1FyMlkBSSAw+Bg6iNTjLkuiFI435N8OgMJqhguloH9YZ07LATp9fZZvbYW5gpCJqkGHGuzUhuE4JkGTLkvy+DFoV+Tch5iPP7u9I5qGlSk14whj5KJVECtctYKADAZV8bi+ZX76xKSUNbfdFjzpoj18qkOgEt5n5RrnWOil0wzrAOL4z93lPKZQYbzNhv9XZ5d8KTQwyQptDY5bkVrvFiCpNxISabbeVFF8jB9eScb2QFBmarkciqSwGuXSKb8K4nF7ThPoYyRLBEofgiSAlYJdYIw9ux2OxbLJaayGZgoX4YQUeJZLlru7vLmVQAajCjAkshTTec8QZuCzuaiPQZPcI4QPDEqzET/Kc7HzdDx9kL4wqWluw+40WVJSCQj4+Jzg1yORpwn7gPKV0g0+acqUxcmBC2SIIZMOVKnk7p8DiZNxFS8a6VyCkEBYCayTgwBpgxWEtpAYMRIYt7McIPizSdfwNmKL33r90kj7G+3qKCwqkYri2ghmohuzYG+l7WTiTTG/BlEEOMJ4x3b66ekGuwofPDT97l4+IRv/aN/TNXMSUExrjv8ZqBKFSIVIZRmSYQslEj5etCCaqS8Z9EkujwxkGRQhVafJs1XiogKmGZANRuS7FB95OkPnvPtf/VXxE2kG2y5+Q2BTD/sxjG76FYW7yO77RathFkDBk2jNbNFw21YYa3BlcmuMYoQhRgyLc1KQCVHU1l228Rq03O/c4hofAg5sokcjRbJBojr1Zp58xAnCSNQVzWzWZOjK4c9PvSopKhQtDry7qMZ7z6e8/W359zcrBnGgDItSIMPgk8w+ETXDTnKVAIz43h0pTlbJppqIEWHVjXKJjTxoP1Kw4g2oUyHM4VOSYWxClEVDx/NedSds17tWN3t0CbivcYHS+gCV5dLduOI94G6UYgyuJiYz3MsmRFFbQPBg+BISqiaitpmxoOLMIb8XadUNro6cDZrqHVkWQuNVSzmhquLBWfzhvPlnLNFzWJuaWpBmyx/8A76TvjpRy/43s8/5eV94H7YsZc7rlDYxZK2bkHpEg2VDn4vE5I9PbTWoMiTlZSNa1MCiWVBT3mRNyov7FPTX8QUxBQJMct9kuQGOpWtJJRpTTr5GXDSoE87xAnoUdgSx9QaDj+fkhUmPeTBg2ECKE/9J+IRhX+dyZAZW9MGffBaCLE0CxzW9syoKK93uh0dEPVU2A/pIHWbPGYOe0FK2Qh0MpwTQWmNj+Hwcoe0Kp3ZRf4A+avpSyvXLScTigTFq4V4PLdyspq+5gnB6SSgfH5Jx8nEJM84BSPK5/vt4zfzUUXFo+UlNigGs8+RkVVFpWt2/Z6EgWDo9h0xQD+vGZ2id8LoaxSBT19tSXrG7dozC5aQtnjneXXXl0lVIMbEGBQ+WVIyyOItolhuu4qt91gdqIxws92y930eQiToPQxRgzf0QyRIQ9I1ztd0UXJ0HEJKGpQ+UIpzMZoZjBm4p9zTOqd9KU1TW2KKDKMrhSqHNSMDbCEzlkQX0zdVaq9c4ObimZLV7ktiQ35MY5hJVw0CCrTkhkQX85iUKZK01pLaWWYxkAHTWJirAWEMAULM3jaoXIumzDzQWlPXClJHGFfs1i+4frHm0RuWi4czEj7zI1WZ5AWfDeTiCGkEcQcZajYVj3g35joxKV49v2a16vj9f/h55rM5iGBbg2l0BiYgT0G9R4LDDRv6zTXL1rJeDXzyyac8/Mq3eOOLv4N4S+o8OlSYVCGqynWfRFAeZcneUyaCiQclbHICIVPCo0SS9ejKEbklpjUq9axfRf7N//g3/OAvf85yVrM4q3Nto0sjUWu2Y4f3Iykl7m7XhAiJilhiV0UibdCMqSZuPV0f6EJCxoFVF3FuYNlqGh9yqpoIPgbqqkZHjVEKHx2VrTB1w2o94MY9y3aGJVGpBGkkpJ6AJSaDj5ZeVexTxFJTiaOxEVPV9OPAfneHVonKWoypqKRh3DusNcxaoTXCoj0nxOxhYqsta3Y8vBj4YLUiVfDNN95mtd6z2Q1s+8j9bmDwCbRCG0tTW5aN4WpuedB6FmrPO4/PefOhxsoIOmVGQEZUUCL0Y46pFcn1QLZv0yw7Q9sKt9dbkvNUZzO8CwyjZ3CBcfB0+56kFL2DlISqrlHakGLCOZfTSaInDDnTxcXEvpfsZYXJMnRVgbJYK9TVnKU6Z7XZst3v8/0eY/53nwekxGzeqIV8kSlFUxmsTixmlmVdUamIkUhVwbwxXCxmXJ3NOWsrzs9a2qai0oq6SFu1QEyGrhsQNXDWQJtGmugxSfBaE8lg0jQ0yVv3ZITIgVkEHNNOin+KjxEJoHSpIUpSg1ITc+PIgs+yg8w2zE1+YUO8NjzgwC44NvzFW8FwUgMda8CD2fehnlEHs9spzSEnVMRyD+XJSIw+s7wPQMex3prMFCfZZwrFQ2KqifJylHvSUHpgrVEyySvKWlXWq2m4Mh2/T3lImNlaJ+YDhyFXlmbEaSBEHhweWvNTdOHAPvlscMNnBhkePnqbpmmojSUki5iGIJogKlPl1dFpOJE105CIKpWF+lc4CqkYizDBDhMdRop3R8IgmOKhkC1NAjpl1ZHRCqvzl5dSLF9vPvkheJ5ev2IXIlFbggDkbN5QEiSq2lBpRYw+v/6heYfDeFDytFFULChQRqRCzJSWMAw41xMT+Vi8QyRSK4X3kZv7Pbv7gaEf8KlIL1SDpExLVxJySoXvUMMeSS1KXeRGWXSp2ydNcCx0scRUFudBqMoyAij0rUicTPBUbpQONy/kib8SJJRpomSJSG01EoX3fvg+f/GXH/LP/7t/QXv5iP56Q+gjhhqtinZNJZQF3RrQ2fuBCGmMEMoNqiJKB7arl4jrSBg2boupGv7hP/9Dzh89AQzDZqS72WGiQXRFDPpQ4OSUDJXReyWZomrkeJMERRw9KmRZRaJAnDEBHiSgdI+yW1BrJPY8//A5f/G//DWr6y1KLXm+6UkxMa81s6YijSPa1IzDQD9CN2TNqJCjF1GqTIQN2/0eEwTvMxgQE/RJQYpUOrMCnIsoVdGPPaMf2G47Km2YzxrO5mcslw2D98QSu7nf9mzWHZdXC6KHsc+Z1LNmwfninM02m6gSArv1hrqxVFXFrNLEhWW/DxgVsZbshK4zBTamJaI1WhSVjrR1RwobSBZRCdFl4oQul1wxOZXJ+KrclyqABEzlaZuG2XnNcllx+XDO6m7H3fWergNlK5IWXt3esd+sceOIxEhIOadcQmD0AeciEh2Vzotca4rbeVL0fszGma3BKsXZ2ZxZrTlrDQ8vZjy4nHFxNmcxM5zNGxqraZuKujJonUjJgWi8V+y2HT947xl/9ZNPeeVb0nJB0wrd/Rbfb4jDDoIjN/iqmDdLAe5KU/2aI/B0D+bnhBgJJXYya3zTQfYxUf2zTCxveuWsEsu6NS3m6gTBTpxM1ikgwMmk/VdlE697vJTmgFz4vcZM+BVK3t/1eqevUSDxw+8c5AuqeJ5k2tKxGSEn8Sg9MYrkwIaZ3kuVjV1JOj2Mw+c+HH85dxPLLaYpOWNC2yGpeNiAp8dpLNN0bhP5ffNrTgB1Pj6VXmdvHD77yXG89ncTD0K97nUBYMxntzr67eN/W4/57BHdXuFchZZlNlZXhuCFFBNaJ1CWumnQxhDNokiiNFRzYkqsOo1hBioQnaW7z+bFSluShliinrPXkEUpgzU6F5pWZWmfHWjnNQ+fzHlwNUdrjR8j6/uOfRfo+8jee7oxsRuGEhetkaSOut2UmaCxeFyR8v6VyHVCrqkijRJqHdDR03d9jlETlUF3OESnHR/pMPkSIimGHF/4GqYoKGNP5kdxgiGPAGfZcg6zRoFYwAY9TX2AyVRXIQd/Lg25rhOybDVODDKH94lnH71P9XZFYsXm7hk//dFPcINwfj7PNVXMZsL9bsfd7TX9vuP2+pbtesfjNx7wjb/3TWzVFNJnZNitqWyFHzzPn98zuoqvf+1dAJTVmMZmLwZFdrJ3Icf7pZF+c4OOI4jw8vkNTmoefPmbaL3A7z3Kt2XIqZGkCyBcBkQmknQg4VEpTxpjiNkmImQDCSUBqfZEbvD+GlvP6O/P+X/8n/4/fPt//gGPLg2LpWW2EOrUcnU+p7EV3X7P6mbLFz7/Lrc3K3abjmV7xn4IhJhrL2sM87rmrG5IopHUQ1DcdI57F9n6yM4ntO/Bec7mM7qho/KBmVY8WrSM/Y6ZWAKGXb9mdANzbXhwtqBqG/qU2PoOa2vA4pNhSIq9F+YqpxJEP5B0ZLu+Zxj2VNZijaWyNSTQCI3JKSsqjFSSow01oHzPzAaWi0Dz2CJiaGYzrr71JnY+w3vLpy/u+eDTlzy/XaFMxbxtOG8sl7ViUY188S3Ng3NDXY0gYx5ymZRlwxIRDZIGxOhiqEeehKaIVorazrk8W7C571ldd9yv91SVofKJcYz0LrDvR4beQRKMqTDWIkpnsCIlLpcLfBypm4YhBoYY2e42rDYjo0vM2hpbtfR9wHvBVA394OmGoUis875ci6C1ZdZa2iaDCnWtczKeJFQKzBvHcqY5X8w4m1ecXyw4m7fM64q2NlgNlckGr6rUTJIsQy98+vSaDz76mDfffcLy7bcId09J257aaGw1J8wucXqJizqvS9N6UCb4x5pJiCEWD7/jfptZopzUBnmNUVqISU0p5rzmvVRe8yh/KHIKKXGZSh8KiCObMR9VnnsopvLglMWQgZJY7pfjPwohnjAe8vOPn2N6nAIrHBr5Ui+R5WWHNKxyTnQZukBe0jO4m47shammOoAgZdBUarlYem2jNeiStZiyDD9LQKf65sikVRM4I5Pf3a/XRP+xx2eXS5xd5TQFpQi6yrm15e8OB18iOjJQnUrxVhgJExID09aEkLPtFTG7BuusCTNq8v5NqJQRNx9DZjKQJ4c5rTkX3s57fIrEkA1WYozs+5Ex5PcqouGM8Jf3d96x33a5wE8pL+AhlJlfPnahUPAmekv5efIeFTytNTRmhtGKujJISliJtBK5c5H7lyN+bIixQiuYWUHsHJJBcJA8MW6J4zU67Fnfjog8YnnxJTBLotJMKZCTxEErld1UD8ZrRT92mGE4CAMJh9Jk1FpN872ycRFzgU0iiScMHUYJm82en/7sY15cr3jrS98gjZHNao1gMGJQyoAWgorYxqBqIakyrYiQxoREnQt47XDDHdGteXBxxvp+y83qBW989Rs8/to3EVXhukB3t0OCxuiWJBa0KoZxCiURrSVPsjUoK0e3Z3SepPqUN+YkOQe2TL5FOXTVgboD6ZEUePXhK77zp99lfduBqpidzXnyzqMsyQmOWhvqesHt7QrnAov2DKsdSqCuazSBumno+4EkGheEm/WO9T7QLCyeETf2tCZPmpwS/D6w323Zrfcs5mfECGPouFhaLuYts1lD6nrCkBh9YkBY3e84O1tkmqsPDEOPNhUhJnxINMbiE3z66VMuLi+xdUVlq6mCYxgHQkjYakRpgzbZLVww5f7UxFjR7WfEAFVt0Qay0Uv2v0jBM0GA2bG4UPtFCjsmIjqgSNg6cFFXzOeGhw/P2K57nj1fUVWJ+uuPGH2O4UwKhhgwNnsejOPIOHrGkHOJQ8xTQ5JiHEb6EYIX2qrm8uKcBw8uePzgnLNFy3JuqSqhbSxVpaiNRcWAkEEQksJ5w3qf+OWHL3jv58/44MWWF53Ct0vM8gFVSswSnFnh8wtDLyM+WsYCVk3uwmW4jiiF0fqINlM2wrLWnkoOQgI/jBitCz0trzxaBFE6N7WF4RBCaciVKuaC04Y4rUNHaDE3yRypchyfN20s0yMzEE5iMAFd4p2mBpwpLUJOiv8Dep8Ok3mjbf7+TxgNKaWSZsIUBDG9c55mhpjjjvOLHo5pMoUsJ+awh8SS9qO1Lky346YfQ46imwq4bAVXwJ+UQZ3p2E+BmVN6oGS4P/+eyk2J8yOiVTm3eX1NkFkU5bin7zWEcDg3Whc5zWT4KPlZMWZN628fv5mPPrW8vB+JUUN1kQHXcl9Ws2U2X1ZQz0Fri9Y2b6vovH5Kkd9INtJLokliciGussww6byO6qTQSWGU0IhgJaBlYNZGnjxa8s5bC958smCxrLJOPIH3kWFMdF2PT47eBZ4937C+T2z3kc1+oOs9KRmcN3inSUmTRAhK8jEU7yajoFZQk7AhMO57tE8Y05JURdIGn2CUiVcqBHVq1E0xuC3gxcROIh1mI5O+V4s6AAV5yBQxalozCk26YBkZGC1yp4yucoT1yro0AcBK5WK8rLE6Rfrdlpeffshblw8xak2rPGczWLRbovsZPgSuX2356798jw9/+YJXL3eMw8SMcPze73+DL331K9hFk+sNiezXdyzaGferLa+u73nzrce89c7boDRSW6TJ3lSIZMDHOZQfCb5j3NyxqCzXL16y3vV88Q//IfPZGXHjkGAoORy5OlUehSfiEO3zxFZ5kvLFTkuIHqIX8AFFNnnEr9FpjcawfxH5l//Xf8e3/833MCmidMt66wmyR1T2sDpfLljdrRBt0VVLN1yjdE3TzOmHTUkcEyotqDhwdrbI65+LPFycsdg77FbxfJ0ZXUMURuepUWx3jrTvSPOWmda4Tce+GzFVy8xqaiNY7XKCi/FUqmbsRtLQk6QmaEsQwUvew71YvPasb665vbtDJIN91miM0URiHkZ0e4xPGFthtEIZTSomdVbBmVGwbBi6ntsXH/B4/kWuLmt0a3k0f8CX3jzn6Ytbbu83KAnMTOLBmfDmg3PeedMwawOisiQilB5n8vFIqQxLJ1+kssdQBp9WCXVTMV80XD24ZLvpePb0lvvdwGw5w/vE3f2eQE9dGboxS7JRGqU1EgRdVdS2xdgKHfM92/WBEBXOx7wGxZC9FFzApxGVYNlqrK3RSlHVFY1SQE4OqU0sQyzNfF5xebbgbNlyeT5jNqtYtJbKKtrKUle5N0sxkvxYWNaKGA3DqHn6/J73fv6Cn/7yE242O/7B8gGNe8ZPv/dLdi7QNJb66gGzd34H9DlB6SNYOTXsMRVAtACLkr0Rps5Rle/1KKPIvxfjkWl+kDIyrU26DK6nGmRaTfK+7kN2HpdpIEs2p1ZK5+emI8Dx+mNCOV9/xBRxzjM6f5Bo/KqZ9qlP3ulQ6dB3kgfpkrJhJod1s9SXIZW6NBbZ/iSPyDXeFIEZQ2GmaoUVQUmurQ71DenwEVSaBkIUNk6aFtycEDYNU8q5PzXA/k89PjPIUNmaSWsbtcKlhEsUN/spYSGjzkpltEUX+oxVknVaKVNAXIz4FFFJUDFrkMy0MSOocPLZU572EQOTF2KMghfBhLzBeJflCCFlZkASwafSiOezd5jqlW4BrRTz+Zx506AlImEgI0iTEdjkG5FyBKcix3BqlU0sK5N9EqBMBWJpSnIRvYmRFGqSySyMSiVqO6JKsZ5vKk9w90R/x3b1ip/94H12m4a33/0HfO4rf4A5W+abuNxw2hrU8YrI7Z8KJB2IYYCYgQsJAyqMmQGVVEYZZLqgJmlHQumApGJ2KYn17ZoXLzd87Ztf5/LNt9jd3BM8VGJADEky6iVGMK0FkymYqng9hDGho0JSIKWebneNTtk5+oc//CE053z1//g1tGnwXaBb7dGpwtgKpMqAkJ50Q5PmqCCwOhzeT0iZ7uxy0sGk7cx+lglwqKojqhtEbUAbtrcd//J/+BPun3Xc3gzUzYzFpTCbVcwqS7/ZMJ/NmdVn3N+uqY3mYjFjt9lSWUVlFBIijSQqSVRtQ6Mty2rOq73nVddzt91loGvW4Dcdlc4Si6EfqaxFm8STx0vmlcWoAMllZFZZYhoJUTH0EaMDtzcr3nnnCh8c4xABz37XZUQ1ZJPN4IT9bsSOgbEJGG2KmWpgjCOh9yitsSY7dRtVPFOM4I3GK0P0C9zYQfI0c6h0aerKrP2IVk6TYcgXcKa653U2ootRlq0U7bxhcX7FfufousB8cc7Z+TkYVUCviVoWCSRCLNOyEmUkCH70hJCIPuZmVYRZ01BX2atCFdNSURHRCa10NhP3gnew2Tt+8JOP+Td//jNebTqaxUOcnXOfwKqG+WyBOMf8fM6X3ljw5Qeaj31HHytchj0pZcOBYYUIIYUjbT/ljYB03DwS6WQinmn/edPLviVK20NBrsgbtlX2RBgh0/8dqH7Hv5GTqfzRXCilhE56SvhFaZ3jeUXhQyjXWC4aZTJdKiDwNA2A0vir46Z5uvkdaXQcmBDTP4fpKGXqSGYJTNIQSRyMxCYcPEZB0rFZFyXE4MqVxgHQmpqSDEL47DAt5gAAgKCYvGvSMfUjZrnFxORQcioxK1CzQGUrXPTl/KfjMwrTjmnfKNfsKcVRTVKXQmv03mO0eQ3o+e3jN+vRY7FYTG2xOg9VMntPF812mSCllAs2USC6gAyp1EBF2imCYFBpyjwvgF2pjwyS9xLlaHXkvDG0jefJW0veevuc86Vh1gAhJ2GZqqapK5pWc3ZmWK9XPGoaHl8uWK1GQmwYnaYbIpu14+7Os9sHdtuBrh8ZguCDAYG60ujoWSi4nFviOOIIjApcgpA8IVm8aIzWRGUJqFy7FUB4mmghkLQ6TB8h5Dz5mIvjSWmmyMNdpXJTjRTDssThtbRMPi5F6nWscw//5IlGFoCqsv9EElJBnRQqevrthjA2tK1n1hou5prabBH/kuQ8n77/Ad//mx/z9JljHC3D4EneY23i+nbDfhhZKAXJkfAMuy3LxYJn18/Zb0f+0T/8Y5p2RqgE3VpUow4S2BRC1udFR3d3jVUwdD0vXt5y9YXP8+jtN5Gug9iUxmXISQxxJIaBEAY8juqipWrbg3Y8g58QnUKizk2YGklhRRhu0NoQxor/+3//r/nX//JvuTybgVier0e6cUTd+bxkhoDV96QYuLy8YvjwOa9erqhNhQyRjQtYgORRuiZ4xzwlQnDE2GNl5LyKNJctVzWsOsduTIymZWk1j95+hJJIGHvO24bFYon3I4MbeOPJJYuqhuDZ7tckD1VbURHp++znRFNllqpkw+o+CW7ouX55jSn1tLaZjbRQpiAAAQAASURBVOv6kS6OdPs9SlnOllfM25bKWpIxGGPBTObVCmssqfJoEh998CFVZVnMzzBVw9WsYv7WA56ryHZ3y3Luefdzcx5faepmR5JA/JU66Di2nIaS02BCHcF9nXIEt3KITlRGOG8s7dkT1uuOu7s99/c9F5cNUSIvbwJ7N+SgeT8cGvH1OmCripAEHyIu5NpoZk2W08R8zhpTkxKYKsuftKTcuyihsonKJs4Xcx48WPDgYs7F2RnzeUVjDXVjqCtNU2uUDuhiam/IvY9PgksxD0CDZhjg1e2GT57d82d/8WNebiPbIHROcfudDzHVC3b7HqUUVmouFw9ZLAw3Mcd7owSV0nFIcDRKOEz3U8x/HpiRZT2Qw01RmAiHQbfGKM3oXJYrGItoXdIIjwOUrBMp45vCrpwSc2LKSR/TtzyZ4L7Oaiyjo6k2kqkHFpRW1Kp6rVnPddPEWs51SQYhju9P8QFMIQ9BQLCZypFrKORQEx6Pgsx6KUOPXGqqwpAo6V8FUJGp9yw10hFUkTzQZZLRZa8RScdEi+m8/aqk9P/f4zODDNMEaZoqoRRG5U34yDwQjMr6FGt19k0gZq0WORNZEGKW0aKLxi7nwxfZg8Ck08l03Mm8LBFiKJvMhAblE+a9n/gHJbM+G9hI+d5VikicpAbggmO/27HvOpqqygjPdMJLw5Cz08v8X0qfHiNy4mHmU37NVNgOSidIOXIvpVzkHyPr/AHkSGFAZIQ4ELs93WrLz3/yPi+eb1jd7fj5h3/G1+4i3/qDP+Di6gKJA8SQzzflRooewRODw/ue4DtUDITREZzD+5H5WUt7vkSUySKmI2iVGzSJIJ7kHTHB3d2W7dbzL/7Rf4kKQrfuUJQJuLa5ebER1Qi6VqDCAQhKQ8hSjBSBETesSOMGo4SPn75gvRv4+3/8R9SLB8RR2N3u0dFiTZt5j4WJQPFHUSX1FxyiPMoklJqkMWR/ChdIyZBcvraU0ggBpUdE9qTUg2g++vlL/tX/88/56feuGTvF/XZAqZFVn/BjjxXBxMTjB5rKwGrbUVVZ4rDr9/hU4X2OX0rRE4JHRgUhcbaoaJct9j5P02MMLKqKeW1zwRQGPv/wCbXRbG5vmDcti8WcvuvY7TuG7ZbndxuSVLT1kqHv2dvI7WrPxcWcxVwxEtgPI73TmGqODwYlMFtcMDrHmCL9epW9DuqKqdsciaQhM1tsZbGmQhuLtQbvE84IISjqsWW/XdPMRh48amlKtntKxwVSlftyGuvLFEU0/VP46kqyYVUzV7TzFu+FylYsL2uiSmBKgywCkqOTYt5FyzpD0Z3m6R8pe4tEF1DFpEkiBD/VsJGUDNErdtuBjz9+ztPn13ga/voHH/HXP3kObcWjRaZrdimyaBrmKudEv/14we9/8QHLZk/cOTqyu/m+AJmnaG3WCh4zm6cJdkoxe9SoLBtLZZ0DcvxiElIKhbIoGGUPzupZepPXO1VitNThvB4R5yxPkANC/ZpnQEp47w7mPJM2VimFtRP6fnQEPoCth7UAppXh1yQYE3IeMx16QvNPEyjCiRSC0tQXN7cTyl5Zt8vnm2h5zrnMspHjZ4MCHKYMYGiZGAYTgyKj6BP8klJuekTJgW0g0/SzdCZx+s7K9ZsOWsj8mabYqFC+u+M6Kacn6XC9KzWlDuX3naYQ3vvXPsdvH79ZD1W1YDRJa6I2+brTCtHZgS/pbNSrtEZK8pFITjZI0wYr0yReIE7yBMhxcCOVCFoilfK0JnDewDtPFrz95pyLM0XTBpqZQtLIbr3ivR+9x6tXN8xnC5KuuHrwBh9//BwRx+e+8DZvvf0my5mlblqMnbPddGzOB77weYOIoduMXL9c8/JFx2bn6YcOLYmL85q3Hi64XFYoavwIuyHSO6EbE71P7IOjC45AYPTZdDGaTB2OSeGLoWyIRY9M8W5IpdhVUEK0C5jAoeiV6V7m1AStrBETgAHlzwws5PUjFep3WSNiQFIsXhcDgci+GxiGxLw2EDWLxRlaG2JM3K87PvrompuVYzcaQqqQesb8smZWK+r5GcOYYDI/C57oI8HBzas7Li/O+cY3vp4T16zBtBpMKfQi2ewxeqIfGO9vaY3w9OaWYCxPvvQulQnIuCaFDf22Y+y3hHFgt9twe33Di+e3bPqBP/4//FPe/p0vokURY5mquojEnD6mxBPjPfe3v2DYr1HpCf/D/+V/4U//5AegE6tOses7ds4zBEddQaU1KglGQaUrwv3IJy9WjMNIWyWuNzek4NEq1zpny2z+53WHSRHvAtWQo+KVKGZG0G3Fo4smp1SOI+fLGZVS3N+vsBKpW40aNS6kQi2PKKPxSuN9QA89237Eqxl+HKDyZe9LjC6yCY5hfUtjGkQilRVEWbo+Qip+GmiSrukDiCObRAaFTQkT82ewWtC2oUJxdvGIu7tr3nvvlzx5/IhHDx/QtgsaU/HwoqUSy5tvnPHwQU1VdyidJRJIljAc98/Thut0UFBAaZFCdw8k8fngRKGVpjEG27ScXbTc3my5ud1g65qHT76CmJopXtF7RwiR0QX6fqTbZ58r7xPj6Ol6xzA4nMtMHIxgKk07U8zmMy7Ollydn7OctcxnLU1raWeausrDs9pojMkyKyVkX5GpmfelHIuKECtGJ6x3Iadz3N6z3Tt2feLlXcdH9wO7uKDXNZ2O9IPlQXvG5YMragY+/+SKL3zxTVYqsOtGBl2TTI6pLTPmwxlN8bTuotRQGcxPZZgoZdKfSi0afMSH7N1mtMFYi2ZKNVQHACD3sGU6r6aPmmtapbILXPSeRAaDg8+GjuoAemT2qbEmD0diXtOmY57qg5xyEV772fRz5zyq1E4gB4anmmoxOdZiExtVlc9A6XkpNaLWUoY1+Vd9isXbi8MgSHSW1ceJMXLCzjjUOGnyeSg1UZjO01FGeurp9VmHKZ8ZZLCF2qLKBxOdaTxZBhGZYjOLfB6VQkao0nEiqmIsWacJnYQc9pkwJT5HRBhDxE+GQKJxkWKgEXM0X/lf1hvCMUYyozNZc5O1iFlLUtxAZcKd8oXz1uNHxJSodI6T0gUtKp4Z5cI4opKhfPeU95pQ9RgjsQhHQooZycyWvwVxypupSTnVYnV9TV3d0c4Cto7Ebsf6ds2nH6+4X1lebRq2Ycbux895uvr3/LM//h2ePK4Q6Ug+0PeObj/Sdz2b+w33qztWNyu297esXm7Y7zq8G7i4mvHP/vd/zFf+i2+U5iWUM1duhhhQKhDciISIc5Hrl3c8enjFF778ZYbrbZ7C6JwaostFmlRCVTonPEjeiFNIeBeRYEACSI8bVpgU2G13vHh+wxtf/jrvfOt3SS6yv9uR+oA2TX6JEFBMXWP2AYh+wI890XdQCe3jC5KY8t0LYYwQJOsSRUrz6dG6I4V7glsTJfHp8x3/t//zt/ne33zAdkOO0BJBSWQXepJ3qBSptHDXr0n+lqHvmc9aWO3x3tNUBpWEeVNnrxAtzJQj+IxOJ2VpxfHOoxkShegcbWPQkthsthBHUqqI2rAZHC7tAeiDYrXa88FqizI1X6jPEG1Z7xxiGp5dr/icPUclxX2f2IUzmlQxpoQhYFQC7QjR8ezFc/bDc+qqYj5rWS4X1M2M6AN+HDDG0FQNs/mMKAodPWoElyL7OOLcFhfuUPoxb7y5LDeCLwitPkzo8lqUmTt5YY5l8y2lZDHL0imzl7QRdEXRL4YsQ6bci6os/CEe6LXKZAQyqYBCIyRUUAQRYlCkaIgxZtOkfsT5wGa7ph8St6sd3/7Ln7DuPO98/itc7zW9aNAzbvYebXrOFwueNMITveWNS83X3r7gsvHEMPAYRZcSm/2Iqx8QTZXJMdocNnxJBy7RVDpndkhBkfNao04YSAVUUYKEvD4573NTbTRKzGFyx/+Pvf/6sS3P8juxz89sc2zYG9flTVe2u8hms8kmOdQA0kgYPQiYP3UAzYMgaShQoEYkB+xutquqzKqsNNdG3Bv2uO1+Tg/rt09ENiVNPsxLAbWBRGZeE3HinL1/a63v+hrYn2Wj/k1pnbUIak+Lk2KUf08ptLV7g8MY4/3wHyKjiZHUrYdAwj14NEIP+8ZiPPRzUR9/MWawYA+WZkDBSKxNHrpl4DfaMqYxjD4KinuatTGGIkspRo+HUYYxgslpnEpGjaHMLfvNwQgySBF8AIqlJLTONLLLMngwxvnm1zKC5krrB8BO/oFjQo1SEvheQR0blRFgGIu2Qkkk3z/wdvjD9ftzmekkGxOyj1JUKZG8hyRpBtYYbGGxpgQjjEb2SS1kM1UxRkzZGFERUN7T7O7QyjOdaRZV5PmTGZ88P2A+g6ePS8oKSImh77j7cMV3v/uWi4tLlKnodcXdduD/+n/7d1zetFgDy4PvePHilOnM8snHHzGpZ7x6+ZIQHL/4k5/y85//mKF0LGzNQRm5/LAj+sDR4Yzjo4lQqa3PJ4PBeYPzisFrXFR0IdF0gd5D0w10faB1kd5HBg9DSvik8EnjEwSE6UDSImFUOgMC+fzMgHLME3mITgaH/Vk5stzk0kpOsDG9RuaBBywuhRhCR5XrRQGp4s2bG/4qdZwcG0rrOX30CGVrkj6kd4abbUUwh8xOjrCTJbaoWU7nFNpjZwWdT4SRzRcSha65vLil3ez4s3/6C+ZVg/cNmgXa1sgwoUk+ezGoyO7yHaVJbLcbblcrHn3+U46Olqj+jubqlu1mx6uvz/nu67ecn6+5ud6yW/cQEoePlvzpf/2/IY2xomRtvlc5bWMgxVtcc45yLVfvNvy7f/MFX/z6nOcfPcIUJV+9fMum83ilSaogmSrLSuRcdGiGLuCjJRrN1idScLLIChIRug1rtIKbpqPMcaFetSgd0EYzuEBKmmlt0YVm17TU3kJREIxEjYa2A6Xok9D6V11DXZUMqSDGRHO7pvMKU08gOJLrcmxpoOlvaborKuQ+mZYVk7okpoSPka7bimn58hBVzPCUrLxGh5oilNTBUOeUiqoArUqULSgmigNTs16v+O7te5ohcHjkmFZznAtELzW5NAalXP58Fai0TzL4/og1MhoiApGNkr+475GSkoQp8V3VqJTTKKzm1FQsj0u6IVBPZsyXB2grSxhpNKS/8B6CTyRR9+R+IjD0w34Z431k6HqqumQ6leS/wkrkprRcwjzVjFL2BIw1Vw6xEGUpEgMMPdxtBl6fX/P1qw+8Pv+AizLkzuZHaDvhri9YUdNTEuyUojIsZxMeHyx5uih4siz59NkJdqIImw3T4YauPMCrKTFLn7Viv7jZ02RVZgqmvLdIIpUkJDGI1wpjs0zCWHL7iPcuL4ASwUkVN5nRIt5t4ziX9r1HDIHgw76vkChfYeSP0tMQfG6VNCGEbOQ4SmvvE7LgXto6fg9JnpDepywKRjDBaAXYbCjt972EUirLRO/7OmttXh4VEsMbBSxJPqcO8n1z7tEfKiL3rTJm3/eMZ2gIQUBjo7KvWO5vMhBz3xDm3SDq++DE/8L1wz0Zyvs/qpAtnkqiyVajMyi5yUahk95HTu4JubkZHVMURU/iqJTmaLZEa3h3fUNU2ayOQAgIWp7k0B/dPJVSMoyMVGslu7rcHuxTJ4RSJy7I8ibJ362LpTSS3G/EFOw9EMb4DwEncu68Hr9XblRT1vkks3/DR7rKnpoTx5s50rQtf/MXf8XPfzzl+Fjz7JMTCA0gqJa2BU1U3DrD+nbL7faCP/6J4tHhlMgtTdvw2y8u+Nu/es/7ix0fLhu2246uccwqQ7/tOF6WFNazWRe8+uYNL378GZOyEIPJjNjJIRkodMQNjhQTza7n5nrLj//4F0wLi+v7fIPmrGyVSCpgTMKUCmUiUQfZKvcOfKY86YB3W+KwQ4fIxdsPBF3yi3/5r7CFob27hDYxZQJDSwo7ku9wQ4vvW/pmS9PuaHYbdusdu3bHyfNH/Hz55xSTGWgrgneXUN6QopEZRHm0blHpkt36nM264dUbx//wP/w1v/vqLUOoGJSiiyKtMSpBB8SM7BeGrhFqWHKKpvGCBsZA0XtxZm0GVIrMS8tB7TEqmyCamqbrUKGlLmu6wdFFS13XNCnRNDvK0pFQuOBYDwmiIgXoVcFgJ3QucNQOHFQTfOhoXGTdDtxuNpSzGRc7y5o5NlhmpWdeJiZESuVJauD06ad0zY7z83Murz9QT1ZM6gmHh4d0fUN0A5O65lF6RD3k7U90JN9iVMfRseWzT095dDpF0RJjh1J5o8BI08+njcogQwqZ8eMFPBg7Q6Vy5KeW58UIaCDUWGH3RMXe2Tbq+62XBtHkMXqNyMPkomJzN7DddNzebrm4uOTd+SXHJzN2mx1VWWLKGZcbz11vKBrNrTMwmYItwHseLed8fjbjs1PDj85mnB1V1DYRhhbfeyqvWHjH9W+/pDn5KfWTT9BFCXsKZPaRUeyjKZURDxK7316PlHpyYxAfADDmvoCOP+M4R49mQiPyvC+0fG/4HwfhmBkM3zNsTPdI+h755sFAPBaPlPaDu5jy3q8QxHSW3GqMWxgl0jY9IuEy4Kv8PWNmW0i6mrCtNJkForLQZu/gHPcsgpjTJWQb+fB1SYGOuRgGwj7ZIcXx/bgHLB4yGsbUCh/I0isBZNB5I4D63raA8b3cMy4evJ+K7xXsvQ9F/oy0ztrHJLIYUvqeHvIP1+/fFZQiyIOW/Zhg7GB0po6GFIneMSiH0pbeDOIhpHMMnrayPdPspVIqeWqjODxaMjGOkwPDwTJwcmJ49ky2mUYl/OBo1w0XL9+yuVlzdxlZXcHpkxO+/OKcN++vWfcQzYTGRfwG7KUnvN9weTlA8HS7RnyrfEFzueHkeMq0LJnagRdnmqqcMp+W1EVEqx5pWCLBJ7wLFMlSGosuC1CaOIWYDM5pepdoXaR1icFB20M7JFoX6FzChUTvAj4ZfFSyJEKkp9liDY+SX8ush1EqJsdHZmqF/FRnjxSytlg4DfEemGDUdMt5FVVBUhPWmy1ffX3Fuzc9f/SzFxz86CmpVMR0yOu3N3zxu1tudolqXqDsFFsv6agJKdIMnk3r5VwgMgwDbvDcvL+iLi3Pz6Y0m9+x7VpMfMrZR0u0LrJW3UHw+GbH5sM5s0nFh4v3TI9OePLpp/huh7u95vLiHa9fXvC3f/sdL19uaVsNyVKpkkWpeHx0wKSekDI7jmBIPshiJUWi37G9fUOz/sD6xvGf/t13rK52/PTzx0zmC1wAFzrefFiz2vUEJbIVN5qkx0Q0oI1EtQ8hYMjm4VqBLWV5hMJaxbaPuL5jNilweofVkdKKo//gHJ0KlGVNj+fd7TWzyQStNH3wrHcD1kr8eYqJbbuldAUWqEyNJ/tpRYeOHWEI6FRCcgztFTZtUdoTAmgsfuiJwOA8zjlMJZGfKci6b5uWDOqMMhkO1I4QWoIOBJ+wVsyvY2mIsWPnNtzuIuer99SzhqqcQ99xMml4cnZCYRWRXqaEJBCCgXHayj11njpSNp9XSRao+7tTfD0E7xcZ5t63RElYe0xemCZTQzUx1NNI0lHel7xQSAnqNAoLRlhDZwCvgJjQWUrrB2HfGCSeVpj4AoCHGLJ0KeUeQROCZ3AB5zVN51ltNvgwUFrDZuv5q19+4H/8n79h1QaK0nJyvGBeV3hT4ZXhZtPTePGrWlSGx8cLPjqo+emjKR+fHTAvDcZ4+s4x7xVH4RrnFE5ZnJ6IwT4xJ9Lkmmx0NjkUY+2RjYDKkoKEeJkE6dujuHDmRU1gPLmFXSVnuE9RTPVNXuI8HJTHJXbuK1QSCT55FoTcS4x/b79EUfd/L0niDUp6GJPZC+M9MvYPucQgC5Dcaz1ggybybKl0HujzsiSzX/dG3wqU0XtMRsZTYb0KmV496NlUjiC+ZzGMjMwYRSKtH/SJPGAy379P6nvgzA+5frgnQxj230ycOLPrZN5iEiTHVMZXRUgiPZCicL/9IjeWsgQXM0dDpN+tCMEJdR9LIAAGcdzNRSR/CdHe3m+QUkrYB07G+y2dFhNKk5vJsRAlkNhJIw1n2m/51N5J2bmMDGlx+41JSXSQfFdGpsJoDKn2XyN/mPm1aCPf0UZQQdHsWna3iYOqQA0DBtmwrbc7Xr6+4WpXcdXc4Hzg5MBDOIMwwZiG9d0tf/kXv+av/uKOpje4qCU2x5S4HrSqCXpKYTwuRM7Pr1jdrqmXi3vUVUHKwzEqEIYB7yK3N2v6LvL5z34Mw45Cl9JgpJjBovxJGtAlcgDmzyW4IAccEegYmluU77m7vubmbs3Tn/+Cg8ePSf0OhjW66+mayPZuy+Z2zfr6js3djt2m4/Zmze3tHZvNhl0T0IXmX/5v/4Sf/fmfCYKYIPkIHnTMyBtJmqV4Rd++o992fPH3l/yb//tveHdxx9PHZ9hqwpdfv8a5gaQNZVkRlQzBGtko9z4RfEBHhZV4BrzvUNYSTGIY5PfbIbDtHNNCU5aJurJ0wdNu1symib6TLPPZfEZKsNtuMMowraegwIdIigpLCdownVek3tO6nnldo6uC1nsqp/mwHjBhwk2YsjMHRK9ZxcBkcCyKwNwGal1hTY2yGsyKpCKr7UAXegYagu/RJFo/MLCh0i06bDiaJ87OLI8eTTh7vGR5UGBNS0o9qCEXyrQ/VBPjM38vbWEE6fZFV40PCSO1SZ6AcXcVsyZiRBnyITaCe+M9en+zEjGstwP/47/9JV/89pLL247VtiOmxL/4Z5+Tgqa0gXqiuOsslxtP++aadetZzOecHS749PERnz075JMnC54cTVhUCh08vh+InSM5TfKau+srvvvqS2gKHs8PqI4f5Y2WGhcKeWgWJDuEuD+AR9AxwQNgIGVmxkifHs+t3ECQC0Om3N+nMXwfMPiHMob9SZfPv/HX9skPuakU86L0vT87FkOl+J4+UORdmUY4figRGOUA/+Dr5yn8e68zBJFQjX+Oh3+WEdu4L7hy66jsefDgZ8zA1li098DLeJLtgYaRbj2+Bo1GS5ORHZeTEjdmY8z+NaU4xlpJIzC+npFmuPdfGOUX6p7ueE+HvC+0Ot8PYlz1BybD7+vVDGHfS4SQ2VopYZXCKlk2aMZ0FKFlJx/2oJ1ofnM2uhEaslEJHR198jw9mmOtYbPdsN7cEvycaWXY7XqS92wub3BNj02G0pzgnGO7MzSv1wyxppo/4uSoYtt0NM2OqpTFzdHygMPFjEIpWHiiczR3O1a14XCqsRVMF4bSFMIw0wGbz+oUIl3fM3QDKYw+BMJCQMvPWihJ45oVMmiFHBcdomYIlt4rukHMi3edox8Su86zaQZ6F+ldJHhho+pk8NkI02idQWUtXj1a6NPKjOdRZjHsC0TKyT/Chkgp5N8XdpPC4KkgebrVHctnMz76+EeU9YLWB757ecv/9B9+zZvzLTtv8ZsPxPd3TGcHLKaHzGtLCpr1ZkcMQrNvdxu6Zsduu+XpkzOsDezubri5vqYoFI/4Yxh7Qu9RwbO5eMfm+obrvsVrxbMXH2FJ3L17y+76jndvLvnqy3OurwN9o0mD5ng5o9SRSgVKa1B+QHnxqUkuYVwG9kPH5cVLvv3yS779+h3f/vaG8zdrPnpyRmE0yTWUWvPjFyfMJhO++u6C220vw1CSCNWgoao0QckmPJGB2FzntClIKWKqEm01rm2JJjIkxd2uxeooKQOFpXMDbpcoQ8T5yHa7ZdvsKGwp5twpYJShLCq01vjkwDmUsSRtKEqDVRoXB5EGxARegQoUqkXrgZAik7LGpYjzcqZvu56YErUuCZLLiVeJNk1omWGCwUdH4wZK3VMw5LjpiHeOGD2rJhHtkmgS7+46VqtrDkt48otjnjydUtoOxwg46bwggH0zur+kP5ZkpYAiGz7n4XR/jZj+2OMoQEd0ETLID9oEsH7fP5EXnWRPNOkqBISQepc3oCh01Kio0EUi9Akdkhj6Icu4GMXDznsnQ22KNLsdiYAPiavrhr/525ecf7jm8bNDfvzZR7RD5LvLHS+vepyqqVMNXcUuKm6HnTCTnOd4OufJ0ZJPnx7w0aMZz05mPJpXTIxiGDoG50idZxpLFqqniQ0uzdhR4LKZfUxjFOX92yrvU078CsIoSDFmyZq677XGJYNWGG3kE8mS2nHhEqLMLio+6Lfy3x37AekD8qyj8secVF62jsvkuO8Dxs9Fvn82Tszn5pj+QJYTj31FGqUXYy+WAZMYAj54QCQfe8+q8Rzkns0qP/roA5Z78CSMkDhaCyTyYl3nXip8rw982NP46PchDg/7nPH9kdv1+wuXH3L9YJBhmhpGba3GIhivmEOkjB4lNZKouUeVcusoGr2ITkLzNgqs8hTKUxNo1huGwVNOFhCdmCWZAmMqfExEownJ4GP2c8j0FD26d2bK66hfUeOmikRAzBsVSDLDODgBjJEp+QMcG11rRtf4sUEfzb4eDA/5YRetdqZJKcTxNP95lUcrnSlqTR+5uxt4fLpgt3GYIrFbDby/7PjuzZqBKVFV+OaOFz96zGE9YFJBjBXvzwfeXXQMqUCVE6wyzGrNbDYnBSizBrsuNXUV2LWa66sNpx+BLuRhGDc06IQ2ihQCbTNw9WHFwfERR8sZyu9kyAyWlAw+GnGZjgPFUUV5eiyNQFIor6QgpCDGOH7H0NxB13L1/pL64Iinf/RHWGsYNg3Wd2zvLvn2i2/5+svXvD/fsLru6DuAQiJbdJTmnkA9VVkTGYS2GmXoSGncpoobM2nN5vaC26trXr3s+Pf/0zesVzs+fXHEyaNDlLLgT/jqu/c0Q6IwmuADIQQqW6CSJnhP3zvmkwlaW6IfMMZibEmMCVsqsJbBD3SdY9fDZGKYRUcKia53aN0RQqLve0KMYtQTA73rUUTK0uJToLBieBmTZ4KnsJaJMaRiAO/ypqxi7Qt8o+nLCU5VZGEJLgaaIdC6gaXqqYxDU3F89ozlsef99SUuODZO4QdFoRUTUzD4SNdf8/RY89GLOY8fWw4ONfOFwxaeSMgu/Pm+z3OdytSv8VKZNqjG5wjG03j/bIyP4Gh6+L1DKWV0L2U6sRqfsbGqZDo+0rg3jeevv/jAr79e0UTLEAUg/OZGoaLGDS1VqbnZCfVrouDzz5/x5HjGZ0+XPDuacLwsqIuATq1sPYaA7yPBaVww3DrFX/3mLW+utizndxxs1tQHp5AyLT4fHWE8nK2kmoQsE5ANfj5T8gEtWfJx/3MIRJnpwJmh5fcRcXnYVmNBu5dm7N+2lPYFbYSv/yHAEDNrKY3H0DiXIxXzXobAvtCmjPmPr3As2COoOsoz7qmBZMT3AcI9Fr2U9v4S+1tDC7Mt5I2kRCnlr/lAa7hnOeRzVVJ0BMWPcXQ5frBcyN/7vpHLDtRGalJIgZBkKrk3MVJEFfbvk1L/pc+FtHX3Rfb+vld7sGJvfAn3n7f6flzoH67fr6sNCZ2bLR88KSaKpKiUuc+C15oXTz/i+vaK6D0xZX+VvFGTCNx7GY5WiUInyhQIrqO2AUPH6fGML79Z8+XXN8xnNa5r0DExn045WB7ghoG1q+jsIW6IeCxFXRGIVFpRTgvOjuc8fXTA8yfHHCznxODxw0B0jsW05vh4QlF6jPFYnSjtiAdnY8WoaLqetu1IIaGjyttEAS/S2EvhMVkHDfKzWaWxxlKVlkVdyCJGaUKqSMmwa3vWG8XgA/0QaIdI38OuE7bAED190oRoSNoIgySEnMhR5F5KEZQwTBKQjGLfto40aqQWxZRQEbySyNBuCDx7/gmLgxNuVzvOP9zyH//Tr/jmm/eYYsbEGBofCL6l2zlit6EvNNpbbm+e4N0nlEXCtTtS8sTkeHR2yPr2Bjf0tG2LCXFvEKtiRAVH7Hdsri7Zrde0ruPJjz5neXzI3fk71pe3bK63fPXlG66uHde3PU3rmU+mFIVGxUDrBzbNhts3b5geGKYHC7SRHmVze8vVxQWvvv2Or37zki++POfycpCI1as7hhR4fHqAVYnKRJ6cTInhMa/Ob7je9PRBQBpZBGr84AghYa3CeY+1BqUsMQYw0k/7tgfvKI2mUCI5TV7WgBaFiQp6hw87YkwUMQkEFQNKRUlj0wmDJUWHigN1VUnUNJG2a8EaAoree5Qm+70FCqvw3qGLElNPpCdXis55dgNoXTKoBSlNiVHTYulSwLHBJ0M/rMCvUGFH6LcQBlIMlFZxtFyilMUWGlPWvN9c0iUDVYmyCmUdSbt9rdVaetPvbez29Vft9yyQ+yUFceyb7nGA/aJBFgsJCCKbSHpfo8f6I2Bf9kjYD3YxLwoTynAv1UxKGKVKkVzCBQiDoWsV29ax3rRcXW64vl7R7BpOj0oWsznfffuK5x8dMT+YcX655j9/ccHdpqc8fMqtm7Lpd1w1nj4Zki5xQbFZ74g1VPMJH50c8eRowePTJc9O5pwtC+a1wqiACo7kAqEfcEOUVAQiRewow4oJhp6ewdTEohYnNpWXAUF6hL00FAgpSDR47heIgeQTOvutyMJYlqOQ+4Qse9TjlijPhfe90Rj/uF9t3bM+yeulvHzZ25JF8dBSeeYca37Iw79GCNcksqFjZnwmSDnh6iFb9WEvY22RTR4fLoayND9JDOn4x7XEYHxPtrmftyGzYFT2q7t/raOfzd5ocwQU8vsQg9zvD02+934M6f7r/5DrB4MMP/14iveevh/wbqDvA4NLOA8+ahn+tRFHWB9RtmSMJTNGKLSFhkI5bOypVeSwMiwnhsoo3KzChYqQNC7CEMWRNdHjU6brJQEvAloQ7BSISsLMBCkaDbxG5rYUvZgJefeklYx6CXTLaLqRUmBc0I6RSuNNM54USTBKQbjIIEqmv8Tk8nwUxaxMCWqsFRIjmCbs+pZXFzv6fmD5RlFWnpvbDSHMOTqa0nrDrnMsDgz/u3/9M6rCo1RJ39W8fBl4fxUJukAXFWU1oShr6nqOVhZy9CPGgAk0Q8/7Dxs+bj22nqCSzq7OI1tI0Tct282W1XrL889/RmV6fL+jaxqapuf9hx03Vw3bdU/nOz7908/5l2f/NdPJXJr/oEhetj2JgWZ9SXt3TbNa03QDL/7RP2N+eEzoOu7evUe1DZevLvjbv/iC8zdrXC/5ukVhMEWB8wNaG0IoGYYOFLRdh+97cKJtTz4K2p0CVkdUHHj38ju++tVXvPrumtevV8QBfvLpCWUd0WzRquCTJ3Mm1vDt62tuNh0uIHmzStH7iHcS26iNFcZD8JRVgfeBwYf9QGaVGIApAt4FHB5rNAUaHSJ1UWLqQO88pdZUsxntrgEfsKWlLDSD6/ApMJkvwCQ63wMD1WxJ6rYE1aGqI3ZxIClLrzSOSIrS5Hml0TK7o1KiSR3RN4TocN6z7sUUKPgekqPScHy4ZGITWnkeHc14cjbj5EhTVA5ND8rmU9FAHCVGDyrnCB6MGqBMBxZvgBFciPm3R+kUBBVISuROwkAajb9EQGdy9FCIKVPj/B5wUEmMFJ0P3DWBOwddHBHvwDevr6kNlDry5KTiT378hGencz46W/Ls0ZLlzDKbaCqVIHWSRDME3JDwAzhv6UPJda/523cbfrPW9NWStuvB9egUMYgx4cF8Rtf1dH2Hz6ZBAfFjMIUWGmWUe1QbxdA24s8xmxCiMLxISt7f/VZCsqaFxpg37EaNxuzihZKvOKL3WqpcCvdeBA/NCOPoRSDIxvepdaj92SbFKD7AN0aQQc5HH+Q8E8BXY3SRGSf3BUY8I+7PVGOMmETmw3asRXI/iIRmz4RIAqQYbQW0SomYweP7Ypq+93ViCnugQGWURKOEVpzBgZBi3jCwr/QhCMid+L6WUICYB2wUdV/8RybeQ7PLh/GUD0GF8WcaI6v+cP1+Xq0X4NqHnIAVogwiCra9wxgorKWcTOneO9q+x3mPCzlm24iZ3bSe0Dcti+UB3TAQrPjKtG0niVDeMzk95eJu4OpyTVU6nG8prSGlHZW9xXUdVkFhFcvJRJYzKVLgOVyWPHl0yqcvTnl6dsByZkFJHFtMJUZr6qKgrmDwgZjPWWPHezNLWZXC53MlJokDDz6hdMnY7yglPgBRp2yiHUn4DEBL+obSCmUVdV1RlCVWGw6mnuM5oBQ+GgZf4rxl13h2naPtIrs20A2ezg0MPuKjpvcalwpiNIQ0Sm/zqkbJEBbS+FNkZgPjRlMWSNZWzGdHHB4/5f2HDW/fveWrb97wm9++RZsJB8uaqGAeHD5v96IfiH6g2UZuri4Y2pZZkUhdj+tb5gcztFX0u47Bedpu4LCscmZ9InmH8o727oZms6brWmZHS84+/wy32XJ3/gG387y/uON2NXC3jXy4GTg6PqCaz1l7tz+Xbdfz6tvXbLd3VLMSW2riENhc7xiGSNt5TJgymxzSHzmazrMeBprzKxKaJycLmnZHRPP40QG2qAkvL7jZdpIQEAKh7zC2IupIUjlJJZ9/xiisNpTW0vmOSWmYlEZSOguLJTCbZJaDcxCFrRCVwhUStW2rUnTsNmYvHfEqc96zi4GQAkYbujAw9J6kNVVVUJWStqW1krhnW4AuGJzKLCMtcd9Oo41m2wVU9HQhsesbMAM+XpBiYNes8MMOlRylVlgV0ckzOzlgUkt9HZLicrujC5Zi/phQRAYUXiWiDlk7HcfBYN8XCag+zgfk5yWDEeMOBcTLjHx7jvVzT2nPtW78fR74BY0DnpLaOtZlyOBfUnt2Ssq+TSAxhYMLnF+s+eWvVnz5zR23jWO1G1itdtzebNHB8y/+9BN+/tmSb9+11IeHMNNsgmUTNGtX8m4N/s2GzfaOy+s1lY3Mp5Enx1NenMz5+OyAZ4+WfPRoydG8oi7EUFQDMXjcEHB9wGWfiN4p+mS5bR2/ef2WW6fp9JyhPKB4/An66AlhHEkzEzMSMUmA2nHporRFG/FKUJo8C4xzXAYk95t+sim5QpksD8gg630CVZZsKnnvxqUaShFDxEeXPbPGpZrezw37jlhBDPdSg+DT3kBxz2rPrNKU0yOI932KuPZEMYIch3j14IxDQAHxllL51+XPyWJr7CFHM/60Z5aKwWUOVRh79/xPIs+4mYGaRhlKXtyP18jchFEWlL7XR/3/u34wyPCv/vkTYkq0bUvX9nSNp9l51mvP3caxageS0fRBc7tpMZnSolKAQWheCs+08BxPNU+OppwdlRxMdTYhKfFJ0Q8KF+UQaV0iREXnA4OPDE7e6MEj8S2ZDisawCQAREIQvJS1bFqK+9iMqhy9l+2EMndB9LSaey30iGrFnJ8axq1h/nBVintWg3P+3uRjZEPku/vhhxWTwUXL3U7RtA3lewfa0Q2ebVeiC0OZY1T+6Mc/4o9+9gmkOyI17y4G/t2//5p37zs8hqgGTLWjnkwp64GymGJtwaSoiNkzI3WB8/MbNndb5ssFyqj9kKFVIkXPbrumb1uKQvPkxRla9ejQcXv5kq9/95qvv77lw0VH6BPJBKoji3MdMENFcXQV1xCHCj03r19x/fY1bduzeP4xJ59+gk6Rq+9esX1/zbDa8cu//g1X73eoVFFajTEFPgSUjgQVScrilGYzRLpN5Phmy4dX52hrmS6WaJ/AB5QKmMIztA1f/PIbfvm3r3j57RXWWE6O51jjKHIE19APVMrw/PECaw2/++4DNxsBsMTlX2rJtKrks0LMeiJSGOtCUlNSCNRFQWU0JgW0SlRFLnxWdJmTqqJAkUJDcJ6yrCmLCc1mhzKeRVliTaDtG5JOzJcHDOsW7wdCUCjtOHm0ZNt09MFiVcLHnhSkyVQpoEPAqoBPG66HBtds6YcdIXr6YcC5AUXk5GDBs9MT8C3zIlGpjsODCUeLmklpM912YKQBjqBB8io3HeyZCQ9p+yMgnOC+2cxQ3hhr+xDwT/ngi/dnp1wPaFfZIxadNNGD7xP9riX6wOWbG/p1T+wHtErUNnG4tHx2UvL581M+frzg+dmco7llMdFUhaLQCaWcGIU6ccYe+oB34IPGx4LOF3x7teXvP+z4trX00yPmB4ZFWXI6qyiSsEVUsugYKAuN9zqb0OaDHUQrq1TOb07EznHxzbcM7Y6f//HPWC6PWLW9AKWZ3qu1FD6TwcqQfD4n7g18hN7G3thWc89YELA+SVxkLipaa1K8///Rh0De6u+jz3un5ZRjQjM176EEYGxuYkpEN/yXMoEkUVQPaXWj8CHm/45R6Lgpqe9/3yjfNyWwWd9rjJF7ZNQJRgH37hkD3H/9OAZPytn8kFUgbIp7qYlCmoAQAsaa/c+qtcYFfw8u8ICVkPjezzr++iipeEg3HH+eMd3iD9fv5zX0Ts6rKIkkBDg6PWYxmXHx7pym66lrxW++/obtdoMpC7Zdh6lKhhAotKZPHhVkW5+anmFwaA2GJMZ6Od7yN29vadaJzVCjXMLFQgyzFXzy7BHLheLV119ytDDMqsTJcsqzk2NmleHksObZkyOWU0thE0Z5ovd4PBiw1lBYGYLG+G2trWz8EmSEGBVhOp9BBO8T7y9vuNmumE6XaCXpFDp3SwZyUpj4niTF3khOEbFJgFabfVAMjrKImTacmKoCgMNlxHkIrsD7Ch8VXR6ce6fYdoltG2mHgbYPdAO4qHFRk5QlRHAp4WPuw1KO00zS05kk8aJFveC3X7/Duy3rzZrbVYMtD5ipQhLOVKROgC6IITB0kWbXE71jfXNFv9nAZIJrO9arO54/eUrft2hr2K17mjaiVA1Y6YG8g6Hn7sN7VqtbbD3hyY9+Ql1MuX7zin7rGFqhpN9tA+dXO9oEM6253OxIKVEXBbN6hikLhjZx+eaKzu1QydNsduhYoEwNugCveHL6iKNjw7v3t7y7+EDnIi8vbmi6gaOjCTE4jNqyWEz45PkJ+vya222DC54QFUSRls2rEp00KYhBaV0XlEahCYTgmU+nTKsCFQJxGKi0pbal+CYpL/UwZVCAJEzMNGRauGbwsvxAiZQ6uMjgO+q62surBQQrM8NNYawFa4T6HhUeI4uJICxqZQuGELi6vcWFOzoHg0uUVUFKjhAcQUdi8tSF4dHRCYu6QKWe0gR8d000GkeN66E0E5KZoI0nEAhZE6pIWZ5D9vMZjfnYb8IfNjV7icRDuWlK7OUVWWo1CsnHS3omsWOMEfEg0vv95oPtunReCcgTs/Qf2beBmBi6yIf3O/7uVxf8x79/xyYoBgqSMjQ7ASovO8szV3LXat7feXap5e3Flm2j6PvI+Zv39P2WuoCfPDvmX/zkMz46O+bF40NOFoZFpZmUEnuuCfjBy/ImaAYX8xJa5WfXsPGKl+9v+epixS9fvqaNGmVrjh5/xLNHT1B4UqaLiu8fe1+B/cI3CUAQo8gMtJEUEqWFfZ6nuGyjlb5XlzX3Q79ICnJdz/KKvdE/MkgHn4WySmay0RD4Xn6Q9gCAxF/a/RIlRUkcC16WIvsBfTS5zoyAvaw0v7ZxSeGcLHh88Hsw4f9X/+a933vUjF5ce28vFFHlQIK8CFRK7WfB8bWOoETK0tSgckpZfs0PjR7Hf//QZcoPBhmePJ3KGxoqgnP0jUfFiq9/956//ev/wM9+8QtOn73g1fkVhJbUbwl9Q6FEKlBXhqPDGU9PFpzOLQdLy6QCox2EgDUWlBX3VBQhamKy2bkdnBfEOyXFMASGQYxKvE+0nceFREgFvUs0fcBHcT12yL9DMKT8xKa92YksusbDY9RDxzwVjXmho4GZMqIhVBk9GptLbe1+IxbHIjwObKPOOUOVMRmUmeCSxfmBFC3d4GidZtf0qOiZVJ5//me/YL6YM/SJ1Vrxb/4ff8Xb81swJSkb9QzbDV3wpF1DQoaL2hgqW3A8qzisEgdLz+pmxdmTU1SVH8hsThNch+9buq7h6OyIg0XN3c17KiJD03H5/pq+DxhjqSYGU0RqreRNkyeR6AcUCWMSbr3Gbzb4zmNnS05/9BNmsymbd2/Znl/gdz1vXl3w+tUlfrD4YGkHTxdaOu+ZzeaSx+0jEcNNl5gkxWY98Oar16w+XFJMJtiy4PT0hKNnS5q797TbwNtXF3z123esbjuWy4rZxKKpubtr0QlOjg7oXIe2kadnC5ROfPvqig83W5SVwu69FzNEElVhMKYgBUdVSlyrUaAKRaU0tRHmSIwRFUQDFVwkBomODMGQYilbJBCDynJCM3hUHzHW4lTA94Gw7RgCmKJmtesAhakMQzDyTAwN4CAFnGtJvsO3O6IbCHR5u2vR+bCblAWPDg+pTOJoWrKsEkVhOV3UPDo+ZjZVHCwStpB7U27W+4hAnVI2yvN585s1Z1IJ8718v93V2U39Hh4dC6EGFffRqwBqNGlVSrYAWY+Wsg5TYQk9XLy+5O2rdwy7gaOjE+Jm4P/wz57zL3pFUZWcHi04OZ5zejhhMbFMysSkiFidxEzMJ4JT+JDwTv4JHrxXuGRog+X93cCvv3vHFxd3XNspfnpMoRJPpoqffHTAL54tuUiBFAu8NtysNhircu8gTYNOWT+chIUg/MXIsNny27/7G06Plxj/MZ88+SN+8/L1Put4bBRIkZgk/o1Mq1N750XZ2in2GRFSVJx7sF3XDzYcDzbxD669R0ManYTjf+EvcK9HjPkzzo3UWEhS+t6wD/KZDX7AKIMxltGvY5+MESVac19URxaBUvvXnGJiGAZikrzokONXR7R8pIl+z4OHtAdSYGQbqL2TMkAIAdd7tJVYqphpzTrLM+41ht83dhzfw5iLbcpSGBT7BmB8j/8hKKG1xnsxI/vD9ft56RD3pqM6KazSNLuW0Dsikc4NmLpk1ezoBkdyjnZwAnpr6KNsPkPbEYFV24i5atQUiDeTTkIfX+9aktLo6YIhBlIqIQYmheLo8IjCtTw5nPLkyPDzH53xyfNjzo6XlEYzqQylSRDF+d7o0UNCVPVapTzIaFIS4zdjLKhCZKVKvEwUAVMYeT6jxtlEowOBAaMixojnUvKRWV2hopznNmXpaEp782+hI0eS8hl2kIjvMYddmn4HNmJ0AptQyiKZBQU+CODQukjbB3of6frIrvU0bWC7G+gHYSD2IcmyKYEL4rqfopjc6NyDJeU5f3dNwuEDJFVTVFa2+NERvEPrxEfPnlJWlr5r6NslybcczCf4tiO4ktXtmsF5Dg6WrFa3FHXF7dZxvR74xIs5Nv2Aco5ht+Xq/Xu0MRw9eczx0xdszy9obnb0W8/lhw1v317RNp6zR0c8ez6hqEoGN4jZpY8UKVDrApMMoYfa1qTk2fqGYehp2h3GliRTo+yEUimOZxP6gwN80GzbLe+vNvTBc/boEB8cJjU8Ppkxnz7l69dv4XZgCIoUBypboJOH4DEgsqAUUDEyDA5ipOk6vPeUxkjPHR1rJyZ9KclmVulI0sKM8S7JtlZDSg4fI2gjm2KbTQqNRdcTSAgYZY3ERoeADz04R1Vr0I6+6eR5tAJ6+RBxMRJI9IOj7RwxKsqyIuEpjGJWGaazOUYlrApMC0eZHJMysZgUnBxOmR8uCfaAN1cDf/fb9zQh97O2wsclISTc0BO8x1aJokKkDTlNJCv5hMWnH9aEERlgz8hLZH+4fI0+CzLY5r5AjQJrMeKT616KjRrJEPleVzIcp5AgaIgKFQ2xT/StoRtEot1FQ9CWZCyREucS7y42FPE1785v6do105khBMWLkzmnPzngxYsjzh7NOD6YMp9UzEzB1Goqm7B4SB4QrxXnosxlLhG8pQ+aIRiGVNAFxfn1mt++ueTL1+dsUslWzXB4Jlpxdrrk9HDGXT6PBDI0KMiM8LzQUiqzASClsB+KAaKT5QGAykbRaGHHGmUxRhzjYmaa+hj2C47xc9IZpBEGgvQMo3wVfV/rZTET9mwDY22W76ssp1B5IZTlCRkgkP5hBBxU7pYzyzS3OnvQQUlseIzxgXnk9xc0gZTBq3tJg7WSUkESadteljH+fLBXGMQU0Yj8T+XeBYSpJwlfgZQUIXicS/uvYfJM7Pzwg2rqDwYZ/u2//UuWh3OePj5ifXfH3eUdTx5/zF/+5V/w7u1L/vy/+tek5HH9ltBcUbqWjw4mPH50QFlAXWmOD2fMJ4rKDFSFQ6lsnBQDNsHQ9wSXEURlUElhy4IQPLW1TMtAWZVoDDEWkEp8SDiXfRuSwQdF6zzOx8yGCDgHmJJt19O2Dp/AOcVm2+GVJiSF97LN1kbAiBhAG0tVV1mOYSS2U4nppehxJCdaTox73Uxuh/MNnAcpomjXwmj4Jt8jRM2AIRhN1AEdHU/PDnj69BHbTcdq3fO3v/wdf/fLb5kuDimSYdv1eNdgnAKdCGGQh4NIiBrnLZtdQ2gDr9+2vHnzmk8+/QhbVvdyECLBdRDF7+Ds4ycQpUiasuTb372iaxPVZMmm7XAhMqmrTAXKcU0xEpwTDw0i6/O3NOsN/RA5+PiMs48+ol+tuHn5kn61QaWS715fsR4KVrvAqum43XUMWft5pBwpemxZMpuWVMs5s8oQk6a5aWhurtnsNujC8PzFE04up0xm8LvfnXPx9h1nx0tm1ZTe9XQOVtsNlx/WkAIfPQ08e3rCan3DNHiOFjXms0dEPDfrDj9o3OAotMITiE5RloVoLPOSJBqN1QVeKzbOy0AIeZMc8UHjHAyZZdN7ReMTRZK4y5ACGNkYGRSYqbg7U6DqgqQVIfTEEGhvekIEHxtCvNqjsc4NpBQgRmZVSV0XKKVYzmuODxZ5WzZQW8XRtGBRWV48PmYxnzGrS+raUlaRsuopqhboSckTSGgexCnukfYgx2H0MkAL74eRnhp5wETIQGsCokp70xvxU0beLyNNSYxjJnAQaUYyuC5xc7Pii7//hs3Nlkenp3z68QvmM8vZScdnnxwwncwwxhJ8wBQaqxUaQbW99wyDxw8eP0SCh+AUKVpStDg0TYAP646/e/WKv/nuA+frRG8XFHNLnRrq2PCTswn/xz/7mHI+MFxeM8Q5rZnI+RALVE5DCEpehy4LtBXDpRClOUvB0TYbVtbTth1fff21mBspLe9ggKP5gmHo6Z1sgrRSKGuIIeRzQu2N17SSX1dJEgzGTOYk6//9f4+0v3uq3PfjIcfh2DsvBWwszPnXkxbK/7jpH8+z8Uop4TLIIXFKJudG+/0W3xi9fw0jvBRDIKVcMJPe/33ifYEMUbTVIcpworNfjqDpQXSFI4CihJVGPmuHYdgX2REkGB2WyeyHfTThA3bHMDhCui/O43tWFoVsf40Rfe4D4CaEQFHIVna/JXkA2PwhXeL39/r0ZE4/9MQEzgVc7wjbGzqE+VfrSGoci5NjlPG44DAWogr4GCCS45TzntJoolboCBOVOJrBJ4+PMcZwfr3iZpfPVW1QUVPoxLyE5voNB8bzjz875uefHfOjj484OKwwJuVoPcQPQsU9m0wc+rOBmbbikYCiawfAU1U5T93o3EkbScDIVNuowafEq/N3zJeHLA+OKWvDkHpcGBhixAC1qsAPlMZgtGwxkx8o0RRTizX3G9skGZOyJRyz6iOMhuE6RUgehacwCWtAK8ekAImQ06RUEEJF7xJdF2kax243sOsj/RBp20DfizFzjOzNbAMRTyJQgI4QPUpJbKBBBhatE7NZzaS2uCLgykD0Gq0i67sVR0c12/WOyWxBVCWOmhAmfPmy4Tdfr3jyJwN/nAoYOmg7rl6/xu06VDI8/+kvYOdorrfcnN9we71mu+n46OyEz55PKKcTlssZ/TDQ9QNd73G9JwwOnRL9dotSARstLkbEiU6jjMSlJ9dnMFRTKcOL0yNQFee3BdfXl3y4WhFj4MnpIfgBbeDJyQll+YL0u5e8fb9mOqspbIlCAGGVFFVhKezIuAtgwRNxXUdvK6lhOR0LpbG2wlYCMrsY8CTK6VRMKAm4IEBzyMkI3kVhTITIxjf0LmZmRQKl8WnAxQF0oi5L6qqkzIN4Ci3WiOZeK810MqValMymArRNKotOKceSQ2mjgHbzOacnhxzMa44PZixnBYUd0EVJr5foYseX37xni2ZQ8OY68Be/bDlcFLR3LWFoOHtk+Ed/8ojC5uSt3DXJlfLChgymRfFjyANk4j7tSSCE3Ikrk5ctuSKpMf0oiecICWOUAA5JWI/jgg8ECFVoSdelIA5ArGAw9DvwzYB1nsKLtNinxEQpytpQh5YqbPnnv/iI52cLHp8tWSxmzKYVi0lJYWVpI0woIAjgFH1k8J7gPd7L/4eoiVFSwAYPXdRse3h7ecfff/WGry+uaJSlVZaD4yMOVcKkgU/Ojvmn//inbIqazQ7u0zeiYBg6J1VloCWGsQ4rbGFJGTzVSWV2Qx6Os/cIWmOKMicM5gUCYugv3koPWJV5lnMhCkvaFJAQRoMaF9F6NHfYr9VCjmBXKJxzAhfpDCJk0Ej9A5Bg9E74h0yWlHKQgDXi3+bc/rWNr2+UxY49FkpR2OJ73gkmv84xVlx6E/tgaSM/g9YSNy9xy6PcB5HiIGwvFPfG5Zo9wDL2U/9L1w8GGf7P//2/xbmBzz99zG5zy3I2p66/YrN1LA5O+Lu//g3FpEBpz6eP53z65BnHM8NyZrFGPjSrEkY5YTcIEIfVggn6wdPtetEiawMEaYSdtKlDHnz6geziLjT4Ulu0ikyrgoQY17gUhXaVIASNc7ItHAbovWyHm8Zxs4rsPPgkOkAfxH2095Guk6YYxJzNeVCxEAAi8wRjkobV2lIaBbUfp4QenGUYKpOAZPmrhaJGQYgJlxt7OVkGjHH89I9/Tl3PuXz/hm9fnfP//Hf/GR8qpvWcqA2mrCgGS+8HjM36oBQpy5JSa3SUWL44ON5fXfHNNy/5s3/2T5guF6iQ9tGifhhkK2oUB4eHolePkbZ33G07ytkx203ian1DjBCM6FKdz9SfwZGCQ4dAGBrW15esN1vK5SFPPv8cWxiuX54Tti1+iLy/uuGbd3f87u2G1S7QOk3rAtrAwWKKLkusKpjPZhTGUpcVtYWisgL+RJVNXwznr97x1Ze3PH/xmL637O46gienOCwoiglVZTF2xm674cOHNSpFTk8P6DvZLh0eLPj8k6ekl1dsmogtDaYwkAxlUWKJzOslRmRaNG1L0w4UWlznXd9T6BJtPG5wcj+g0UakO1Eb9LSkmk0x2jAER8ARjOgV+25g1zlc44mxw/kB1/UoHSTPVufNMHIIWqU5Xs5zATAsJxOq0gAOqz2VbjiYTTg9OuHR4ZzHxwtKFZkWiqKsZCg0CaUH0WhpsZGMye99RkaDPHGnjZDu6VzswYf/75fSGVDTIqFRRobpe3pV1uuRdV3RgCsIwbK57fnrv/yS92+uOTt+xB//0c84PCyw1lOWARscJMVsIo9K2wxiChYSXecZhoB3EedCBtgVJDEuJRp6Cs5XHX/12+/49dtLvlt33AaLtwsKbVn2PaVf8/MXR/x3//of8ezY0LU3dMZzGNekasnGwdYnbrcOXc1Z94kQDa4R5kldVFhdolJi5RvwPTpOscrghwFdVpTWEBFK8XFd0RC5ajsAisIyOhtrJUXThbDfoo9kypQ38qR7OmBeWwrIMcqzHgAMAA9NC/eSgoxgx2youvczALTRGJ3R9/z3H1Lngg97pDzl+C5JtMioP2DNCAwUch/lqMoQw7ieyI2ny0aP7Cl9IhvJXzvdUySN/j4wojLrI6WEtRbnHLYQ0OCecSDFfjR7VUphrDhQp5hwg8NYQ1mWjMbBKj0E0NL+6z2kCY6gwsNG5Q9yid/f67/56QnXN1ccHR4TojwPQUmvMnQD7a6h0Jqjo2N2fUeb4/SS0hhrZfEweAbn6V2Pdz2VVTxaljx/ZDk7Kjlczrm97fnLXzkurObtbc82f5NFpTksEz86q/nRkymfPplzurTMJwmTdsQAIJ40kNuOvA0zFiBTbjPT7na1om9atAoMbUAbB1phrKIoNGWhslGqQaN5fHrGZy9aLq5v+PblS3ZdT9KKqi44PT7k5OAIY0uG3rPLGvvkHMl3HBVTllpRZSlGVEmAwNG4Lm9hhXqeeyKVIPp91C8kDE5SPEZAVimMLaAyhJnCHSicn5CwIqftPF0TaHeetnW0Q6IfIt0QaFykC5E+iK+EUFe9MD2MAZM4OlhQlorBRnyhcIMl+sDqesVNbYlOcXB4wtV1y6uXG75985r/+W/OsdWESA1ekVyku11z+fot+MiTjz9BJcO3f//3bK5uuflwhdWGT54cUhSW2XRKVZfMphN8SjT9QD9EWXgNkWbTsL5Z4UIUvwMMSpf4OGBsKXVWSY/pEWq2ToGUPI/mFQWHIsd0nmbTMq8LumGAsKEqprx48oxqMkdbAVONVpLsljx1aZnWJWVhCWlMw9JsVj1NLxGATdPRD15qUVDoFBmco/eBwQ0QI6UtKCvD3XpFRKJLfcrS5PH+VSNAltlg1qBKkfbYlCgrS1UWWBKFsswnU+qyJAaHipFCyXItqoBWnsNJwcFixuFyytHBnNOjJdNKU5WaqrYYlTA6UVrxg0hG0QWoCwHmvIh8eLnyXP6qYVIozBCoCXzu4ae/mOxBsZSXG2NXk5KkAoySUTk19hxQaZ30qAHIv5NGNp3UtSD4+r1HZF5UmqT3XkREWcykpFFYVNSEAd6fv+f8zQeS06xuBn7zxRtu3l1zUik+e7rg6HjB06enPD875tHRksNlybwyTCtFaSLgpPdUAY0wR0KKKPnI8L2n64YMEqV8FmlUsnhGWYRl3cGrDyt++c07vvzuAx82Hc6W6KlhPq+pCk3FwOdPT/nzf/xjFkcF3256FGZv8mq0oZgIYASRia2Yz2fc3q6IUQydXUC29bk93Vtqx4A1OgO8Y8qEMIp9CFkeKUCDfCQGa+2+jo+LBmOsJIg5J6yC4MVDypYUVSX+NikJYzMIC+Be1iAz4ijTGG92pcc/IyCeftCrjfdICpHgBlI1gcyAeGjAOPZgSchh0sc8YFOmLAsVjOWeeTkyGO6vRIoiPUrB5e8zyk+0sBnIaR3iOCb9ERByz/hDrh8MMnxycsrV9SXWKT59/EI2RhRUKlCknmXl+fj5kidnc44PSiZmQKmWwhrKrFWPyYOKBKQRTmjI7uxt6xmcnDjGaPE5QBPjiBmJ/ia4MXfW4VFoY4WCnClLRSGFyRstjrxaUavsdj6FsqqwVgzfun6GC+DRuJRwPtG7xOAim3WLc4nBO3xQ7Noen2SrLtIM0QgOIUI0GaW3+JgBhiTO83s9u7zAvIX2xBREx6YsyhiqylIYQ2UWDIPh3/+HX3Px9g2XtxtSOqIoDcqURKVZWMtsVjOa0ITo6foWUsAooQkaKxStpve8fPeet+fnLOcLgnO4vkdNoNu1rNcb6uWSalLTrFZMJ3NubjZsnWLrIu+3gevBsN31nK/WlGcbdq0XDViUwmZt4vbNBZcfrijqKbPHj3n00XNW5+/oVxu6LrAdNP+vv/iKv/vthlUfQFnm05KfnB3y2dPHnB3NSK5l6DtQmqHv2e12lDZhfaJr7gQ9VmL+44OnaxS/+tsL/u5X7zk4njOb1gze07mGtt9hbYktJ5ycHsHRkjh09C5S1XPZpCrLk9NHOGf53XfnJCvmV+I2L9volHoZpjSUlTQ+IQaqouKwPqKazHB9wAcYPPSDaKn6vqcfBoboGdqWYXA0fUtUYGq573ebnVBPrUUpg8YxqTSFLTDWoi3UdcXJ0QGFilgSNgUsgdLCtOg4PphzevaIJ49PWUwrppWhMAnlO2odmRQGZUZXWWFBON8RYst0mVAWJAp4HAwfJEakQIgW89DsLwo9Nhnx9RhBtEz4F/r+SG9LoJWweETTK420SgYdDTFUXL1f8+1vv+bdd++Z1zP+5Z/9CcsDizGespK1l7ZiQqnITXXSdI0kfPgh4frc7CAAkfNaWCXRsh3g7dUtf/PbN/zNV+dcDp4wmzGYEm0q5qZgYRzPp5H/5p/+iH/9Tz7juI4kt0OlgcMY2N1d41cfOCinVMmitjs+Ovs5X3z7jrd3krpz8uiEs+MjQtejFBykHcd+zePyGL25oVIJPxTsvCcAQRte3VxgCkvKngbJF9iyprAao4SEHJQU3shYJDJ7QWU21fjJjLq/jNaPBWZfTkaKXTY5k7+iMo1PWFlVpfFRKHJjPxO8mJuS5QiFKhgNP8diiYDjWCPn/DD0FOUI8gZiDHgvPgZFUcp5qBTBO4yWEhSDUPfEjX8svEEMyXLjlo9QvA/5PlN7o998zBJy9HAIgegDprB7ECQkaUqsyV4jWa6h1CijUPRtT0IaDPtAUkJ+BO6TO9KesbD3yMj/PJRt/OH6/br821/hr6/p1nPKsqCeWFSW3BAV1JquG2jfXeC8Q6dIYQqsMUyqCdPphOXxlMViwbQ+ojSR+SQxK3dU9oakBkKwVAV8Nt9QBUPpIk2c4pLG9y0LbThbHnI4rSl1QbsNuNZT1FAUYsrndZDn38omdJ/MQ6RYzNBR0TUNQ7uDKOZfPiE6b5UHohxjrU1ksVywXB5wcnzAf3X0T3ERdoPjZrPh4sMHzs8/cPn2ile/fU1VFhwcLpgdLChnJaU1lBQoVaIowVQoIlYlohrj0HQerIT2zHhyjSAho//PCHKKRj/Ee8M3CKgUsTisAVOUGKMJswgnGkWF9zP6weAGWG8Htl3Pejew3g3susC2UTRtYvABnyK7vmFqxEOg3W3xztF3PaHreD+7xu+27NYdMRjubq75+tsrLu8ci+kh1bQgbbf0ry/QuuPtl1+xu96yXa2xUXP18g3tZkdhCp4/ecTBcsa0KojeA+InpVWLRaEKz7Qw2KJA64J4tmD3eMGr1+/oO4+yFUVd8u7iA8oWOOcZwsAQIAZNiiJpDHEgoFjOLcvFMbve0Q6e3e0gofBdR1CeLgSCrYl7Gn9gMa8pdALlMKpHpS5HtBe8/7Diw3XDkCyN82x3nbAetZYhJ45Gd4aYPDabOK6bhiFKj1hqQxEFMC+VoShLiqrEFmKiZ4ymKCxlpbBakugKlb1AQqTQoJSjriIHyynL+YSTgwWHizllYZjOCubTGq1E9qrxFFpTFQWV1ei8hRVPHelLovOy+NQeUkeK0GlDnyxdLKmipkqJuVYMQBqTiMim1Gn0IhpPkAxKCyWUvS8DwuqUypD/jrg5yVygICASJm0saJ8ZePJ9QBNdQI8OKdGAN/g+8eHtOa+/fcVkWvHs0SmVndAeOT75+Iz/9r81JFMwmc6oqgqjFSUBowZhIMcBnYunD+CD1FPnE74P2ZtOFqouiN+Cd4oYFSoVAgl6zdZ5Llc7fvXtN3z53TlXm4FdMuyCIVRTqrJmVtcclfC49Pzpz17wj3/8jIMq0cc7NnrKiSnooqOJihg1LgQUiQLAWqpClmvD4MWvoyhwQfqaIiaSG7i5Ome3WvH8+UdM5ktUlZcAI5NE5dSP6Ikx4AaR/GijKKpKYiNtgU6JkIf5uqrz55cZlyni/ECI975NwlZQKGO+t+Tx3gtnxWRJGionb43LmfvaE0bz+ZhZGikzCoL0HNZaiqLY+3SJCX4+2zNIELOBI7Dv9fbMVO77tjD2LKN5tQKi+A/GEDDWZoZqyMtJSe8YF0spwTD8ryyX+NlnB/z5nzzj2dMnlKUhBMlVbQfJBp1NCk6OakobiG4LKRBTRoyMJSpB44TiqsCIfiglTdNvGbzQBmOMAjAoLQ61RuNcoCgsfe+yk68RSre1JOnAGUZTsrylislT2ITSXhBMUwjVTIVsdOEhDVRFycSajLQrlLagKvrWEp245JI0/eBwSVII+iHSZ0CiG2L2ixC6rvMRjyIkiwsa73ODLET2vW4yIiZ+AYVLCZXEgT/6yN//6muKrMcKLDk4OGDwiS7GTGAPhOglf1sjekRGVVeknlW8ePEZKfQ0m1umU8v19TXbJxuiG3D9FoqSZrXGt4Hl2QlKl/SdmBS+vbrg9S28vbzhahe5azwxaErg8q6n64Q2RfKYNEA/cHt+Lj9nUfH4+UfEXUd/s2N93fLdN9f8+rfv+fUXNywXc/7op494ejLlZG6ZlomjecXjkxkqTNk1Pb1PQscKhwQ8Lgyk0ONcj0rQ7LY47zGqwtrEz3/2HB8ULmlioZhWkrxAFCR26Dv6vhdDorZDDQZrNGXhKUpHVRY8efKIbdeRosIWhuVsSmUjhQ0YHcVUJilcjPQDrNYNrW+5u27ZNgNtF2j6iPeBhBSykCLGKsqiRCUB2awqmJU1fbvjaFYxn0yY1iVaJUqbmE4KyjKbbalIoSKlbqkNLBdTTg6POFzOODmYcHw8Y1JYigKSCih6tJamzigj70EA5xUxeJqm4fbulrvVDbOZ4Y/+5CmRbESGzXvyjMuHJLsmpaRgBvIdhhSlbLil0PgkG2zZQMuWYvQ1ISZ0zFrBIJ4kbjDc3Q28ff2e775+xaye8qf/9BdMKoVKPQqHMaP2K2dBK0BFkpHnpJzPWK2vGfpEchqCgJY+Jhrfc9sFfv3yir/44oK3a8dNFxm0wVcVlpJaFxxXmo8ONH/242f863/8gk/P5pSmR0Vh9KikKMqSNLG8uvKso0EVlhim3H1zwaYTGp0fHBfvLlhfvafUgeViitKGgg63fU8x3PLx4hBP5JdfveLdhyv6fqAqNIv5lNmkkq3WckFRzRiCJuoSigkkTSoKYlHgAzgXQWnKUnSppJzgsaff3W/45cNSexR8/L2HsomRHZC5fUJNzIO0QqQNPqb9tiCqSFJp/2squyMLMyX7GxgETFUCbmmjMYhvQ1mV2cE4SBZ7kDuuqkr2cVAp5W2OIUV5pmKMoj/V2VJUC/tANg4mxzSJ4W2MQlskgRodn2OSoowAAVrpHNUUMUoRvMdUJdPJRIppcEQXiIxF9t6BK6XRETw9AETyee1DTqn5w/X7eO38wKASLiFZ7B5qrbEpEbxH25LbZsPdtgFrUAoKNwgwTETRk3RDPe1Y6pJJnZhUDqt2mNTjhp4UNU8Ojzj7Vz9l42t2YcJql7i8bbg4f8/q5pZvvvqOy+8KDiYTlrMJpycLjo4qqmKgqhS2lI2vLQUkMOLrJR4nq2tsWWGMYrmc0O0aNqtW5AQJJvMFzvV431DXRtiPSmRBWkMyoJPBlIb5tObF2Rn+j6OkFq1XXF5dc315xfura9avdhQFLCYF5YtnPDk5BbSMUdpkSaHaA5+onB8f9R4QVUnl7Vl2bs+gtlJiJKiMRhaUUmdUKogZcAg+ElNAWentdOGY2gKmlsVBSUoTehdpe8+u7dnuOpFbtB3bpmXTaOZqYLXb4XdbAho3eNptw5vv3hBOl9Q2MSjPfHbIR89K6umOD1c39K7h/e++4teVw9CxvbvG9QOVqUhtYD6fcHBygsmSvrrIrDAr77cQWDPdOYMs1kBiIJnEwdLy6cePOD+/pukdJM3Z8SEBy91qTb8TrXRAUjcERBLvGaU0TdPx1e/eses9fYSoNdPDBd3g98lBEKhM4nha8smzx2ib0MoJ4wAtbBHg4PiInTdsr1YEpTG1xSQoraWyhbT0KKpCbIzrsmA6qdkVoIspVVlQFjZvW5XQ70WEjkmgdJQB2ESKMlKXink9YTGdMJ9OmNSWxXTGYlZxeDRnUstSplAisVExyraEBq0DJm9ghU3sUBhS1MRgcb1maB2b1Q4fHXHqUWkmrM0oLNVoZdmkMGhlGZRBWY+ynqi89CPZLFDSHQIpyiwiS1+pyylmBk3KHilZYis/u7zDKiWRihgtkiedJVQZRDfZR40kQGdw0G87ri5uuPlwQ2kMn714RlUX4hlFwFrNXGuKsiYljR96UINo8GMihSBK1Si+Vc4HhsHh8s/hPfnngoAkWIUsh+g9OK/ZDYm7bcPL82t+9/aCD6sN7293tBGiLUlGYaxmbg3HteL5oeXnHz/in//iM56ezih0D8GRomGRHGdpRxc9igmdnmQWgMhQdpsN281GgFVtSUEz0sNlqeXp+oZf/f3fS2RwWfJivqC0luPTE95/uJI462yGWJUTUkpU5f2CLaFwweG9k+VPBIn5/T4jQKNyfLuApN4FVE5iiSntY7itEWbk+HGnlHDe7Q0nAcbECfFeKDIrLWYmcQah4pjalT3/BKkVRmzuv4zSxLzICZm18TD9auyTRqamjzEbhwubzOTFYggBhRi1jqzMECMh+ixXzewNNJN69oNq6g8GGf7Zn3zGZFJSWCsoUMqDsyrEId0qqjKReo9XkvsKBmONDGha7VExcQ7NmpAE08mEqkz4IXJ9fUPSmqQVtihpho6qrum9Y0x+0EpjlSEF2XL5wUMyJK2JBkiiNfbBUxmwOmJwKGUplEHsSjxKB6CHKCiXxFIawKKNRMfI4K5ZznUe8OVhSymb8rlE0zpB1JLoefrBMfjsmNyHrF1yxJB1nl4ceyPggsKpiFdRIpwQvXrKHH1DjofSkeTAxYBOZBf7iEE2GxQmm116ytJwfLSAWDOroNCR9x+ueXf8nklhiKmlmGs2q4aimDOZntB1is020Q4t//E/v+PvfrNh1SmcLgimpqwMszK7v/tEcoHY9agQuXp3wds3F0QXqSY108UxF795ycV3b3n5zVvO311SaMP/6X//U05PltQ6UibP4axgPi2pSkNdCUVucJaudwRVY43JOa+G6COb1Zab2zua3oOe4l3ibt1S2pLeQecD7XZH77wcHFGL+WcEjMVaS5cUm9WOvhkgwfxgQUCz3vU5CkehU2C3uub0aMJyZvGxp0uJpCSi1SdDSJGrux2364HOyZbUGENRCtZcaUtpDHVZYK3BqoDWiroqqesKYk2hoSysUFaJWBOxJjGbKuazisODBceHcw6WE2alZTqdUJcGowMQsCZRGdDKE6IjxSADfjT0g6ZrIuu7ltVqx5tX77m+vkDrxOFhxcefHOB9wAePd9Amx3RWk5QmWYiZjhcyG0dHLwNvyvUxeYnxVFYkLPmYRonUYgQYVBJ2QRoMfQPb7cDbd9e8efMBYww/+/HHPH9ygjUDzfaOGCTC1OoSrZT4WGSDJW3kcA8qYqcVxXzKzeaWoROTx2bw7Pqey03HTZP49ctrfnuxoTMTBl2DNiyKkkfzCS9OZvzsxSF//scv+NmLI2baU6oOlUS3OEpz0AI0mLIghYJkS5Kd4pXGzicsosQzpRBI0dPhabpA0+1YB8OsnHC7bdh98QW2qHFtR9reMaxuMaWmWYOdV9TTCZU/4WBxyLbrcUkTyimb1mOmC+qjR0QzBT1BF1a8FJRs4vcqxgd0/YdAwkPQ4R86FFtrRdpD2hdglY08YzapHGmECfFj2A8FSSQVY3zS+D1MNucaN/taGwpr8y2Rh4mkpTHMQ4eYMMn3IsZsPDSyFHK01Pg1SeBlc1DYIhfKTBc0QlM0RoDwGDKVOBtbjmCAfL9EUZh820qzEPeFV6OtxjuHDx4d9R5oCEHMqYxOuaG8v8bs7D9cv59X0FDOp0wOl6ggcoDddstyPmc6nWJsQbr0TKsCjMK7geR7tBYfFWKBTgodOqJzFNMSHZyYz2lNCgrXdXTDLfXScLIwPF0c4HWNKp6xaz7h/btrLt5csbndsrnb8v52zYfrFUWhqSYy+M/mBYtlyWxeYE1iMi1k6aDA6MBECbhX2IJUTbgOLa/Or/ju1Q0//vkf0w89xJ6TkznTSUIVPfPjA5RJaCOstCKJ7l6cyTWxKjmYnPL88SNCivQust5tWd3esLm9ZjGrBfxQI81bZY1clpGOJnlkqhtkHTAPniN5TkFYRybnAG63OzbbHUTFtJ4AkVAI+Cca4kTSMT/LnjHBw5QlZZGoysh8ajk7Xgh130PTeprBkdBMi0SFY9cHNlFRTmYcTqfMyymHyxwhiaE6W3J6OOd0XrLZbaiT5+78nLpK1NZw+uiEqiioi1KkwH4gBjHntGOPqaIMKBmMUcrsk3BMlmKOZnHLWcX006dc32y5W22zNMDgh5q7uw1d0+OxUFbo0qC1MLeIlqK2HJ8dEW92uE5kMsoYytJgUsFoxGlVoKgr0BCiJ0ZZmPmQiBgiFo+mLDWnp0swFqWhUIYiG8FpJbp9q6CyMJtUtM0WV5fY0mKtxhpFVRkmk4q6tBRWMykrlvMp02nFfDZhMrFMJoaq1BRaBkijZeEiE0PA2oDVQVYiQfypVO43YjYnSUnT9xLfOvSRrnes1wObu44P5zd89/U7+qbhk8+WfPzzn8HRQoanEIU0l/sepVIe7NIe0FdKEuS0UODw8V6fHlNER40yoyz5Xm6qsv9IUmZvHh2TDNIjj3SM8055kDNJobNpfXSJoe1YXa25urgmuMjxwRHL+YSqVigVwIjWQsWEMgVFmTfkyRGy+XU3BJyLZEKHAONeBvBIAmWy8b7COyUzSlRsOs/lasP5zZoPdzuu1g2bNvBhtWPnAslYnK5I+axb1iVPD2Z8fHrETz865Y9/9IyPzuZYOnSSxJSkJIJy4nY89i3aaxaxYJgeMFiLTwafICiDtiVKK4bocRjZqisorMYScGmg2dxwfHjEpC4oCrMf2GPwOOfBKKy24s+CziCFGESOaxofPL0b5LlMCasfAD6FzBEpRZQ2DIPbe0H4Luy9skhIDGxmO5rs+Om9xxiR1e1ZkgnQaZ8OEmNe2iB9SVlm/6eUsFp6L/GdiQKImMxiygt2bWxmLNxHa8vy3tH3fWZaqvu7TonkojCGqhLfvSL/+ZH14H3I0h72c/j/6nKJk6N51gM7XHKy1VEBZcAUBUqLK7rSKrMNBDkxthCZQ0yoqEd8D62sbMV0oppIJnMIkdcfdlDUqLJkUIE+ePRM43rwQ0Rrg+97ZtUUE3PMmRZDIhm8g+TOp4gVM2Ux/dF5kzUyKeA+d1WNpmCy1VMqCD3bJMgRapnsDciNbZDc+MKCLhyqMtiyACXuuxLpU+IDBJ/wIcqB1w2ZbmRxIdH1gX5QDF7hvKbrAz6IjsgnYVKEBL2DwkLvEl40GRkRCwLITIT65mPPYlozqwpAo2MN0dM0PRcfrphPavxwB9Gx3naoYk6IFW9eXvH6uw+8entD32k+evKYZQ99kghQazQTGyiNor1Z0324wzdrkvN88Z9/ye2V5EIfRcNv/+NfcH3+nn7Xc1CXnP3Rx5A8i0XNwbymAJFaELA6ofW9J4DBY7WjNJaiLvImO2DrgkW9ZFIr7u62tK0jWinkvY+0vUSGdX3B4AKDD4QkKH/Kh8kYc2XrBVF7+r5nMwTWTcfN3RoFTMqSUisKHRjmBf3A3oAnJIdPAz5qtK5YLmdENIukMDnVoSg0Wkes0VRaYbWitErkDZMSq0UCMClmTGrDbFqyXM6Yz6dMJ5b5vGRaWyZ1gTHI11LybGlt9wdETFIhhFavGXrNrnFsV47r2y0XH7Z89+aWV99dsm1a/ADTGg6XNc+05chN2fRTNpuOy/MtXduyPOg5ebTk8dND9gNdEqQ7MOyfG6MNzktzZrXcjzpZyZXXCpVjDZM3OA9tE+i2d1xerri72eF94sXTxzx+vKQsEpOyJ4VBXNcVYtwkZg4oAlqNvB4ZgDe7jt999YZdBxcXK3ZNIMSCZghshshtA7dN4nwVcFgshuWk4vHRAZ8/PeHnn5zwi89P+PjpnMU0oWOL8Q4ThZ7mo2boBobBE0IuQKPRa26UZc+jRUpiNSKIFhlPDB5PxKsCM12iJ0siEVPXfHJ6xuF0ys15gQodBY5ZqTisDf3qipvNhi4mei/AVWkMh2rJyeOac1VzlzR9DCILyPnQVtzS9pzNcbiPaTTX/P41mgFJ3JIwQ3RubmIWYI6Zz0YLaKa0sAWSShgrjLOQN/YxStyTmMrdgxkxyDkVvCcoOavFOChSWgspYY18rZFiGGLIkqX8eY+bzySZ5qM8Y6RtSM71+OfzZxPk3jHZODLuQTABM/QYWYUwfMj+DPL9xKNCaIECZEged5ae5PfY5kJOLrwoOY9HFsgfrt/P6zYULJfHzB4/YnV9SUpQVDPq5YL5dMpsPuPN+0seHx0QQqDvG1Y3l5wez5hOamJMTGY1KMVsvmDoG9a3O2YTw9A0GCXa9aJKFJOOMjbYtEWlAY1D1QbzuOTJ6XNSVLg+0mwDm3XL+w/XXN2seH+3I14NWBWZ1Ia6LljOZ0zrksmkpKwU86WnmojHC8ESUknbJy7vtrz9f/8ndn0PBpbzEh1a/vSffMLTjx/L8GsE/OubjmbbUZqSoiixZSnbdiVHXmE1k8mSxydLVHpB9APRD2QkIftr6ay5zyynewSCkTYuaCX7Z0m00jFLthQhKkxVsbu6ZbPacXx0wma1YjqdUpUl1gpDwGZPlaLQoi3XClOEPAA7lM566ZwONasMIeae7WTG6rCm6QJ3dx13t1tODxdMK4W1XgbwENBGMZ0Y5k8PiWqBMZH5vGQ+LyiMyhtBoTErcRtD6yxKMORkAhlnVP6BlRrBWI02mX49DrYxMq1L7Nkh02nF+ftLmq5nWsPRgYA6Hk3QRlLZkK14SA5l4fC4pKoVR0MgcG/+lpDoypgMWkVqayTFKnkZ3pVQyCW6T87BxaTAmKl4emhFYQyFlt6wKDRVYZlWlsWsYjKpUClQT2rKshRwoS6ZTmsBObRIe1SKaAJaI+e1SsJSURlEyOw0BagQSCFilcwZYBgC4mXhxPizbTy7ZmC1HbjddNzetdzdyqLl9npN3w4kr8B7phPFo1jjVS1yE5/QMVAn6alVhOg6WSjaROgDQz/Qqw6HoygUushAmdzy91R2xI9ZqPlSB2UpgAyUY+R3ks9f6ZQ9SJKwP4P8fARFGGC32bHbtjSbHYTE0cGc0lRYq7BlQtlEQmLglTHymRkBxrvesd5u6duAHxLBJ2T0U5iU45dDlktEAZoar7jZetbrgZtNy+Xtjg+rHec3a+52A20AVZaYomII4smgg2JqFUfLCc9PD/nR00f87NPHfP7smJNlQW08Oq5QUZaAMYGLiuACygcmaI5Sokw9dVGQjHhkrLcNnUsYMyFFy10zEMs5u2DYucAQA0ElUteQtndsfEe7ekLqz8A7LjY7fNIMwaGV4vTJGSnB3XojXgbW7healgLKAuecAMrjoiJ7HPgc/aqVybInsIUYYDP6GWRmZwx+P+SPz/d9OkTYL2ZMlmqmFCUlrhh9sgQ08GH0R5P7KoQg87TOjLGYMitTzLFJQWRq+bxNWfZhxuSJLEn1OUFLaTJTmT24GUPINgRpD3aozDwbe7Yf2uf8cPHo2Emh839HeRhs2FO/EGtlAROSylGRZm+MlMZYlwSymhRyyNhMRgVeJ1bbLXYylWQJI/EzqVC03lFaRdO1FHWFtIkQgqLpOioj288+eQqr6cIgKFhSzEyJNbnIjY1q/hB0RqrSvmNkjFze/3rKSLsKZIRStClGK4wS1MuM4AtiSCPRUDngKX9wMZA1z4aIoR8k1SKhcS7R9Z4QhGblfGLX9vQBOp/onJgyDr0gj0MPg3M5MzqJxtD1lMEyLyzD4OiDUAp3Q8e7d1fMphNuP7xhc7tjt+rxsWXdvuT29pa+7Xl2cszHTybsWs/lXcOq82y6nrYfxHCmDbz6my+pV3f4YUfXtly+uSQMg2ya24bdh3MOJyXVvKYsDIUR+tVyKZowlUIeQMS5VmV0Xw5JkcjYQlOUmiEXa6VFerBcVsxmFc2mZ7sbMDpSeZiUBVVvGHzFru/ZtUGoX1oLspcdp2RoUExrS10aEoayCKJHSlBaw6SqIA0YXTA4EH8NoWVGxoLgWNSao9kMY3QGEzRlYagrS13K1y8rw3xaMKkLDpcT5pOKui6YVBZTKIyGotDYjLpqhPqqVTaMSVmTF8VpWzxMoO8iTb/FBXCh4Kuvr/n6uxVv3/e8+iAFYd0PkBLTyYTCaPQQqe8S72NPU/VsteP2ouXmYsfBtOTRIyhKxSefHZBUiy4UtlBClcLk4qkorMUMlqIoMYjhmNI1DJJTrZKVuEinuL5ac325oe8cRWF4/uIpx0dTtOlRqifFiFE1fpRWaCMPX7IQTTaJHDWMioREvv7m27e0YcKbyx273hKVYtsn7raeVRPoe49Olp8+P+Xjs0M+eXrCTz4549PnR5wcFUyKiFY9CqE/SgOq8AH6bqBrB/xA9mPxdE60jeOEq7hHdo1iTPREIn8E8JxUU7abrUikLJA9TCaF5mBimOuSJ8dLPn9xzOF8yvvLG1ywDKkQ6Y5r+fzZCR9/+oR49JT/y9eRm50G4yEIndNkynaMfg8uQB6sHxg+jukSo2fAvYGjIcZA8FG05yQBGrTAKJFIdNkXI0kT1Pf93qQr5e5KCQ1s/71iTq9QSnx1Ev5e8qAEeEgpQW78lQKVo5REWHa/AZKECNn6GG2lhCQxwLyPklLSzEedWRRkKUbG6xMkH6RmJGGpjMUepQneU5QVYgiluGdNSMpHIuwbBhLEzJbS5v592L//3yeL/OH6Pbq+uNixWEfebweGfsPZ0QlHdU3pLc2q5ayesDh9xPHxEevVLU8/OuNmpvjJZ8+oqyl3qwYfRN45mZ/yy7/5a26uP/D08SnddkORJXGPJguGLlCUHXHSYksZKItoCcqjrOgfVK05Pa1JTPm0WbLdDGxWO9arlu26pdm0bLcNq7s1Kaei1LVlOiuZzSsKq5lOJxhzwicfH7M8eM7tesfNesPtdk3TbmjbhqEHHzWksGchbbc7Xr98QxigLGsWyyWTSc10KjVMaWFTKqUxRlhEPil0dmzHxvu4ayVaaEZAMMueYlLZFzLtezORTisZvpHnsS4mfP7THzP0nmEYxIsiKtq+x2860QenxGwxo6qsDF/GMpnUlGWFfOcEKqF1xGgvAK0opim0hioxNYY61VjXs5xYKpvyAJkIw4BFUxQwnWjKqhSX/qlBm0AKA0RQKIIRVqRYyYjUUufzbX+O5PchKhm4cshIXobIGUeIYANGwWxheVYc0zQDg4Onz46x9ZRiUhNSkjS13rHeNWy2LZu2Yb3eMfSFeNtoKwuqTL9GK2wpQ47VUBpLaeS/xRtBWJhloSlr8R2py4qytBSFGK/bQkAGaw3WIGkERsx/x3M0xihGjSphjMdoGZ7UQ9YY0lePunehcovTk7DeLK4F10bWnWPAERS8+7Dm1dsVt+vAtglsGs/tduDybseqdbg+EX2iLip0LCkoKJQsuAZleNfBkbeorcOSODAD07pkcVQTy4p3F1uSy/WTDGCnSEyOwXfC3iiqezp9NhP0XuQoI3uPBNpkf6AwbqpzNoWWOqiyqTNRkYLFZXCh2TTstmI4ezCfMptMGPpeFj0KlClImXav8jJVel1prqvJlKoOwiwJHu9HUz8F0WVje8/ggkTGhoGrXeKvfvmB9zc7dn1k5xIuKlxShCSsF+UTVeqZGcPhvObJyZKPnxzyybMTfvTRI56ezJlXCat7FE2Ws0hfkoLBh8TQB7o+0XtLlwzX2567vkc1CVNUWAUEh1WJqRpk0dDuqGzkcufpNjtWux2T+YxJWTCj4Wg25VEdWfgNcTBor4nlhLooQClKP4icNfQiiwsaikKMP/MsH1IiRJcfUSWGpKR7hpYSY+iYZV7RB2L0ecmeGTjZB0EMFeVev091MA/+X8xVfQj7BffYR8Sxh8lsy/EymToUY0Qbg1VK4l+TPE9KJTHtlFcrKYgPmAd6P8+zl2u4B0sqeS4fsF8zkLafg8IPN7j+wSBDNrsnRfE/kuNftFXGjGZxsvUbBieor1IoLfmlKtPBRh2aAA8IupsNOZSGejrn9dUFu8stqjRyTptLtNHEEJhMahkIq5K6KijLgqBl8xeNx5YW7yKLacHtzY5FWYG6N34JOgr9Nookg3FLpwRZvsfYszuyvN15iEjYKMZJGa8mjUOhFm3vmEihlcruyfL9YoqETI1CBbQOaGMorQw3Kg/BIVhpXpUBNG0nKGFMBh+hzRtW74QF0XQ9vQv0Q6B1jl2rqKqSSWyI/Rbbt6QAfYCLzZWYEW7W6FhCdHTNBt96FtOax9MZdVlhTIFfwJNlxRAU667nZr2la3tU8Ny++cDXmzWGnpQch/WE+ckJdW2pKyXusbYgBYm5EUDGY/AUOhuakCnXRt7flJ2olZGlhrZyX+no8w0vD7M1gbIomZRz5rPAdG1ZbXY0nUcRCYcFmBnHhzOSNtneRw56F8QzYxgcg/NZa2ZYLGqWtWjyfMjNXlFSGZULakECrFWUhWIyKahKw6QqmE4qpmVBXRXUdcl0UjKbZYTZQlFpbGkxSlgNRswiSAhlUo/AlhKtvULnhwwISl5v5+lax2bruLvzfLje8eFqy+3dHcVU8ejxGf/+P33Lb7/dsQ0VDZo+FaKdJOJjiY5gdMI4z7rrGOKa263moNI8ff4JH50tWM4Ck0nIOKCYU8l8LwY8Osk0rZQY4ykKQrI0HWw28OF9y/V1QwwKNwjqv7pdsVzM+ezT55ycTjk8qgmxpev7zFIwDFoTvGHwep8aY0yBIbv76mJvSx58Qdd7VhvNdRd5eRlYdREfA36IGGV5vJzz8Y+XfPbskI/Olrx4POf4YMKkAKM8mh6VZIsirBDJku8d9I2nawODV/hU0PSKbRvpgmJA5Ch6NB1UCZ1GSZUMwUolKmtRk5onj06pyoLQNaTo8YVmtf6AGVp+8mjCn/zkE3700REHc8P7izd8/uQxCUPSJW3XM3Qbnj4+ZHFU8T54CB2Dq/HZsXik4u2bVvmf/fAsUZeSOi00vlxE8hkXsrESKcspYiDpe6A1xqzxSxE1qLwZkcIZvESb7UGGPJiMvw4CPIRMs0ONMo7MdkHYACmOwIiAsPIXZRB5CJqk7PuBvS9+Sql9ikQIGShRQjMdZRzj65KfJ+WalDcLSmQVPkb84El9L54R+feSjkJTfeBnQa5fowuzRhN9ZlPsG4w/UBl+X69Xd1v07YbiTUSrxLy846AsOFvOUXHg9HxBSoF3V3eEoeHpcIBr1rj0iNBuuL5b0zSO05NTrtc9v3t9xXazQ9eHlHrKzd2K5aBZHhbEnUOpLUkryskEUxQYW2Pz1mv0/NAqgLbUVaQsDEeHS5w7xA3ghsR217FZNWzWO7abht2u5fz9Cv9GmGF1XXOwPOBgecjB/BGPTp8RojD5drsNm90VJ6cTXJ9QRpOcUG8LWzKt59xsV6zubrh8f0OKgaIy/Pgnn0m99J7JpGZS15lMFaiqEYDQ/2ChgwzhedmURvABYeoxGmXr+2dtBDB1UVCWE2baEGPi+PEj0hAZek/f9uzWW7qmFTAgJnZ3De12h7aK46MjqiwHiClS14WA7oDRFmuz1CQlrFJYLRGhpUoUSjxeuqGDwaGMpppYtAlURlMoKBLoFAhEObvIqHOSAYMkrAGdk3pgxFNSBmfvwRUxRMrvWRr1+wFjBFCdTg2L+ZKYvXJMWVJPa7RRRKWJaFwMDINn13Rsdg0xJGxZoo30XkXWj2MUxsgW1GZqt9UC5et8lhmrhVloBAQme/hI9y9AkMoGhyqvAchnsBgWqrzpL7IXTy4vSWWwWHT/LkRiiHgnbISm8TQ7R9sFiCXrlePmesvmrmW12RLLyPHzQ756/YFf/faCm12kTwUUtdD2E7howEs6wiQqbNJYEjoEdNSsm0R3scUcvmc+nXJQGz46rDk9mDA/qlAHx7im5/rWSyJFoakrw6QSOUYysh02ZSnvjRFCYwqR6P2egZeUZpQNKoxIUaPKvpBq3wsnJRGNQx9pdyt2u44YA5U1nBwumFSleHakQKQnaWFciDNW9j/Kz41CgJGQEk3X4RIMEbadZ7PtcT77u0UZRvvg6V2k6aHDc7lRfP3hjnWT8Fh8yoO1ShQqsqwtT04WfHS65OOzI148OuCjJwc8PpkJ+0cFFC0qefGbQ+7ppA3RIeBCF+h66JyhjZbbNnHVQKOnhFCBqkSyFgSsNDuPMRofS4zv2PUDfd+RnMNttyQdqZTjk7MlP3t6QBiuISh056AvmR+csPOBN29+i9eWsp5kFr7F2BJVlFhjCUoRlBb5ffbcED+n/OzKLY41FoqRmRD3YOTIU5J594FBNgBi9J4iWd4qgJ/NTMo4+jAgrE5ZxOq9vOJhL6IUaFsA4qmRUVx5LcT931dj6kR64MWFRI/KUaW+Zwj+kHUBAoTYB8kTcewTf+D1w0GG/GKS0phkSSFJ7MyeSqoIQ6DZ7mjbTrZFKQFu726pM5tBzCBlsJbNsqGoxVH/xUfPWRycsOsCfQj0zrNrW3a7HZ3vWF/vSClye9UCQYbBuqAoNYW1TOYTGfSNpvVQBsVEWaIpJetUayloOj6I0VHfo+sphQzA3OeQikERpAeO7WIopjKlKze1Kf+9HPvmBp/zhIUFEkdKYOZwMP55KY8wagmN3HTTSSQpLQ05MDhNCAUpyRYghCkh5u12js1KCBugqSs2ZaLpIo0zbHae15fX1EVBqTRVWfJoespiNqUuDFbn16UiXkExFSPMk7nhybIQyWkMqOSpS8Xh4oBpbagqMVI0VmHF7SbHvwmijdIELVt4rQQ4YtT0jw/v/n3Ow7eW9AKd31Mxi8obVpNdX2swdko9Kej6gT4EjuOE5yFS1DXT+Sy7HQv45Xyk85Ghd2x3PV3nabsBjYAJxmpxDNeK2aRmUleUZUFRFhijKEtNYaEqRddY1aI1NEoAJm2MRG9pBZnGna1LZDFMRKlI1Jk2lyTfOEbRvkWPMFtcYrvpuVvtuL5teH+14vxDy+3Kc7seWG97+j7Qdy2Pny/4eXnCRRe4TYo+ScFOCqEyoimjUA8nxnB0OOHJyQmfPDnixdMTzg4rlvX/h70/7bUky9L0sGftyczOcAf3cI8ps7LGrm6y1FQ3qYlfBAgSKEgi9B/1BwQQEiRAIEAIRFMkmuhmo7uKrKrMrJxi8OFOZ7JhD/qw9rbj2S2JIYAfOoG0gCMi3P3ee46dbXuv9a53EHqTELkoCJI0XVxKRnLB5AbVCIKHbCjZczg7vnk/8/NfHPjm2wuH5wXJwv3dlm2v2uTdzR2vP7tBXOHx8ZHDQZ1wY5rr/RA6fyHHyOl0WtlBQ99jnSemjPe+Ovd6XCh8++HAu28nHi4z83FhHzo+++yGr9684g++fMXXb3Z8fj9wf9vTBRAiwlz/XcFSmqTAsiyGywmOh4l5LBRxxOJ5XgzfnQqHZBUiSxlJqqnTvUzrQaWU1fUu2ozbHNlvB4JzpHGizBekK2xCYetmPh96PttYXFl4eTxwPh642XbshoB1CyeZ+XA6ENwWkQVK1GmTVz8YI9dGpC6m3/Jk0Fjb6hze/rtwncjUCLQUFTCFUg8OpdI5dSDTvd/pepXWXEteDUYLpRoOtQl+UVM33SHVU6UdbKWsoAei1OK10K6vQqdeGgXWCvH23qoF95WZUZuwNZrTqHFuTBFb7PVgrYW9dfb6WUGNyOSTZqBUczmdtugZpdOCkpTGq7ISvYeKmKOv65OpA5+cEb+/freu86jmwk4AKWoaeFmUkj2f+PbxCRGNE3a28O3HD2zdzHazwRTh/YcXjueF41RINvBwTiwSeH8pxHlkejmxDSPD5obXtx6IzPGA9Sel+ncDhUIIHussxjuQhBiH5IwrOu62ndB5IfaG3d7x5rNblvmGaU6M48LpNHJ8PvP0/ML5OPP49Mz794+IGPpNR7/t2e137LY3vH59z2ZbODzNbJPFei0+O7fhJz/+CV++mbmcJ6Zp4nw+cDgcuBxHPn74wMPDI8Yo+2/oOjZD4I//7Cd0nUo1ilTD65xUlyxlpZfTKEbNq6HhoJUEoIOrovWCkvn0bLUq2RNr8J1jsx+4e31DjhVsTJnxcuZ8ODNNI4Kaax+eXnh6esRbw2azoe96rAi7mx1GHDEBEjR9oAGfCJd5YpouOCPc3PRquhtV0z2PC2SL2AQm4byjlZNSZcOUpta9euO0maKhaf/b/ailIlKHbwrK6r6udYa1FSC3BecMwdW0NpPICF4KQ1fYDZ7P7m5xXb8yKEzzT6CAyRRifb+5+qgpWJDRptg5v54DRZRW3/xz2osu6O1qTDcjWeuaWMip1anaPOvwYSHOmXGcGceZaYq8HM6cL5oC8vA88e7DyPEciUtks91zPEdeXkYu40xcJnb3nr/f7/j+UnhOcMFUnT5QJXc5Z1wzlqwAtLMQAmy7HTcbz+u7gc9vtry+2fD3Xm+5GTy9F7KHc2cZPDogUAWe1kdNTm3aVq/1VjZqoIc1KqVsppC1VhA84BACOVqWRciTnq0l63l0OB45Ho44F7i92bEZAp2HkhdKWZR5WNIql5XKIm80fEo7zetgLWd++c13vHv3wjQLl3Ph3YcnjB/AeMQ5lpSVJT0nTueFyzLy8ZCYk6aEBMl0wXC37/jyzT0/fvuKP/jijq+/uOH1bc/9NmgqmsmYorVKjSyDpOb+OuMwxCUznhbGS2ZeYMyWcREOU+LDIfI0RlLvMfVeYRxFgnq2VMlwclonRGuxm47BL+S0cDo9MSU4XiY+PD5BVuBqGUeC9bh5ZI6JDz//JU/TSAgDN5st2+2W/X7P/vaWfrsnG8fLOGOHDTOGuViKDSSroANFJSe5nvWllJoeYTQ5p817PmEONF+EUoHZXA1GdXjpcFb3DStqdp5SWmPLrWmm03VPrPXSEheslWpUHVVRYGyt7bSmSVGfIWW8ysrETLrZYa1uvoXrGmpRmJ/6bClQ0qQjcWXJ/JDrB4MMWURdCUTpcWJNbaarIYcY0njmfJwgsZpjpZgrnbzJhguFSFwmXGdBIr433PhbfOe5vd1xc3enhjNFP5h5WZjnuVJ6FqXZXBYu55HzODHOC4fDgdNh5PvvnkhxofMGJ5lHb/jTP/ya1/tb9SBS0dtKb24IsphrnIjo81Dd0+VK5a0nY+MxiLFQsjaSld4IRhEZdJN9OZx4fDxijWN/e0OKqhEMwdZpW928qiNtM/do8hNF/TPB6Z+VlHArtbB9yLrh5yKk0msjZS3L5DndeMZJWHLP4QLx8Mx+v+MP3r7C5gnnMsEZTEGBARGyAdKMLQVDxjtDZwveBawNKlEIcLMPOKcLOuVPTE8qYyUjCuRIwbjWUCjCL1aqoVSVzEjbsGuGgdXfb+FVLQJLDZ2K0oqsenFsNpZhO1RUVzdg3wU2uwFxTQpT6T7UZn6KTHMizgnjDKHv1O+gAmA+eFw1HtLmKykwVXO/xXist5X+lihZo0lN/YdiyFmUKr7UtkZUX3ocE+dz4nxaOLxceHyaeHg48+33Tzw/nzDe8u7diSSG81gYk+H904WxWMaoCGWQgi1CFw3fHxJPl8RUXfuDQLCw8YVtsLy93fDFqz0//vyeH39xy/1e2HSiyRQOJMd1HTtbKFkPCiOlujbnirxqtNA8er75buJvfv6Ov/7VC4+HSNcFfvLVZ/y9P/6ct3eeTVdwJpPTjK2FUsm2Gn0Jxgea24kxhrQsvKoneU65Gvo15FabYWssxnV8+cbyv/1f/wcaNxsTN7sdd7uB/TawGxzBl/o5XSjXsRAlJwU/a9RlioXLVDifIqcT/PqbF56OIzeffc5chF89HnguA5Ptdf2kTCqFmCO2Nrdt6iawRiQZEQyW4DsFyayhiGGcRwYr3G4Gni4L//Rf/QLrhHk6sN1AcYGvP79nIPH08XuePnzksze3nC4LD4tA2uJdoWRLigsxz9V0SKdLOsFoz4kCXdKMFKMazf526sQVGVcYXg+zUgundpghSufLKdemXg0OS8X2hSvjwNZYzSVroWlqY79Gb1YALANpyfXwTYg0vxEAUwHFBiZknPMVhKhGRqtcIq8gg+4eBjHKqGhGSFKkThfhU2PLknNls+katBUcaEByagyFBtpUd7psGuuikLOtjBEtfKS0svv31+/i1RuHtY6hC8zLqJ5BIsxGyOKxYhnPJ3zwxPOFcbZ0eaQLHyFHHh+PnC4zH08z745nfvb9B3IpfDjP5DiRxwu33kP5lrutZd8Jw2DZ7gf6riN0HrFC1wf6wdP1nn63wXlfZUMWsW5lMek5WzXxTthshFwcqexJcceyvGUeE+Ml8fx44uPHFx6fXnj3/h2/+e4dgpoR39/37Hee16+2bPaWfvBsNhuCdzjTcbvvcfdCLq9Ws7PdZstnr99wOZ05vVx4eTowXybyIogXrPEUDDknpnnBWte2GK2V2r+ptUKLsKk9vu6vOtovOVFMJptETnXabnXQ1QZAtqCaamBzF7h/e1dlewslZ+7P97x6ec04TiqJWyYeHh7gu+8J1tMPO5wfOJ8X4pzo+455yZwvFzor3Nz27PaelBMxJZZFWJbIsgiFBdcZ9j5UhgYYq9I5LezNlbVgVi1uw1QQsZVRq1Tn6+jT1jpT1gGUfgsFNI1VirXS7WstKyBFU7LoBNdV0902uZSCVGZbYyTAlfGFGO3psiUlqSbQdRgSCzkaUiqkWJmWS2SZI8usHiW3tx3ztPCzv/nAx4eFwzlxOM2cRvUBG8fEsijbwjtler4cRkzwXJbCJRUeDhMp6/3qQsK6wDKjaW1FZTkPY+b5XLjMELMgueBMbOoDvMA2GHbBcrP1fHaz483djtt9z6v7DbvBs90EboeAlYykRWt8gdkISTRbo+RYax+9ZyqtZB0OXj+vTFl9RvQ+56qlFxPIxZPmwPFg+PDdiW9/88DpaWSZE8s803WBzz67583nr7m70wSNkicu4whF2RRFisobig44C0JMKn1JWfB4cquzxFKK4zwmHg4jl0kYR8v7YyYykVkAw/kyMY4TMUaNms7gnecf/OEbvvzsns9f3fDlm1ve3m+533fcDp5tZ3C+qIyZUb3uUvWAKlCyMoDIOkhLMVWQcmG8JNJiiXgu2fBwmvkwClMJmG4gUs/arAwCGhugAjEZ7UexXnsl17HEhcs0k8xAclu+Py4Mw0DMmSSWHsv5nIhxxotwazM2HgjHMzI/UpaBvOzJxz1FOmwsdJte/SuSsLn7nLi9YXSejKmsKO1WlhTxaA8Uc7yysGhSCVkbd1DvQQUmqh9Gjiw6if4tQKBkrXuUfanrqkVoihFIwjzN635qqizDVEIAFO3bna3DkQp+SmM7oClybWC1smDzCjZ8OrhqaGLRYucHR3X/YJBhiY3S3khRpk6pdBOXLBjj2W/3xHnm/bsPasR2ynTDHu86vPP0IWDF8fRw5P5Vj1DoOqdvwBSMV7TbSH2QS6HvhJw9Qi32q7yk1GiNJWqiQ4xqrjhOC+N5ZBknxtOBzg8aoVZ1w5SaDFsjID/VE7fNXylMqg0RaUizqfSnerAZu2qglGZ83XwQQRxgLKfzmWVJZDG8f/cOgDevP2OJSr8zxhC8xxrNHO6Cx7mk5jhZF4l1sk7RhFLP6HYQ5drksDpMOyM4X8AXgujz2jvLq63h1U3P7eBhUS26OuFq019qFMr5eMRbpfcNw4A3hc4lfLDqIWATweWVbldKrpjHFUkVA6aCLzY7NdJUXKaeoQ3y0/9sEwtlU6AFhNWoudIYD0VZI6ZOQ6QoEGNtjd+SojJGXxTxd/WFoA+JVVtktkEnF6kY7KbDhVAbGN1ATBFNESr6UOccKSvoA0imFEcqupnmxVKiTnFLNhozdF6YLhPHlxfmZWSz73j/4cx//k9/zbfvJ17OkfOUOV4SMVv11igzN7dbPjwe2G5uOJ9nXLfhZbaVBqjGLTMJK5Z3xxnzm48s58zOGDa95XYT+PL1jh+92fPjtzf86M09N4Oj94XOJ4woC0hqgVFMJhdtnsSo0WYp+ue5QJoNKXpOZ8O33yz89Ke/5O9+deLlkuj3W/7iT9/yZ3/6GV985nl15yjxhMkzVooyVsgY43TPKJZiLWFTpSxFcM6xzDoWMMaoiaBz+nynjLeWtMRq8OWIyfLlH+wQY1hSRGLBlKw+FpLanBpoBLfaEJtCie3Ai4znxGWEZfbMqePdGf722xPu+IGXKfEyZfr7t/T7eqgZBY/as29FlM1V98HGHFD02RJ8jzjBdx6843zMXM4zR4HuAqSo6QWXE725kMwO6e54tXf81d/8Ha5kHt4/8+2HA+/LyNL/PULvsdJB6ZSxUtR0sSHR7f6tutash8i1flSfBalgjk4Qs6bW1KJOdX7Vb8AoaKGHvErFCmqApAeaICt9VgtRIwXjHalmJBlrcV4PsZbFHBedoDnvtAhJyxqB2fKjVbNdf27Rn+ec0889qwkY+d80VnJOmSa5uiu3dIyWNy3oGsilplg0f4q6pzYmGqDpHc5eEzYalb3ek0xR5kXSr2sxmr+/fjevwVisMdz0PRO6pziB3jnEWfousMSZbrOBSQhDRzzDQo8xhYWZKUfOCzwcZo5joUiizLHuC4EojsMIy7xwNJlOIvCE9xbrdb/Y7bfc3m7Z7jv2tzNdp7RY9QBx9F1QeYV1tZCttQlCKnWiE6rB3x5K9nz++WuW+TPGMXM8XHg5XHh+OfH8cuTlcOb58cSvf/EBZ4Vh03N7t2G/C9zeb7nZ92w2DjEF7xzGFm73e25v9+SIUqDnmZwmQo1/q171+lK8r/WVRuBeAQY99EUg5lSB2loZfDpHqVdB97PLeMQ7dXfv+g7vXWUPXr9IrGCDx5YOUqYbPLd3NxrRt0SWZWGcZi6XC/M0czieOR5OPD8e2YQN0zQR04yhsN9vGAat90qKmg4Ws579SeUERmwrZVagQazyIUrOZFGQFqMgANAInGRTz5c6qSw16Urfc70ha92p9bE1CrRIg+vXyaKpX6qTbkQn/NOYyEsdm6dK707XXylV2WEqpFS4jDOXORGXzNNh5HxJPD0uXC4zh+PIZVw4TzPncWaZF+Y5EWPk//h/+A8oOfOf/F//Fe8PMEvglMD0niXquZSWCHFht+vYbQOH40ToPeMyU6xjJJFqUTsnsEW9ylLO2CJc5sj7D4+cjiMOS3CGwTuG3nG363i13/B6t+HL+zve3G3ZbTzbwdI7ECLGqnTZmoIzswIJJK13imBweFH/sJLT+jmXOmwiK5OgAfzlujq1X0DrwpKEIp64BJ6eMr/66bd89+0L03mhDxtu9zcATJcLpSiw/eH9I89Pj8pgJBKLsm198MrqFE03wyirdrdVD5KULbkoAyoDkgwxC6dD4eOHkcdD5HhJfHg4scS6hqxKm1/tNrx59YY3r+94fX/D7e2G7d6w33Z4KziT8SbjTMYxUUmPWhe1OsuA5OafZUgxM0+Zec6M08w8Z5apMEfHkhyXRfj++cAvH17Iwz12t8c5r+zKcq3fcpMPNzZmfWgiBVL1TCnq3bXdv+bP/8FfME4jMRVN6aMQjWBiRCRxOwRMN+PSyOCyDm7SCC+Z8eWiiRY5sVhlUW36jlcbmPaB7zKI7ZXBXxlIuUKF1lmkgpztat5XQK1FDMZo8tW6o1VfG+fcmooFME3T6h0lfKIaqDWn9x4hsiypAgKRGOd18FKR2tqIrd2W1kjBrTVhqR4QKSnYRa0jG0AionIKt8q9PmFF/IDrB4MMp1PEW42j1Ie9RnOUgliN50EMfhMQb1lE+HhY+E//i7/CbQZiETrb8T/5x/8IWxaOTxfuF6N5uOPCZxY+3w+6IZuyGuaYXChLZBpHutCvLuDWmPUk6rJht6vmPkkUyavTqxwXKAuSF9UcN9qJkZUGtu7069RcV20RbVxpU8s2la/TytbZS6XVGls1a7WIxxnuv3jNqy8+h6IH4qu3b7mMF3LMnD8+agZ8WpinF8bLzN3tLf2g70XITHEmuMDQ9dzc7FBzGJ2iqVZG9cdKrdH7lkXZAKaoQUiuNYezQnC2bhpCsaovX7t8McRUeD6cVHtTZvptz83WMV0u1VFazQpjjCyzQTpHSurB4b1b72GdUauvhejrbLSfFSCofzc35K1RCY2og3C1PTVQGzpzzdMWQyZVqqUie83h9fqR5k8+Y/3NUhk5xakvBiXjvMe6TF4Sheo1kAsuQy62UgSNFlNJG/I5ZuacmcbCZUw8fDzw8eOJx5cT7z8eeXwaeXg4czmcIS5susz//n/3F3z3OPFP/um3HGLPhY5oPFNEKfYGlqVgomcqjmAcY74g04kpzdjisaoEwgr03rKTRD+f+Xe/vuXzuzv+6OvXfPlmz5vbnt4vBJmxZsKZsQIjqWo+bQXbSp1EU2U/gRgD87KQcUyx8PBh4d33M7/89Zlf/fqF8yXx6n7P//wff8Wf/r17Xt8bQjdhzImusyxlRFYQo6YckBU8MoVslfKpn5NgnGpaW1EmknFeAQgSGIsaafmCmITESFDtEiYqjZ1UcDiEisi2ddhAxZg09WUunE9jRdOFmBxL1OnJVBzfvFw4HyJLgW4YKJcjNnSEsEOcr2TSZtxl6pKVdY01gx5j9NBw3jFsNrDbQeiUxus9Y6HqXAtZ7ui80L/9B/xf/rN/wh9+dctvfnHki7st5ZfP/He/+JbvmTj+4Zec7+4ppmDr9uWcV9pvpdalrIVeTkWL/qy0O+MKMSZ9pnTZI8YQOnUKNklvtLHCMqsT+6cUu1QyLujkLS4Lpggxp3p/dcJmqutwlkyZFqXlSaFUcFHdlnN9fi2hc8RlqayV68Titw4vUTbEGl/8iTmkRtsmlhoz6ZzXzYQW5XTVIrbv1TwkrLPqr2HU+NKKIS5RU0DEIkbNXJXamCHBssw465XpUQ2XrBjV4BplMcQUiePyQ4/V31//ll2dqHu9iSOhxeTlDNngvVfpWM6kJeHEMc8QF8vLCJ134LZ0m4ANW2zYMmx3jNOFEDZ4Ay4lvBGKrQCktwRXm5mSOV5OXD4eKPKC85bNruPubsvd3Zah71Sq1zmGzuM7Qz/0+K7DeVunZVYj2qo0MZNJVevvjaPrLDd7x+eve5bomdOecTGcjoXTaeHh3TNPjy8cnl94+PAtOc+EzrPd9rx6fcPd/Y6b2x2hU0PAvne4ata82faIBFJeNCUma/xw8yIwCRKtSWgUcjWfLSWzzDPOtRQZjRDXy1yn+UWQmJjGieIcx5cX7P0NIQyrTjgp4rrGXxYKxWad8DllNvkgOAIb2ZK5I8fMskTSlDk8HDk+n5jHhXSKhM6BgXmJ+KCsXd1j9T4r8ALWKihlV1p1pk1c1oFQHWa1fai06ZBc66ZVoksDKxq4oGaKospT1kQg0Nqv1jdNsqAVsKGkwM9++oH/8v/1lxyeZxgjYSzk01RrL4M4YZpmxpg5zzNZhMsSeT4vZPEcI1xmOC8CtmMpmRk1HE8lY4shGAj5wuM4YEg8jZkDhsU6zrUWS6U2jgawFo/DpsxSVISWsjaYtkTd44vG3HfiMRasEzbec7MT7iTxozd7bv7wLW/u73h1t+H1Tc/N3hGc0FmLK1lZCtXwkzhTSqweF/qarFX5txrlCepfr1GuUs/6IkIqsUpIq9s/OlQyqTZx0UAFmko25BkuF8PLofDtNx/4/tfPjKfE69f3/MN/8GPevNkjZYYy1zNKa9yUiwJ0dXAxLwvLHImpMI0z0xIZJ41ZLIzIxxdiXKCol5irHnjWd8Tk+Nuffsevf/mBZSlgPW92Pa9ud7x+vePzN/e8+ewVr+5uuNn2OFswRKTMWHOimBk176/mzFS2njpOos5ztbrLyn6Zl8LlsnA6LRovHjVaNJbAjOFcCh8PF3757SO/+fDE7AN3w2t8ziRJKjkoqT46LaC7VIlRrn3BKvIEClIynbMMTvjZf/evePXqNcYGUtRhsR8ctkzc7Qr72y2v7274+vUNt9uB999/4LvvH5lmy5JgjBNGEn/yJz/i7Zdv+eztW5LZ8U9+PfP+MZMNOmzJ+plp822q58m/yQYoRaO/jVFzf7gSlZyr4HCJa8qW1l+1nzE6VtahSbkOSaqHgrGG3l1NJJvwPldWc0qFqXpXGFEzXKSmYxmriRNcY8+VMVfW2mk1NK3Dm099wP4Hj7D89psjXdchJlUTG93A+uDwQTdtqZrVJcKcBt69f+DxsDDNhktMpOWJb17+c+LlzG7bIyTisnDbFf7j/+jf5+0ffE6qjUk1MiAbx9PTmYePD9zd3uGsowsdzpsrNQyjr0dEKfRWNystOj0lFjX2EuqHUDfuXMF07UAR6sSbqjGWZmipP6MZXWbbDCxrv5ZbRrs2voJVtN4IXd/hugERz6bAbo6qaUuFH//hj4k5VqAhs8yRaYocDydiShxennh4eGG8LPTB8PUXX9J1gZwjl4sCAVYsne8YNtpoIOC9pesCUgrLoggflREg9VArBWVikDEmI6VmR58vpGVBBEJn+ezNDiMz8zypREaKmgqdzsxzNWJJIyEYrNvUyaf+0kFmpeVVxE+n2jX/uh2qdY3pZg5STF3klSYFKuWAq1ZTIFdWgZGW1ayfrTE1UiZVV1dTan0iFKsOrAWlsukhbimz5fIws0wLeRmZLgvLpKaL0zQxzTOHy8hlmng6jXz78cQvvh15ekksxfL+8cKUAGcYY8L5DmxHjJ6ts7zmhVksM47JGkYjnOdF/TRImKQUcSl1A8kJExOhJGyOdK6wDYVNcNxuLG/v9/zo7R0//qznq7e3vNr1DJ0lGI2EQi7XrN26sjVDWg8GTT2hFnMZKxr3mZeBn/71S9UiF3753YFf/PIjx5PKSr78+ob/4M/e8md//JpXd56YHgm+AFGfrDyQ5kIaqxYVgzNWEW8BrGo/vTUKmLXD1Ojn1FAhMVLNwArZJIpJyoIxiZTnymBRE0IpSq1XKyOl2a1T/KQb/Pk4E+dCXpTiGZNGVV6WzNP5zE+/fcc/+9l3fLxEUij4MEAR0jQi8wx20ZUqbp22laq3LaJgnVmlCiiw5j3OebrQIwbcXaAb9uSiCRaxUSndwIXEP//pt3zz/YXvPj6xCRvOz4Z/+e47fv7uieVuz/2PDNN5ophCP2iSzlJdrE1SDWSqJoirN4RpmtmirCIUsIsxVqChNtxWUyBiXIBcYxwj1inLLNa4SmusMrxECMYS86JGQG095bLq/HJWhkI7wEoFQZFriWAqc0I3g1KLa3M9hMP1UGuZzc65mhyhYLIRPdxzvE4UgDWqM6Woplv1Z5VYf54YlQQZUdM2o2yuWBISq4kWbT5V9L2XQloWdVmeC955XeOu5tMvIP6HIfy/v/4tvKQQ08w4ltqYi5p7ZtEseYGYHWURRBylGJIMfDwX+iDk3FMI+NJRJLDZv8aGC/vdHidgSASjk6TgDWEIdM7gRCBF3PYGN5zrlH3i4Wniw8MDYj7gneVm27O/Hbjdb9ntPNtNp87/zuK94L3Dh0AXPLZTw2KtC3RvMlVTXiyILfgiuC6zGYTP3gS+/tHnzPNnnI8Tzw8nXp6PvBwuPD0e+OnP3hHzd3R9YLvtudkNvLrbsrsJ7HeBzjusg+DBtNQYA6ZO8+KSOZ7OhGBVymgEaxzJGEqMBN+Byep9Qp0Q1mfQVhNXEcuUFyye8/ORpw+PeOvYbLekWg9UZUGtNxYaYyIbZYDqmdIGK2rIaH3BdgbpPORMnEe22wEfDE9PH1mezux2gftXN+QyarwbKp10XvCdxdiMcRoJZ1FmlKl1oE4gUWBaX+UKVhtrSUBjMWj0Z2NuglhLLEUbUKm+FFIo1pBro5frxmqor2tdz0Iqjl9+O/J/+ye/5PtjYlMcP8494XAiC/ihY9h2PD0fkT5wjkkBcbE8XGDCQhg4U5htBuOY0kw21StrKVgTla2GMKZEnifmmElZagNcVL6XKoMAEJMx8YKJkcFEtghbU3uLwbLpHZves+0CP3r9iu3Gc7sN3O97Xt9u2O88XVBwx1n1qMi5ULJKPRVzqu1pcTrBqBJAU9m7igOJDlhs+xpl4zlrKVHjC5OvcrwMshicWIrRGlSTDD05eyieZSlcjomnx5HvfvORD++ecSbwox9/wU9+8pbtzhF8QuTAMp2U3UfzHqlmg0YZKM53iNOhl63gYbMnykWB9pJYY5dzqRH3IsxjYp4SQx/4i7//NX3Xsdtu6TcDXbB0zmAtVc6aEI4o/0jls7Tkl7oHXiv1NryhDqocOeoQ43yKPL+MHI4zFIejg9ogjxHeTQt/+91Hfv6b7znMmWQc+6EjGqNeYFIotdeCZgxvrv1DKVW2XkG01g84S7LCq32PzwtdnjBE0jxDuvDV/S1/708+54//YM/bW89t5zHpwvHwzB98dkP5i7ecJ9Gh2tMjfSf82Z//RActDl4uCS+zMlNsogXRjpeFknRAlkuh73ttxms90BgCrfZokhtlraiBv3Vegcmc8c7h+04HP/Vex6iAkgBlqfWaqOzC2isIkHNe2QeADtoFus7jvMU7ZcunJdafrzGVueQVVBiGYf2MPzWDbP/fzLlDCCtD47/v+sEgw3/1X/21RvZUunzfd/SdZ7PpGXpH6NQET+kUBmvu6MKZ/8U//oeMJvN8PjNeFs2e32/BGI7nE+dxYmscWVxFxeqktX6fFAt/+9Nf8P2379RgpxjevHmlmsXeMQxq/BiCfrDbzQYKnC8XNrtNLWIXSomYOv1P5YpOSm1MdaNp77Z2ChVKry4BDZTWfqgW8e17NlOfTzXPVBaBRnDoWWecTr5c0WbIUyUQtb/KCWL6TDNrY6qMgYU0TaR5puTM+Xjh8vjE4elAXCIhdNztbwk+cDwdCdWw0Dl9WJ0LFBsZJ5hnnbKlkpTOjiBF4wkv88w4zbQUh8/e3NF3SuHSGkVIC4xjgmTJSTidjhiT8KGnkiFWdBFpSHz9hVTlwpWpQv1fjDT/tIraqxxHdUvo51EaKa2yp+QT9bNtQBErJTGbq4YcUWO7YnQzKCgNW9FYz9O7xH/2f/7nfPfzB06nmZTh4WXhMoMfAsnAJSVKEE4lM/uO3zyPzMXiu4FjSeAdxjlmmRTdlExED9UxweO08DwuXFIikeqGAy5nvCjRKrjMrkzs+sCtz+y+3PNqF3h9t+HLtzd88fqGVzcb7vY9zhWsjFgTkXKqaSbtVgtNpd5umCllpZ+r1LW6XhSr1LNiWBbLf/r/+BfMizBlx5QMNlj+6I9e8+/8O1/wJ398jw8TzlwIYWacLtU3QwGMvAiPH84cn85I1ubbG1s1ZZnkYffqhm53SzEaTfoJ9LcmIhhjtIb7ZIqz0tOzSo9sqe7N1aRVa4pCXDTTOo2JGBW8m+dMSerqPCfLaRE+nBb+7v0zf/v9E3/z7QPPiyDdwE0Y8N2ApdCJxRfV+woGZ1wFrEqlCupbNxVoWBekyVjnq05Om3hvwJgOELows6SFaZppUWffH0/41z9ins8cTo+U08SHxxMPF7i533NrOr3NJTGPM77zOrmwLadZIC4rA4H6LFEPlCbIaw27SHtmqu1sTGTx5LKwLKq7rogsgcCyaLHonVeE24LJotG8TWpkG6JucO4TTR/1MxQ1gRN0kmSrkZL+LF2fWlDpe0jjslIM22RA6YRNp3lF95uOO1XARf/7qpE0lfWlwEMFsXNScWW9B1q46NfNOaopsffruaExq/pZx6UCxrX48d6v6+H31+/mVVyvhVdj1dFYdJ5SfUOkt1ivKUzGGkwu+O2AdQ6JCyVlJDiSCwy7wG4Pm01QhgQFKwVv0UandxAcFk03MHGmmCN2WbCbur5KZF5GTscjh3cneH8gBOiC42Y3cHe3Z7/fsOkcnbdsNhqtGHqnLMegYKf3obL2dFhgpTbdYnWDA4JYei/sNoE3nw2U8jkxWj4+vHA8Ljy/nPj23XsOLweeHk785lfvcV6NFG/2W/b7ge3WsuktoYcQHF3XAZnxMvHdN++5f7UjhB7rhEUSl/GoJs1ShwGfyJqgMhpLrRey0vo7q/r9NCeo01R1ZKogNXXrajVdyVXuBQoPN6BRgQKbaxNlDeKUebXbb9jtN4iNPD9+1HNk0hjyrgu11i30gyX0Bhcs1tcqJOsZ0RKZKFynglBBBa1hMGatKam1T83Ka5WUnofFXE3+1q+Vulb1Vy4KnFIqSFP3tikJ5+w4JccknpgdRjy2QEiwKZYPc2Q79FyyAkNjShyXRLKFkmYuYySWTNf1OBI5RbwxpBIhF7LRyPFv373HSTUPlUJnVSKnDExH5w0bb/DWsO0sd7sN243jZghsO8fdfsvNtmO/7emdxQfD0AneghDVWJIIMl8HjYIykE2deOuMkFQanT7rMKkoO8Fao8y+5sUmLeZZ61cj9cyJqC4/ZxLKYo1ZAM88BU4TzItKF3JyzHPm47dPHJ7O5JQJQfgHf/4TvvjqFcMgiLkgNl1/Vlmu/YQRcJCqJDEbsL4oS84UiqnG1ZWgKwWs1yLbpspnKULB46zXZAVd3qRUiIuedbHkGjVqKeqUqY+CqcOI9QmqLOfqXl7acqp1QYmwxMI8j4zTwjRpOsW3H4+8e7hg/Ja72w3FWB4Pz3zz7iM/++4j708zyTnEdWzCBvEDY0z0JupnKDo8yEVqkrYOqdqzWyobpnkVlFoTljRjc0TmxFD2/Ojta9J8YjqM2PP3fLl/y9d3hq09Y5fI6fkd5+cH7m7v2Wxfcb/fqH+DgW4wbMOF4gw4i5s9pURiFBYmjHMagiAGE1SSscRUDRVVApHSFexr8gNrLcbAsmizrmlbRv9ugXk+g2hqFlYHOqHr6HplZCzLUn3t1K+u1UBN/tmYE7ZKK7T20dpEJfGsfydWZjamykdNXuUdrT5cva4+YZdKHVL90OsHgwxf/eQnTNPIOJ55ejkQPzxCne6E4OmHwDD0St8NA9vdPV988QW3Nx22UnKnKfJ0GjVSJUeOlwtPTw8QJ5z1xCliS81RrbSw6Xjhq8+/wknP4fnC+Tjx8f0z59ORUiJIpuscw6Zj0/X8yZ/9KY8fn5jmid1+q4esFLyBV3d7hsEjVVvfzB9LgZQjrjhFzip1yVij8Sm1OW4NDagkQafrSpvius9fi94KZDQ6WzEVHSJqwVoNHI0YmnRAbMGjDVQoBpGOkj2kTnW/RSgRfvyHXzJfFtKsBfp0GbXY/WA4HdWIqUUvGusZx8jxtLBELbqfn58IXnQT926VH/gAMWbubjf0nYDoay31AJunhWUuONsxT1oEhU5wXuqUpC3G39Y2axySNkKiUSW1Mag+FpUGXf3y9VA1ivKrVrttcGalo5uG+nN1qFftPeuGJEbUbGqlElYgKFf9owgUw8tz5p/+1w98/5tnYnHItucwJ7IzDHkg5cyYZpZcWMRj7IYxL8y5ME8zS46qY6dq1HMkp4RFI5SiyfzsVx84nyFg6HzhVbBsOs/gHLshcLv13Gw73twriPD56xte3QxseyE4CJIRVECZOVAkY6vTceOENIpkmwiv1ExpmeTNX6QBPLXAKRFfCt4k/uG/+zXb3Q4xjmKEYeN483ZHt0l0mxMpT+oRYiNiogIVomDFfImcnibmsxCsmuRMpU7ALWpy0zbYdljK9dW3Sz0+ascu7fdKg+T0MSsFi7CkQlo0NiouC+N5Zr5EWASo08bsWJJGOL07jvzd+2f++jcf+eZ5IvotL6kjGUvfben7AS+G3qlXSswFjdb0Gmuk6MdaBH/6q70X2qFSdXarnr8CmCEEbFFqZc5F6fopEoaBL9/8Kb/6+U95//47Np/tFYzcvSYvC9ldiIjqBhfLxTis9YSuw4eAs01mpKBcA2gkWJ3ItBgx0yi7GWOdFrjWUIqaY3rvdXIbk7IhimqxlU2gscS2Gpg558i5rDINHYJkLPqcqhdC84WoM7e6Ho0YnLcr6t5kbDFWJ+ZUUfc6DVBPhuqoHGvcam6HetN0ywpYgBYjJReWvPz2nmSElPTvpGoUs8pcxGqccNHEimWOWOOwTiNWdaJyfR1NN6m38Ych/L+//u27olHvA2yLqQU90C3FOaL12O0G03VgHS50GGPpNjuCVV8A9SCwOGfoCjhR5pare4ahYNA6YPE6VS2ScECWieQNYiKhJMyaLhDpugPjeCHHhXm+cLxMPJ+OfPPhROcVYNj2HTc3G25ueoatZxg6fOfUvHkI9H3H0HucX3vfFVgT48AmDLb6Buh7F7H0ww4fNmQcj88Hnl9OzJfI89OB58cDL09nvvvuhV/96j1CZugD213Hzc2WYdPRDV4TKLafkzOcTuC9Nk9pTsxmUkq71SZHbDunKrvJlBWkdM7ixDH0G7wfCL6Hoi77heaLVc+QOs0mU8/F9nv1GZUKRzTVaDU6lup34J3n8y/fMvQdl+OR5+czMc6EYNntBrpB8AFcULZojAp6FJEroaDtw23YUVo0pSY4tHODNtwqgkgTWNS9ao0yaN+rJtzUCYKIJWNJc2aJUZmKxeiE1MB4zlyOC+OpgMlcykTJE74UumjZL5ZDyXiEuRQkziwpUkqNbc5ZI7CN0LsZZ6rvlwXwVdtvcfTMhwd8Z/j3/vwLvA/sth27vmO/8ez6jt3Qs910bLpAH5Sx4JrURBItYSznRIpjZSJoVDymgtnVb6f5bTXWq6p/K0ulFGxuPXGt1Wv9rvVjaeQ5FKCpYHQ9l8BpdHwypCjM0TDNgee55/ll5sPHM99+N/JyWKB4NdYk0jnh669f8ZM//II3b3q6ISHuSMmxNocWYVDJ9NMEBZz3eG8RWyWGUpDOwKADzHylXa9rtQrCKwOvTsgzK4gCbS1Tm0v1qwqYWp/IahwNCkas4EJlTJCb5AaNJk9JGdfzwjImZaskIWYhEzguhl99nPibbx6J5sJunzleRn7z7W94OV5I4hA/4GzH4DYM3Q4i5GkmFou4oCbL9XNNJev9kqtEvTHOtWeo6TVxVlm1NUiJkGaIFzad4JPn5eGBf/HP/iXn58/4ky93+Hzh5cOvCCZDH5DNgDHCy+HCy+N33MkNJd6QlkSyhvMJcjaEfgDpaekrRbSWjUukADHpIH3tM+q/na11S8r4YNff9z7U9VtTKlBgQaAm2HBlpra+p9Yf1lpijPp7SJXDstaerW7OlQFacsFU77kMis3m1oN9kqYI63BPWdX6585dE7v+DVnr/4/rB4MM/+F/+PeZ55nz+cz5fGY+T8xz4nSeOR4vnM8jjw8Ty3KB8kIp3yIGbvZ7NtuBYejxnWPwjn2/wwVhiTuGP/0DSo5sN4Xj00g/qEGgeNVfSYHPP/uMV/d3GNRQKMXIPI1M45mXwwuX05nLdGGZZx4/PPLrX/ya82laGQbWgOsMf/EXf07fb3QyUdTEDNG2dp4jJhhciyGpm5FUowt9hKvGTpokQB/I5smgxX2m4eiybgM6OVbUSa2TC5lmh6B0Yz1g1iSDatgGosCHKeDqzDjDZujUmX6MdD7w4cMLn3/5JV/80du18G3GRuOcGE8LLy8jH94/8tXXb/jVL/6O83HCOkfwnqEfcN6zc1vIkaFXqrUVU83aLCVr8d82IaVTG7Zbh3NR9fL1flHaZLEWC1WbicgnjBHqGdkQ/AoUVDfTYqTKPHTy2bSdGtujf99KzQc2QjuWmwln0322IYWSKK7TfP25hZJgMpZ3RviVMcxGqXBHMiYX9rnSNwtQhCVHfJwxOeHJOulGUx06kzGhELyls4beC0Mw3G72uFl4HTz/q3/0E252nv3geX2353Y3cLfv2W4czqm+U4g4I8AMOdXmmnpfG41JVE9I/i1pRKloJSiLQ4rUqUYDy67UdckGg8WWDBLxPvM/+otXhI4rjVwWxD4hNiMmKM3RO6wVpMYZSgZS5HKYuJxnJAWKcVAMko2Ccsmg0ZhaGGGSHqYoUt2a9mqpRJNStEKx1Pvflk8y2rTnWDg9jcyjOl/HRfO2tUAQpinzcBx5dzjzq6cjf/fxmV9+fOGSLeK39EEbhr7zbEKAouix944l63Mt1fitrNSc1fWhvpq64YpUA1ZLGHosujHnUrQgKK34rSZGRqNThcI8qkP0nAXpd8hwy5KSggehhzTjcgCjYAfGYaxDnaUzaRx1Uze21Z7qZ2BrSko1YnTe1QY7KSRlakFmtMBNRSVFlubZofRuRbZdjYmLOG/RM1Z3R2ssOS0U1AE+pqUyycqqJ3Srzk+LoTUBwmjHk5OyHgqpxlZqogeojnCe50/ogOXKZtBbv+psQSqlMLFU2l/7OruaGlnVUaLZ6bnUCWBF8611dUJQpSj1+yxLWumFIjp5Vdrhgvfd70GG3+EriTIU1xg6qwa1GM1Nbwa2+EDoNiTnycYx+QGcw4qliCF6iw8K8qu1zHXNmlzIJRJLIpKIFDwRj0rWFlMoecYQNRbOWWxJdIMgxqtPQhrIKakHSNTG8nyc+PB0gu+POA+bTcfdzY79rmez8WyHoP/eeIbeEDrBem3qrTcEv9HceBsaMZB6gBAsGH8BhP1Not8GvN9j+Izzcebl+cz5OPP0cOLjh2deXg58fDzz/sOJAvjQsdsO+vO3nqfjzHYwdMEikthGr0Z0rihDr/7S2kcTnOI84rpOhxQVV9d/NSp3TUCrEz6VfjX5VK0OaryFlFZrNQYc7aABKggrGWyiHzym3GCKYRxHlilSclQfsGLrAEYqM9BjxKv3UMk0JTkriKD/W6pXlTTpAyDWruuF0tySdEBiBAz2k3hfodrtgxTyUpinzPPDiY+PL3UYo5+p6+H08YlbKdigklEtXQPBQrCWIRRed3v22wDFYSy63iuTq+s8nRE6b+iDYdtbthtlyzonDJ1jtxsI3hI8BKNm3M56+s4TnEpXVbGnaSFCJudFwaFSAQ1r0PmS+l5Yp+BvDZlSCTSFnNTNX0EXFHRJYJ2hpFTLP0Outwm0JMxSvxf6c1JuTEqpqFteDUulGCKZMSZkcjw+CT/76cLl6cjjw5Eshu12x+vbgZRmRODrH33J28/33N95hk0BOVFMVPoBBUlOf1YWptPMw4cjFIczUZngFrItJJPZ3O0Ybm5VBlSagKgNPW1dT4bGpGtsDkGQGnNZRIF3KdUfrVaJpjJYFYxKtbaqz0Uxen5HlXWXmIlz1Nj0ZWFZMkssLFGFL7lYxgTnpfAwZn7xceZvP5w5zRfm/MS0KL/I2i3eO4auo+s6dpstnXUKviZhvEz4jacLDnEBsX5tmqVNhJuyUlC5d2U4WuexPqgPxTjzfB6J336Ps+CNIccdx29mHh6/5cOXPV/dFvYuEwatF1MsPDw/8i/+8q8oKXF7d8Pp4YX37x6Yyoan8prT+BnG3iLiKhBqGZxXj6o63IkpkqKaTrdBlLV+ZWC20kD/W1gWfUPKGLg27SnqkDK35t9cEypazQS6fmNSTykjVWbeXktN/EtRGcNKFp1oSRWYKzingEb97yXivLumSgg0X4n0CRO27/v/ntNUrx8MMgzdkc4XtoNQ7reUvKMUS0JjfKZZI/kul5lpnDmfJy7jwjRFDuOFh8ORmBKQcEULXe8C+90G5zKPNvHxnWWzDbjO0Q0d3RCgRIZOd4jgdfP3HrowcHvb8/btPcp4LcQ4czmd+eLNW04vJ2IsXM4XLpcj83KpxV/7IAXRwGUM4FxApCFMFYAon85WWYEAdeuthbW12lzX72w+cRa/ItFy9UOg8FtXQyfRJrl8ilY2ylz7OlEtEFKQLGSJPJ8e2WwH3j99z+uvXzFstgpWFEfKhk3y2uglx8vTyOX4xLAxfP75HS8vL1wuM8+HFw6HI0M3MPQB7wxL9Bjb0QWPGo7qQ2OsYJ1Q0oJxke2No98YjCtYW1YtYcka9dnc342xn5g7lk+eJ6mAANUnoCh90FSkvlIC221bGxLDJ7p9s36r66VNdpWWAZrVHJek0XQ1osRYg/HquvuSM88mkZ1FWCi+tpN5wYlgTVbdoUAXInevPN4bhq4nOMt+07HvHds+cLMbuN04tr2hC8J+F9hte7ogeAfO6oHbeS2yrAXnEkucMaYQl1m15pXOZI0eCGtkIVmPCmOqL0ibFIua8MgnE5BSqglXYzxQYet676s/hqEgsmDsRGkeC5KrcaBBgsE5Wz9TpabqoWcAbcAulxNxngiuq4WS/kxplAVMfc50PdOeqfYEVZ2knnllfc2lFhONldGKxmwE4zwxR8YxkaNhXoTDaeHj4cy7l4nvns/85vGZ71/OzASy6zmyJYm6vYvruN9vsSYT54klRRaEHIXgB2y/RbzTSdJaM8qqSSttXdYJk7EV2epEY1rrvmBEQdJY5QWlNLNSwdT9YUmZf/lXf02MM93QI0tkniPL4QWcZWMdYXDEiuKLdQjUhiORoxZYOed1qq97FCsTQCc+Uqm3psod0ioNCCFQisq1lmVhTW5AHa7BE5e4Gs4qdU7ND0MXNOrS1M++vq/6ia+IvG2gQpVKNDfpT3OajTGUrCw4rc8/ARdoee1X9+Z2WesUfae+7orkr5OQug/FFFe5RU5pPXxzyvVgvu47OnFUoGZJCyYbfSaNPhOpxnTFuFCqKfLvr9+9qxh/PaesQ6w+s8Uo+FmKVY00Vs3qUmVV5YKIxVuHGP26xdgKmF7dntSTqGCyYFFjujlnHBZTEiUlUtbpsS0KPIRisEX9IIw3SA5Yp+6tKSdSgZgLYZ6Zl4l5mRinidPTwsfnR6xoTPVu6NhvO25vB272gf3W0m8E74S+DwQfNakheHwIGBcw1oNVuZsRfV6d1abJuYg1ns1NYtgGnLlhmT7jeJg5Pp95fnrmdBp5fDpzGRPjZeb55QySCcGwCZoG0AXLfjfSBcuwCWy2Dh8gdOCDYJ1KS9xQEJdoUZelJFKpaUL1qBABrCOnGckKkjtYAXj9ulYo1P1cautlBNIK59MmOiLQ9Zbb+xtu8p552nIZX/CdZ8mR08tYa0P97EPzsV0/75pH30AFVBqzAhxr3VKUxZsVCFib4zYxFPnk7+qEOSeVsJ7PFw4vC4eHC6fzxDIl5nnCe0+/O/BqWPjf/Ic/AQlgLc7ZOmASrC1q6m0NQ3D0Tb9tnXp9BE/fSY1Wj1hbCA6cKUohTzOQFczJeR0K6ZtLeK+eOAWdhjbAFnQPNpTVt8FYbRwVaKj3oDIwdYorKxZkTKsZUKYLRX2IaI12GwNobaRNk1lBCdO8ySrrs1QwQ2D92mVZSCmSJsfDh8hf/cvvuR8ufPH5jq/+4DVv3tziHKQyksuM6yzGTDh3Vm+eymDR+Nk6/ErKvDs+nVnmQvMgSlIquJOIJtJvFDZD1kzXCqw1LyOtl4yoaaWCUyrJaoNMHdI0ZmOlY2Mp2Wk0aSxrskhOhbjoAGSZE/MStVaOKrdISYjRsKQqv1ngPCdeThc+ni68ex75/rDwd9898OEyk12HuB5jA84Jnfdsup5t36lJqAHv1DAyxgS+w/pOAQbjV8k0fPI5Gam9gjKvCjpQc3S4boPf3FCMYf/5FzgLz89P2OQoeSAtE8d5orAg2dG9vSXmhWkyLE8z/+W/+Gv+8m9+xm4T+OKrP+Hw4QP/3d/8HTN7Tt3I+01PvnmLD34FN0pWrzIpNeY6Up/3ayJVSnn1hNIYdQURSpUzNG+WtkZLKbVWKau8wdjrECaXWttRpd8NIBJR5mmxGKM/t1R5mam1dcwRF/y6p7T6TT4BQJvE9dO9R9CBzZqjUtRY/IdcPxhkQCZM0z9Z1f7krIZxPhS2O8ure69ZsGlHSnLN0Z0z4xQZx8jpfGY8zVwumbhkDpeJlC7kNGl7VDdk33mGvsNaQ++cHkr7oWr8HKGzWKsumNbYqtt1dH1PH4Iemi7UBisS44gNBkqqBnt6KqkhZMGKggvAGotYT6brLVAYAcQR04wU3dTaYVEqzWk97P41UCEnLajNp9+Uf/Pv/dYfVHRyRdsbECEF21nEW95/eOB4unAeJ/rtlirA14mzydWYUgjBgWScK3z51We8/fye0+nC89OBl6cjMWaO57OayHBDCD1xMUxzzeDtDD54pTQaENOz2Vr6oZm3mYqsGqXmS6mHL+sivkbUVWoPnx6i16b4kz+s+vx8BWHqg2lqo0K6fj7rlLMCC6VY4izEJDx9OPPydFEX6aSaQmst/Wbm++9mbjfCj98MhM7TOd0otn3Prvf0zuEtlYLq2G2U3tYFy3bf0wXHpnP0TtQrpPeqIcw69VBDUshJfTXaBuSdmnUYpw1bqpOXlCsYk9XcZ6W0l7LmBbf7baiuzfU9G+Raj9RDTeST+9w2NVCGTFGgobQDzFYjoEqz04mLwYgmGFDUDEknN+37Zgo6bdvcWAwZJ/qclRQpRbBe5RPLcjX3KRVwus5tEjrZyXXir+vqutXJ9XlDmBb49sOR41E4HBKPjy+8nGa+/Xji598+8d3hwtMCE2C7gWHYYK0jiCH4Du87Nv3Adujx1nIxwmVZOF9m+r4j+J7iAxGj2eJVnmOkhslWtLoxmdpAzDQWUKNiCBUwKxi0YC9ZpTRqmJkI3nKzu2G3Gfj+3feEvkPsAjYyLYnpcGDoN4QwaFpEjqRZWVClQEz6Wbp6oGmVqwCTFiVa3M1R3YtL0rQJ55QNY43Fd6GaCbVG364H3xKjNuvG1PSZvFItP/Ve0GdTGUcxxVZh6duvIGzT+wXrtMCor7V9PzWmbOu0VJCyaRGryW6Nd/oUcVctrVtNipo0wuWiIEyprIMqsdAiX6dfpoEpRSp10awmn/ooVsrhJ6DGmoMuBu+qPrT8f9rLf3/9LlzGh3pWSQUW9Bm3YrUBso4wbCptttJ6naMYQ50xq9GtNcwYbCmkukc3PFfqRNEUwRVDLA100DWds2rcXRG8QCeG0IoKsYhERCKmRERmTNG91gSHsxoJvtkM6k2TItM0c55HLtOFh6cL/t0zfefYDo7t1rLbBm5uNmz76m/lhb739ENH6Fp6hcd3HRhRSatU8z4pWIlka3BWGVzOFHZD4PXre1LMnMfINGbO58ThMHE8XTgez1yOZw6HkWVZ6LoOZw3boWO/79lsHdutZ7sNdB34LrG5AZ8K2IJJpebUa71GyWtztcwXDAXvDLaee1o6/fb5B7bqlmGZJpxRE9c2JKncRwXcLbhesOLxmx3dYul6zzRfOB2OvLycmSYdAnnfYYwhBE1E2G479vsNpSU+yLWpoE2eYW1ItfmV1QhQFN355HXrWspJmOakUagvI2kq7PvAzabXvdMpKGWc4cc/3vPvlbe6l6OsMGuNeguhrAJXY5lNG27lgtiMkZkiVwaiqVIJQ6mAwkxBB25ZmhyuAfD6PdRR0YArGu1orsBb/Ytaf1hlWza5TsN5TW4NZllZgBonzTqzauRiYxvAoMkUpd6+dZ7xiVKmARWaeCZr6WlQm6+yRCRlvF346vOBP//7HT/5YsP9nRCGCZH3+t6lVOatvg5bZX3rud9Yd8pPJ8XI4XhSnzmxChg0RmjRisfUhDyayWhRgKweldVjpL75Cl5pfyBrM3i9uUZBt7lofGmO5KjeACnl9d8pam2mCYKQslCSps4tSbjMmafjmY8vZ96/nPn4cuLheOHpPHJcCovpebosTDisCfS+Y9NZNsGy7zuGbqBzTpvslNVQOoPzAdd1uH9NLqGSSlnrV6lN6DrU4Trhd6HD9juViXc7ppyY7YwzATGGZCMxXXiOB74/ZKxNTDvD43Thm4f3/Bf/zc95uSzc7Ax//ctnnr//wC9//Z7ZnDmFyPKjt8iuEIxVE04jGr1d60GVvHitO+rr07po1nqmVDvLUqoErtUtOqhvJpEpalJWzuqfZ63Hia+DRl3bTe5aFl1XqcZWWmNrbZNaWa8yXzJLXDBWI85bSlesUs/mF6Ms1bSaZzcGqJgqVaXWQYUfXOf8YJBBmoEW6ERPrNL+KKheSlGaoqco2RrwutmUImQ8uRhSviFFWJZMjIUlLizLxDTNXMaJy3livIzM08Lh5cRSqSwaNWjwzir9qrMa/9QpoNCHgPWCtTD0iZQWtpt60FjBOk9OkbkarbSp2ZJmMOokm4sgToUqVe3UsG8tnutzXHLWNIO60bZmKZO5ctpbMVHvH4ZpmchLpA/+tzCFK1r0ybbwCS37+hcbzlC0WHGG29tbHt8/ARZjmtAyURq1yujBIKWsNF6D4J2lC8LQbbm9GRjfvuJ0mng5nnl6eOZwupCzbrQ5p5r3LOxuOroBuk6p0523WKfUnBwBLMXY+qrzeng2+Lk9fMbkleJlTEt8qO993SSvhy8N2a+HRANzFHi43iBTm5JShBiFaS48vyw8PV04PI8aT1e0mJtnBWHScmHnCv/x//JPQAyh0lO9d+w2vcZ2SsFa6AaL7yzeVamAgA+eXGL96DNiIuIqmp8zsTrsO2tJut+veji9r9X5XwTjdPOixosmhelXKQm5XPWj7URq0ZTXP9AiuYFktVihvp7rmqt3XKrsINUm2ShNtCDEWb+BDa5+eZ1UlEr3pGplM4gt3L3ekCLMk8WJbriGzHi+sNnf8XiG9++f+fpPv1QCSkP6acBCRfvb4bk+DyjqbrUaMSJkLOcL/Kf/z/+W7z9mEp53H57BdRynxHcPR0YxLOIo1hF8j3cB7xzbvmO72xN8T04Fm1SeMoQeYzy56OTGe41villTa7SXNdVQtCHtrO/hfDlXKllXgRnTXFtoMolcfxXANmPSkrGhY+h77m5viCWpF8o0k+cZrBrAxvMJ12+wriNVTrMi4CqZMHxiClbvqhSdjolUemjO1XxIp0fOKwXVWUfMmRBC1d/pM26MPmtGrpnNTRLxKXBIUU2iIGAs4qTul6VKMqAxG5q5p7SKSRSxzznV/1WJRq40PfPJz1n3S6OHaopNWqHARy7LujeAyiOMd0hUjee6piJrLG6pn6kRqV5pshYz+kxbrfNsqfTe6+vJ5Kt2Mbe59e+v38XLh+Hq5WE0BpJqAmfE4rwjdBukyiLEOcRaZS+IMhgwhiJO9/pMZQ21WkFpz7IybBQgttSzVgpzmaA4LILLhg5DIGOLGtJZiQgRy1KbbI2vk1JUJ28spRg17ENlYDF3ytCpRexhTDyfJuRdxAfoek/fWbaboMy7beD+bsduGNhtNnSdoxt6XFCvCeMF03uduNfaQrAYC9hMsQtGEslluq4gt46cO6ZlyzRlxjFxOs4cDyMvhwvzlDlfRg6nkceXM9YIzhmGzrEZPJtN4ObVjn4X6DaG4AppjkhRZ/u8RExl68XxQuh8PfPqmWiaubae22r6bTCi51qMJ4y1mLqXsgoU2x5X/eQra9W7gA2WPuy4KwbfTZzOC8uUOF8mLqeJOKtv2Odfvub+1Ss16RMFkUqBnCJSrDYNIrq/SmNB1aGXrPyL696bC/OSmGf9ZaxTzw+fIUWCU9bA9mYgEsEkpeGnxDzNUNQDwIgQS1zlvN55lchVwDibXIcgtQKuQ4wrI9UonT4rYKzAb6nxhlcvKNFQhCpB85SUsJX+L1lleqkoyNYkzka4poGA1kat2czlOhCs0zyh7cfoZy4gxSCViaLlTwWMcx2AlVaaZsTUwWP1rDIFHAUnhSEI3TDz9eeBP/7jwO1wwrtZ71XRXggMVrzWbhXxyA20oGjdlQtSEhBJsbDMM5R+BQpyrZfVM07ZO7nMFGkme3YtiepSVsVvWy+UOui8nn0UTWPQSs1yPl0Yj4mSDSUZ5lTU0yoX1N+yusUk0Zj2uXAeJx5eTjwdLxzmhe8fnvn4dOIwLpoaKIZsPK4bMN2WNB0QLH3f8Wq/5X7TsfXCpnN45ynFcsqJZSmkJXGz3dP1HcV1qCOuVEZ0q7Nqtl8zMjSy3oPrAKeuEe8JwXKaFpY4Q+jBOL13wZFnOC0LT3MifbjwcIg8Pz/yVz/7Db/6cKBYYXOAS/kZp6cDzy8j2UXKzYbbzwvdvJD8okCnczjrSDmpseInfdyn3gb6Z8rcIelw1XtPTOkKzqWCta56UGnfuCyaICGi5toKuumAKsVMKjpdNSJaK9ahjDJTM80XQoB5nkg54byjLCo7Cq5b9zeo7Iqi34+s3ltYp0MVqSyLrMljmULw4QedqT8YZBifn9eoM6zFGLdqZUpuSHJ9vVKjFKC237UFL5AtEHTJUyBjKfTk3CtatmTinCtdJ3G5JC5TVi3cPDNddBJ8OsxM87yi086qWZFzQterpuxmv2XTB4K3WAfWFJzX/HNb49xS0cmtd/phOK+0f2l0/fU9taI9V6MvpdDqA1ypWq2pq1+klK2KO4geYGt08qdN3nq1qMG2uTY0TBeTtOq2fX8Lw9Ap9a3SrrXhq4fAKveQqmGS9TVJUY24sQmxgu87hl3H9qZnfzNwPo6QYDyfmS5Tza+NbG8CYg3TsqgjczGYpaiOuxrHGG+rDpv1gaEeHFCZHMZUPVz9mk+ah9a4UL9O+xc98E1F8QT19TOfHEK62tSg8jzC4TDx4cOFaSz0Q8926Oi8QUrW4qKor8WwDYgUvNtiLZAK0ziqmVFQ8EwLw4LzioBSdEokxuBsquhgnbaaoqi0KOqXSkKswzghtf7Zo2oBVw0ZrcoIrBgyRYvX2iA2qrrevkp1b0Z57UBJjcnAtVGH6gXS5v/Xz2P9uxX9bnYiRdTNXGPEHJdxImSh33YUEgWVPLVDzdbJgxg9xJwbOJ5OPH08c3x+YVkW/ugP7+kGwQUop8Lj43k9FJpnx5rwW1DTLVHX7GZqWUrhdDiy3Qw4q8W/FEuK8JtvHvm77xYk7Pj4cmZ/11PcAH3E43DicCFwt73lZruj845SMl2vU7rLZWKZZtXWOccQAjkl7vaOr756xffPF5aUEBtqDSEruEBtIKCQU+Tx40dSWhg2G0LXc3t7S0EPAY3XKjUHu03dK5xZc+BP5wvPL8+ErmfOBYfFiwMmJMN8PhH6DdE6ZHdb17zuvLYewjkrQJErJdW0uUZdBEZkPTjEWEQM8zKRC3gUFIjOYk1Sw876iDWnYX3cTGWZ5DoJWSpanytYkClRWRVU1g5oLrT3viLmWfPui8ZzaVSWMidC8Gz3ey7juBaoVG+HRt1T35ZrXnNjU+Sa8yyIGlblRJBrUs3KRKh0zByb2/w1Kagd2OuEq0aDlpJVE14vYw2mKFCBKaSihdPvr9/Ny3cboOm9qTTvyhhzHus9GJWJGlFgv4ihiFUNsXFkdLCCMTrZLTqwqAEqddM2rLHMFLIR5pxIuagMQ6xKJhAihqkkfPZYBCsOIxFfvHq+yILK2zxSFIAwksBovC0ODK5GGOtrSBnivLDMM6lEns+Rj8cZPl6AwhAst9sTd7uO1zdb9hs1cdwMDh8M3hd2tzqdN87iu059tMKAIWOMNrcsC4juHYIlBMN2MOTsSa8HUrxlXgrTAnNMKq94eGaaEofnIy+HA0/PJwVhf2lxwdAPlt3Wsd8NdJ3h4cOJmFTS4JzqnAV9TosptbmX2nhqZ1nFdqQUsdZjnafN1lsDh3xavyhXT5vWWulawVvHjbthf2u0QSuWeY6Ml4Xpokbp+/1AtxmwXhBbi/WUuJyOusZQZ/oSNZ5OVmDjOmmvajKWORGXqFTlop4HXfDIEitwrexisdD3hnlJWovY6jRg1F8mODXKjDmRREEiV0H0lIpq+asEojRMoTX0vxXpYJBsMcFVIFmnnNaotFOM1jaFCra5VsfUIYipzWM1WnWm7aUoqXFVrqj/Vmsqi6HWt3Kt042sAydpINHqQ9Dq7tqj1lqU+jyub0ffkiY4SMa7QucXbobM528czp2wdkIkUorV+tdo8lAx4MXWKFUFEPXzFZqdm0p9Ioiuy2mZCc5Vby2URU3C+IKxEWTS4YtU/4HUFmZbo6V+7/r70uo/af+7gh7WOZwfmJeTxnlnYUkwxsycVPpwHCcO48LjWcG/5+OFw3nh6XhhjJniPS/nics4V5NGV5+fjs1uDy6wWQpiHbvNlte3e15vejqTESIpRZaofgSd90gu/OTrr9ntdvzm4wOzsyRT/dg+ARnkE5BB2hCyskOWqBKCLAXrHDf7HV3f8f79e2VF0GSZUKwjzZ6Pl0zqt/z8l7/hV998x8fnC5eksfdnLPP3Z0yEOXu8Hej6PTbo2bDME9FEOukR4yqT3mptbtVfrEU9GqP1jqBDxdXHqQ7KUslY58gGZQpEWRkNztjKMlWwIeesLB1jIavEwdjK5ChmrSdz1uSrlK/r3jbJheiAJ6dEkuqrVeshaYxh57RvSSpjcu4KoBhr8Y0NYX/YMOUHgwzf/PI7fPA69bLga2SkbopSTUsszhqMdUrxrk2fNpiaGGHapB/VY9uiB7p3gqMwdAJRyMVTsmeJkIqQ4l4LuCx43zHPC88vB0oWllnjkU6nMy+HA5fDhAg8Po2KtKaId5bgLT5YQnAMmx5nnT7sDvqgDsx9b/FBfR+02WmbvT7QuVRacFGugFXeGsV8gmCZSrRbkdVMWmY1PfOKMl+f/kKbPje6v2nWM7UZmKYJSqEbOoDKcvi0WTQUqfdd1MVfR7+moqPtc/ikEZXGvWg0G1EmiLEE13O76yoqdsM8LrwcD6Q84ntHNww8PV2YxwvL+dhG8xUZNhjvKxKrW1/oPH1vsI5avNeDohVdVACkLeQKhly9GK7ojKzT+yq7aMhOsZRimSc4nRPfvTvx9HQh+MDr17fc7AOnwwPeqHxBmR+C7z2bvaGUGWsyRgp5yeQ0YrzF+SoNEMAYNaSyehBrx6IHotgrULBSHKsxoSlKtbXOKo07F0yVSYgFydq0GmeQnCCVtZltwK00rq2p9CXMCtxoXvJvUynb/VWWiK7XpkEkX0EGqLQrGuCgfh7TOGOt4EOH9eqnMcdFn5mKduoxoBNeY9QgTaTndPRcTp6/+sufcTyfsC7w7/2jHzPFyDJncml6dnVNLlnvu626TY3aSWo+ZmphRyYtC5ReTYvq4yOSqpmYMKMZ8dY6xAf2ww7rA6Eb2G637LqBbTfQBc9lHDlPEzEpTRTna6SognBBCm/vN/zZH33Jy7/6Oy5jJKelugTrpFJci67VqVScF54ePmJN4d2778nF8D/+R/8+Yr2yuUo136l7olQ2lBSjLvKVcv/ucGTYBbIUsFbNukDR5WUkLwuXh0e2/VYhH1EJl7Fe99ncEmEqPU+uKHcperCVmFcpRFxmqtNjPbQXYopImdWZuBoediFUw6m6J1pTHcAT8zKvLsghBF1LdfKvDYb8FhiAgHGW3ln90dauh2jTHZ5PJ5akTul5aQi/LtyU0vo1V7djjeYz614MTRM5M6/NRRFdQe1rnDUK/on6OSDVeKlkclGn7lK0GchV3nKV2Cog0SLPnIHsrmyJ31+/W5exoRbndT9tE0lQrT8wxoQPHXgP1mG9nrvGOmqwe407BdMM1xrKV6U4gEoAi8ZlJtGviQkyTkEGMpZMLErid1gcBi9FWWIm44pHWKAkKBOSF2U4ZG1Cc5oaeU33A2l7r54rLly1vUvMLDU2e0yZ8THx/uGZX9lntsHy+n7Pzc2gzIJeuL+9YL3ggqUbOkI/sN1FxGQoCRcMMZ60CHcBRJMrSgWkmxRuEEMqgpiOXHqW+RbwHF7OfPjwyPPjgePzmcs4M00zT08TT48n7m4j+/3AL37xC4Ztx/2rGzbbDc4JNzcO3xeMW+g2Bh/03G6NpD7I6pRvhx12TYZSw8dcmaAxLXgrFZJo3jI6Mc6i565SA3Sf8DYwmA03qYK1VQOOKWiGsdYDOIuLagLojKUszZwWliXqGYggzmisYczEmDgfJ0pRENZVTxxvNL0qmkJxui+LBRPcdQ+udbdxOg8PvtZOlXxrDTgrbVxWGXHVNJhyBRnWG1g3WWeg2OoPZBCrNHjrbJXjSR2KtAqjrv061KA1yvVb/haL0VKHLrXMqoxXQzXapPYVdZjVmu72+g2QRda6R9OU9DxUe6U6fCtoA5vVLyWvqEUBuyBmonPwxds7gnuqQFOuSIc+0+NlQmwhdB0in8RC1slzKYXVisoWQme4f7XnZx+/Q2xgd7tXdkWemKcLw9bR9RnKTNOPrIOCNoCsAzsxhdQyUSsAIc3frAKa2QhPp5HDZHiJnsslczzNvBwvvJxHTkvi+8cD37x/4vkycYyJy5KIQMHpAMl39N3AbCE5ofNqjh2MY+gHtpsdWE8XBqaU1PMjBLIRLjFBTlWWLnVQBMZmbvZbbu/uef90JBlHsb4agTeQQd9ba5nF6JrUOkKHNqkaRntj2PQ9292Wp6cnQLiMF6ZpZp4XtsOWJVtytpji+Dg5HkfHbLfgqnGit9h+y8ZahpQJ21vMq8+wncqQlnlBjL4/qc9gS8AD1nrEOUfntRZKzqs0v7FZszLzbQWE7Ccy0tz8o4zFWCFmBUOliErycq4gQO2hrWVeFpytfYbUJL+USXEBUZl7qnWXd76eB5mYig47aX47rj7i2guuZ1VdXXmNF9Y67YdcPxhk+Mu/+YB12qB3wbDd9vSdx9mCd0oJcd7hjMF7g3G2ejgIWEMIQfWOUmkwFYxIRaluvvNrzme2Sc1YpaiJGjoxVxqwxXeOVITN7YbOD1jTkWJhHCeej0dyFo1YHBOHlzMPHx9Y5oUUM8/HC9N8IeUn4hyxRiPJeucZhsBmE9gMgX7w+Or74J3gg2rYhLICJWlOWKueD9YWbFukBrK56jBNKSzLiIitAGNt+mGd1DaAoT1QiuAK4jvyedQiou0xdaNpZnrGV3d+MfX7KsWtaabrjHhFfLP8Nq1K2Um6YLzJFFtBH2fVgIWB/RSY4lx1mh0u9Dw/nXj8eGK6qAO8D75KYPSI1okpvH17z9c/eQ0lIV51rjnrK20UnIY4GjErldyJ+9cQ2iYD0Zg9U53wS3bMOXB8iXz8OPL8fKFQ+OzVDdudYb8VNl0hnRdtXMnKHjCG4A3eQcxaHK0NhdWN0HtPshplVhDEKUU0C8RSCwSDolDGVKmIVE1TVupgpc1LsTVzvR3WukCK5Pq+tHChTTKgUumqgQ/alF+TI9qhorqWUtrkdQXoq/a+rbbSCC7aKBeq1tNUPV4mZSEROE8zlMhu1+OKZZnheFrodw5rA8YoupmXwpLVGdma6leSDc/HmTEZRjH86vHMq3cLT4+PTBfN0y5i1rpimUZscVhHLeoTedHMZFNBMFcKN9seS22YDUDEmcyXb285LxMfDpndpqcLlm7Tc7/f03cDIXT0Xa97SyxMlzNLzCCOORX1WliTSxT43A9b/uSP/pQvvviC8Le/QWZFhsV90mA3hL1SIdOSmC5n7u9veTkceDmPTNOCH9RlHqOJLUijdzZ1lSAZdtsdX3/1JU/PBzIKruYm7jIGcR6TFiQn8jwzOEsxTimLKL2tmOovUz4FOSsDcTUiKtdarkBsfgo5sdTXtQIuBloqxWW86H/Xb6qZz8oYcPW/QfWoJZf12Va5hhbR1nfVQyKvjsp9CNrkzDMtoilVyp+rTArjK7hlDMuoTt4xLqyGSf+aAWT5FMGs+0xK2kBZYwgVnY95IfhQJ5qGFv9cSs1Qp8pbyL91HrnKrEqVgq6fUQUprvqt31+/Y5fxWhKVNjGl0b6vQDGia9RYi7gKLnD1ZZJiFHCtgEIbPigD6ooAK5fw0wZISMWS8EhJmKKiCIOCuotoUeoFgmgzHFEqrsiir1FmbNFUH7L6QKVU5UOVOm5bVSktAUCZh94VKKGyrtDkiiWS48zjZeJlfMR8/4C30HnHzb5ju9WIwpubLbvdwHZzIHRqbtwNlrzMhE7TKtQNPisLzQClkEW9figJ6xQgZtB9Z+gd+/0Ny9d7lqUwL4XLaeZwHDm+HKEU5mni4Wlifnfk5z99IHQ9m83A3d0NN/c9PkRuX+/Y7hz9YAlBpY6mpMryUzBVclrZWUVK3fOu52YDmlrUbkHP8WxZz2wxGVzU9+YFUww+WI3By5FW55nmN3B1pNQJf7FY8ZjSsyyZkvTcX1JkPI/KpjOGvtMzrlQviiJ1yJFFQQTXECRDMqhvmBckZaolAMVpqoBWo3UwUqfp1LXbJv5NWilGGXmt/i00aZ7WLeuJvpopyArYabOinjhtX4Ua3Qda06GAAlJlAAZ91mgSDNYJfln/XM/OnK/gDw2KLgqu5MpMK4U1slikig+k1M+0MmTrWVzq1yU0QUFSxMXCfIkMbfIr6oPkjGGaFiBx9+q2vnid1q+Rm817IysAIMbQbQYeH8/86lcjr97MbLaet296fJfpNpmu1/rHSf0cjTJhKQ0KkrWfuMZ6KhCTUx0q6QSMmA3/9F/8Df/NXz5wngyhv+Xj44Hv3z0wThG32XJaIg8vR5oYK6Frwls16Q9dR/AdMSWcWLabLb3vCdbw5tVndP1AsVo7fjwcWXLmMs0IgWBblHed8FPwXri/ueeLrz6nSFC2r/HKkq9yVCei5q5lLTvquqvFk9TISqDEhFB4enzg6elBaw7gdDjw9PzCkgvGVYp/Fk5Lwe9fsX8dcfNMzGpCai0M3YZNF5BisLsbZHeH9Z5Y2UI5FR2s1ESUuQIIrUE3xqi81cU1TlLXl/plNW8rNWuMNDNrZx1DpxKaVKUTPhe9J1xZmBlYxml9JoyxNWkirjWRNBZInbqsrJB63sQUKVGfhzas6bquMkuvCX5xnpDG1KjDTVAA/YdcPxhk+Od//R1iLM4IfRButoPq9xz0wTL0HcOmJ1g1GHQOuk79EIxVfa3ve2zdVPq+X6m4MUZMDtqoWVvp6awUqrqs9IVIxpqEkOhCIQT9/+IFFwwuaJTJ0O0Bq7S7wz3GOKzxnE4Xnp6PPD49c3h8IU6ReZpJS+Lh5cjDY6UYm5qX7szqeuy9uvJ2wQOZrrNsBo91ELwONpwrdaqdsS7jpeCNUnKN1an32gFSrpN8aYBDbQwqAJBEpSANKGCFCvTva2FjdELSfh9tOOdF0cMuaBHUvrbZ6K1U+bZxFdS7QTIYUcTWGkQSwWpCRdNj3t7fsNn0/PjHhjTDOGfO08IyZ87nmctl5nwemcYLsRSGTY/voNtsEONYlhlTtMEczydySaiXoFFjOmtV85PrIWeqUVtJmGy1CcuWGAOn08zz85mP7w+MU+H+1Z7Xr3uGIePMwiYUjGScKVgUINBeQU1GjbdIVA2cmjBqpJd3DhsMJTbOnkWcq+hOhlS/kZU6yTTXXPU1ZqtNrnTqYUzBtA1zRfHrn+vJSEPBqSCNiBosAbS8cCqK39bSp5m87XNudO/2khpyYaQmE9SCIy+JeUqcT4k5wRQTL8eJnCO7Hfhg6QY1bw3DjHeOvtcJwbIsLMuMc4V+09OFmcskvFwSyW/B9Hz3AOd/9htKXEjzzJc/fs3LYca6BSQxnkasBILPOO91g42JUiLeG7x1kASTHJfTiPUWu9dmzovjdthw16sR1v3tQBh2dP0GYyze6fT9cpk4nWeWmCk4slhEHNY05FlwXmVHDsNm2OAIfPObd+rPEAJLVvQ2FlENZ4aSM05UxmNyYT6e+Od/+1dkge3Naz1UpdLkjO4rVmrEZHWHb8VniZFpHrHOrZF3iCGJysrU6Vy12mVZGHzAb7fYOWluddbIqfUQMJXpUh3STEU2rgWXmvzUzJpV/2cqgr7uM9bgjCOlyLzktXkvOVfD0QqafNLktwikdniprEjZbyJCjBHvw3poGmPouq4ekGVF9JtpWylqlmXEYPpOIztzWs0fSdd9sV1GhKWCClR6YpNqxJyVflu/t1AZG1RwovmUFGUpNHMnU/W3baeFjHP1GK1gR2Nb/P763btaiohSr+UqXayyIowC363Ab6BDygo2Nu26M9qckJs56nWAoKBCNbSt651SaiKT8hdEKv67ntkWi8FKrokVkKkxm6KUXep/U2ZA5WTZOHL+VNed6p6jNRRWmy0jVbJWKd+5CMl7Sqc+IykulJxIaWacLhyOkY+HSCkHvBXu9h2v7rdsN46bnef2ZqAfhC4Ygo+Ebtbpu3N4H2p6havsSwX51Ugy1ftS8C6SQ1RQwDgUDO9J5p44F86HkY8fnnj95jXn88TL84nnpxNPjyc+vDtgvcE4uL3dcXO74eZmYHfTsd86upDZbDziCxYFH4PRgUBKUhmwRWuDNpWu/1DWE/YKLGm1RpaaBtAm/VV+2ZpjaIBSrnr6msKFgeKI0XJ8mfnw/pGcF+5udzhTEFsYtnuNjDSClKgU6jptKobV40hrEjUFx1mMU6ZxkohxlQfoBHI1qkWbXqqxm85hMpisvjoAlW151cRf6/I1EQ2tCZqzgKn7rh47ZV3vXJe11jDVTLKRJNo4rFSAYh2mSPXyQq4yWRqgUevXCm601CCksfkquL96OdTdu6iHTmOlUWz1ASpMOTNFYZlhORUODzPmcqL/qsN3ytC7nBesXei7XpOXsjIeS4bQdQp+GNQTqaADJmMoxWK9I4QdLy8v/PyXf0fXC//T/9kf8qd/vsP2KqnQXlpvTvOR1vaopotdy4SVVTfPM3NJ7IZe2XcUYhG+eX/kn//Vr3kcYXf7hojh4XAhC+yNIXYdqVtUBpL1Hjtn6EMgOMeuC3Te09tbnAvc3t7poC/OvLp7hfcdl3lhXrJO8GMCY8l1+FlKJlWvKyuFzjt2uw2n4wvPx6g1j7Fg1OS8yWNKrVvralj/v9H7+2FDjpGX5QmsykWXZanMGoexlhhV1jXHBSewFJiLYELPzevP6eaFmBZiXrDkanbbYW2Afovb3mH7DSYEcjbMOXGukeH6uFzNpymFUFO6cim4rJMpa5Xd0+SkMevnlZalrsn6zFgFsa0NpJQYx2n9GU2mIEbZCs6pb5VUo8acIrnAPC11/Zi1/9AaxlUwRJS10B4GIJUru1XZIqxKhbUeM0b3gvJv1lv/364fDDK44TU5Z8Y4cTrOfDwcMOq9jDXQBYcPjuDtOk3cDpbd4NU0KDi6vqMPTl2I7RFbo3SMEeba7KnOoygoEYJq042KpEybBJjI1dSw6OBeCqZqmZwrGKdHsS0L3SbSd5a+t9y92vPmiy3n0y3LeSQtkbgobW68jJxOI+fzzHlMqqubF06Hkenjs0a1iSL/lMLQd9zsN+w2ga4zDMESeqNSEi+4LjHsLP3WsSwJFwRI9L2Q6wFvpDQ2lMogtG7VDYnC5fmjFhdOt4trM1qR3AS2FJw4LBaybtqlyDVVAOqmbljtb4tqCEHIttHjGkIs18ZBn+31+xinEZZiDM47dcQulh1BoXnjAau07JhJywR5xPeZYnKdyLqKJEOxECSryVXV7pVzwVh1Ux2nkRjzigYWwBpHLpbDS+Tp8cLD44nzeWS/3/Hlj2/Y7wzeJZypjAlpzZPeuGZShbM4XydSxa6glpSkRnFWp/PJKPgiplK1jGDqJNYYuxZ4rfAsXIED6iSeBhCt++W1GWyQD4qlaPFRAYZS6sGc1azKVISxGYMqs8YgzYzx+qldD3T0kC11RCCixmBxycxTZB5nzueZac7EZPjwlPjlb55JGYxVrb51Cgh2fUfOBeuq9Knopu6DwfkLzo68vGQOJ0N0N4g3zJIZc8CazCInHl4i/+yffYNzC0gmLRFTLMF7pX+GWtDniA++gk1qODlOM+Lg9s4jZKbR4WTL69sNw86x4LChB+sZx8TxcGaKmSh6LGeja1MNm5Re14wSldarTcRxmvmv/8VfMtzeMOFxq5twJOfMtEzVKEubeMkFWzJf3d/yxeaPGJeZ756PeNFn3NTDx2RFu1ujIS2vU9Q08i//6q80JaLrSKIFmzYbTV6hUaaSEk4EnKO3AeM9ZlyYy7w29pSCOGVPqFEmOFFDxpiUZQCQloU5V4ZCdREOztUJDJXtsNToTf0aY5Rx5Cs7QjETXevTNK2QZs65fq4OEaX8NrQfPvF5MFL1pgp+rFGbcv09U8GJdlnrsEafm5TUsbpduah2uk3KjNT4VSDZdNXD56rOrvbjYmBeImrIJFdwqDJvmj8OfAKuNGCk7on//xzAv7/+7bpWgKhwXadVCiMoTdtaRxFLKgIxI041yMUoCFEQYgH7SXPamAqf4k9FRFlQjXVTwb9CPYjXg7fUekBIFHypzUWNMjOtr1TUA8keYwMZx5KVKk2xymQoevYJaoAdc1Qw16o8Q6rPlAoaBPFWJQ6+Rf5tGKeBVKfH0zQzjSPvnifeP54QSWx6x2evdtzdDOy2PbtNYDN4umAJzigbdtB6MAwBGzyUQu58lYnYev4lnEnr1FKMwXXK2ixBWGLi5s7z5u0XGOMYx5mnpyMfvn/kcDhxeJl4eTnz+HDhu2+PiAjDxnOz77m9Cdzc9PS7wGabcB42AwQLx5cj85hUUVGLokaxR9THwNTBwnWx1Ml83bNbs3GNNaj9PwVK0gSRUmWPxeiwa5yZx5nnxyMf3z9ostowcH+3Y9gGbGc1nYpEiRrfqevUrg20rIxKHT5Z6+rQSz3BxNYoa1vZDtTXWZGTZrewNuWNzcVVItkc8anTUlPlZ4gCscW0evy6fsv6vduwrIH7lXtb/RQa60Fak1kbnKuZpKygb2nnYpF6HuXVu6d5UhVTqmF4PcuaTKM+4K1KK6mwzIUYhfMlchwXjnLgeByZ58whw3/7syfuNjPj4tkOltA5TueI85lh4+k3jgVl3IgR9rteJcNWyDER40zfD8psLT2xBI4jFNfR3zhcKDydI+8fEvJy5suvBm53mnZjUGaSRYcHaqZMlSahvktF11ZczmoqOriKc1nISdl6Tk0Hs6hsKTv1EwghYAv0wWttXBlUXQhsNht2/YabYUsIPdOSyFhu7u8pKfPy9MTxdMbYWen3Ygm+YykzJRVyzGRnVqDyZrdFgDkXvv3wxPMl021uyb6rvUHtR8qVUdkQqDZwaHRdKXC732Gt5Xh60cFArmvBWYz17Pc3FKNRvIhBktYJLjhMyRjf4zLMcamsL/VJMLYD6yn9BtyGmI2GEFitf0r6pD4planpnDKJpC02BaopbdCke0b6hL3tvf+tMyjGhZK010w5Eaux9bJUU23RNJz2/Bin5ozGwFIMkgpd19d6TF/bskQQyEajv40oaOiM0X5WTJXQCTFWI0vQ95E1VUuoe56R+l5/WFT3DwYZ7nZbSsnE3NcIsExMegNiihxSJJ5VU1QeRqRknBG2nWUIlr6zdF3gdrdlOzj6ztF5T+ctXXC4ar7ovMMYVK8SvEZUBq9NjLWK8kQPojqYbCwmmroAI1Kp5xDRkyliZFbtnNWGPmAo3YRHDfw0ms5Q8oaUN8QIczTMsXCZFqZx5nQeOV9mLnNkvMxczjPjNHN5OPL+UZQB4GqBHizeQr/xbPY9w9CRS6LrVV6y22sEXeisGlE6g3OCtVmN9Co6XFlW5BIp4nSjqVPPtYon46xlsx0IPtTiWRe1NYp6a/Gih0JZwQZ9AJaYEeNosYeqU2u6POoE8Mp9UJ2aUvwxqblHILZSiX09cIpRhUfxpCVS8lypZ/UfKUQgSVZUvR5CoKZtPgSMsQx45ikRa55wyoXjYnl5Gfn++wPny8xm1/HHf/oVu50ldBlvEyXNazNX3yp1bM0aXeUNEqoxXbbXwqEo5dHYZm6qrI4WHaoMAaV8qV+AU1bJaihXOzMpkDNiy2pKtMouTKPOVazhEyhfwQxRWUuD7OvvfQocNGBCv3G9d1RNoWkbXJs3qLZYXYQLcWnmVBNxSZqHjAJA25s7/PPM89ORnIM6Ec/6+syyLjykfmhNQtnMtOYFlgxLNohTr4Y4KY0/xcD5KTP/5QErSVkaSamS3nlFS62irM7IynYqa+FQ8MHiwgtGLCk7xsvAXAyxCOOSmcaZOc0av1SELKrxyyLXIl704DaVXdAMbakgTS5wWQppBIKuDS9oVF1W/RopksVASpiUsHniH/7Jj/nHf/ojHg5H/k//yf+dYMraZAh6iG+HLYfjkXFaVKMrhlQyyzIzLQuxgMmeRVRaJEZRcGLSvzte2IbAzXbDu7gQUS248/VgTqnqFCNEndgXa6FkYlbKrXNupfk756EevLkUpdVW/TK1Caei7A3RtlYbAamysDXOqCgjYD1MY2QuBee1GHbuCvaVUlRL6B0ms5o5Lsuy+i20n9ca9+kyqXFRm3BQKoCh004x1ESTZkxZqYCo/t0Ysx6eaoCUWUgKNFYwMHRuBVP0Nc7X4rVAKvMKMKySEdWVaNpF+r3x4+/s9WnjWJpBqq1eHfUgaf4zpe5LsTadFUQwUinXtYXRLbz+X6mYojQ4Q5subd5M9X1QSRilWbpegUmDshJzKWSD+geUTJEKJmDBeGwOFOso3pIlQJpIeULKAkQMFkfW4lJEJ99Ulty1R8NQ1KxONIFLf68joQ3tZvDMcyDFRIoL87JwnGYOvz7j7JkuGHaD5/5mw3br2fWO7cbTD46+C4TeEHpL13V0ncd6ZTuFbqlAs07PrLVquGrUw0vN9S4ULiAnfHD4ANvbga9/vCGmzOUUeXg88/Jw5un5wsvTmdNx4nCYeXgYyTwQOs+w6QidZX+zYb8dEGaCzdyMemZ1zlY5abUwT2ltEOpHqI20qfUBaK1QXQYlqUPACv9nkFSQKMQls8wL8xRZ5oI1nlevd9zdD/gu0PcdrncaAymZnBf1jZJUaxEL1RRQjAK4pk7LxQjiTK1b1Jy06lSuxso2Y1JR1k597TUs/Poc1Pe4ejq1gqrVQ6aBKLV+oYJCaF2S5ZMFVevF3xoS1h/TakyV71KNNus5pDEttYZthay0OY6+59xYm1dgp+Q6YBGtjtTdSYc3ZJiiMM+JHBPjZSbOmXFeGFPhQFk9GiZx/LffLuxD4FfvR3ZdYdPpueNCJviZ0BdCH7FGEzOGzZmut3jvAJ1CD5uEcxZnDJcxc1g8hxQZs2Dnws9/deLxcMFwZpo2/MXdgJhFDYxR4Mv7QEGZlGlZlI2bqsQnF7a9U4BMFm2Gi/ZCu43jq89vuEt7Xi6FOWduOo/3HTvrKRQ2uz3O9fo8BsfQbxiGLcEGHbSI5el44TiOnE4H2pR7XiKOysSq8lEDIApppiVyeHmpXYXDOctuuwPnmE2PiKeIXfEfJaJI9fqqQJDoYCblqJZ+FZA6Hg846zDOUlIzcPZa14ghdIa9CSQRxmVWf7LOaeogBt9vGLOBOTNHTdwSY0nWK3jsOxKWEhPFRAqatqYDjOZjoBIIrb+UAT6Noxrg11rCOt3nrLWf1E/ovbJGXy+a0BVT1D1DtFZrX9O8HkIIyhSF+mdV6lWgWD1LmrK1lMwiVg3oEYRErClDOPUZUpZCZSyUQqm+WKuUo9ZiLaFNJUb/A4MMnVcKnc1XbVrOqlWNRXWrsZQVgEhxIcbEMUZepoXyHBEmvL/QOaHzQmdVirDf9vReGDplQfigPgkuGMSoMeMwhNqMW3zwSmsU6PqB4Duss+SyVK15pLjm7ZPwovpGlaDpYWVtAr/oIVaUdivFEDDQV10Thpw7cumIaUdKsCRFKqe5cBkXYsykBKfjyMvhqJT/XDhcLjwcTvDdSwVR1RgoeKuyEm8JnaXfBIbB0fdeDSn7gHNKx+46T0yZXCJphm5QPZGp3y8jGn3iDPvdFu9roVAK83Sh5Bmsr2dEqRFrdWIntm58GQIr4kxFsVcUC0WlqQ95NvpLi6UaYYpGABVbQFItovQQMSVByTqlXM+trA1qM02sDfdKBRPdiObZcD4tPH+ceHm8ULI2yUsuTMsMBn7yh6+5e9Wx21hMWRAToURiXrSRkKwMi4qwU70clEqlv3COkh3FNTRCKEskW1slPFfKkaYh1Mm0ZNRnozbebZph0OKiTkBWvZ9Uz4Q6BdWNJq9fuspX1gO9MROom8M1MhHq39ebvRrkSGm0XKWo5SLkrHKdec5MY2JZYB4T85IpWaNxUtGiAxc4PJ9ZSsdMJleGijqo609dX4FcX19zy0UMiykkEVKNCsM0o7xqjCmGMXZ4C+qPoVThBUtz+ycqN6RJQpTiX9aN2XoFH5alcJlmpqQ6wmwsRVRu0Lw5EVs/GnMFxbnGZTXnXTHVBb6BZNajBmVFoxUrMAHaSM+LxhYaMp6CTzPZLLy677m5Hxh6g6Guf/TzccawGTou4wUmLcyKQCyFmcKsbx2b1btCJRaaPJFLoXPCfHjmz/7s77ENQrpEpeiW+lxXGYy1TsGGmHQN52Wd6ku+NttijCZKUPA+VPooq1tyrjHFn7IKGssAPqEJUotMlOlkpOnC9LKVVWCNmhC1QzbWhBZtziOaL6/ePTHq/zfAYZ5mck5rgoQmY6zk9fqMKeBZUqn64St41fKdGwjb3J5zjZ0sooAu9e+sSRNVy5hyrBMMvX/WXfX7trKm5mmucqbfX7+Ll7V+LWhpjL7ayOmlk2Jbi7MGHOSUkCwK9tWpUtFeS8Hi1kvJ9d9tT9JnsnqpfAKC6VmkUcKNLZSLAtWpvkhbQYKmZY/UibZkCNqtZWm69woKKzpRfffqGWXq+WE1LUyjCdVhfEm6h2GqOSI1KtMKxViCEVKGkjdEYJ4Ty7ywxIU5RT48z3x8fsGZRPCw6R37bcdu17MZPMPg2W17Om8JXmuh7WbAB6sJDr1HNgVngr6MnCFlTJowecTkM5I9RbL66jiDC4ahN+xuN8Svt8RJ9fSnw8LLYeHheeHnv/iWb777QH6cKdZg3IFN7xlc4dVNj3M3vH3V88o5fCgUrhR2ZShWs8GSK3CtdTCSdS2UoqbOGEoScqwAa7bMl4XzcSbOCTEW7/z/m70/a7YlSbL0sE/NzN33dM65Y0wZkZldWdWNbqAbDbCFICnCFwof+ED+UD7wN5ACgsADmwTYRHVXdeVQGZEZcecz7cndzUz5oGq+TxRJdECED0yR3Ck3494z7MHdTE116dK12G56+764YGIMSFJiqpRqtnnWQbDRS4mXnEBcMwQtxCA+1tnWcaAEB9mdlVnF11hzIhNc0V+XJmwTLMTzFssbbV/QRiG02JpxTMXAW1vjzf0H2qjF0mK6bLiAaTC1sQo8/TMv0QUgCAvrwP+/gWEAno9Z+8j1UMB0t9Rcz/KsrjkVmOdsQFWpjGcDdyrCPGdqNt0KwfKzX3z9FR/uvoOu5zF35LDieFDWx0IfcY2lYizqOFmjMIiPKp4NVI+BFE1YU+KBrutNR64GPp16jiocy0QvHZIT821hiJHff1u4ejnSpclSWI3UArudrZPUwek8stkMdMEYMYa/JObJ8rrVqoOaEE189vw5v/o6cX/s+P7dPWMNbNI1Q79ltdrQ9StiTHTd4BpLlk8gHaUo52lmmkamnE1EMGcXG7RGgA1e2k2NAsnvmUjkNJ6YS+XFi+d898fvGVZr/vL5K0LqKbFn0pbnNZfC6GQZi09laSxYzkptuWBgf9hTaialnlkLJTeLcGcuxAQ1U1wPRJLQVyGqsSqGq+d8OhXqEKnjvBTUJUYDGVKH9AMaowPAmXq2RkqtlcmbDkt+heXFuWTGaXLrYHN/GXobFUsxmZFCMlA1hMUDzGOI6Z+gLLVCy4uWpotf35wzIpCzRXRVXUQZVTFr9tWakKKJeueZ6IzSlhCXWrwpIz5eanur7XtcYLVlNlUtH/spj58MMuiT/y1xJtq/3GzPEnQHHGodKMXEyEotPnNvVofHUjkcJ6iFWs/EcCBKpe8C61VPl4I7QST6GIlJeX69o0+BoQts11agxKCs1wN939GliEoh9onYRcL1xDQ3m49I0YzUzmhjqcOsYkwl1pqRZUksJUCS+qPOQ1IDD1YqiJjaaikDSKTvB8Yp83g4E2NHCB13tw/c395zcvG38WzvZxoznz4dF3QoJFOUT87oWA89XWdUwe12Q98FiMowJHZXkDohJCUlF2zKRnmPIVKmGUkRDb5gakWq0YYbolaf6F3EkEjZBT4Wev9lHKMVZA00bl+7/GRAw0VLwGbG1KnKNlKgLgKkNNqaHR7mlqCmruq0RKmRPBXOR+FQlLvHE48PB6ZDpmSbNxrnE8Om5/XnNzx/seLqKjL0ytAF5lmhVGf/te4QtLbsonKs6h1sWxvSkP/oDI9aFmqfdQSSA/Hh0rVsndNGH0Iu18jHKmzxBJvf92KbhrjLk4MaYOmU0OAEFFMd93iyjHni19tikmWdgWCdtDYmgyV+eS6Mp0wZK+dzZpqUUoS5CLnaZ7LCLTOXysvrl7x583vujwrSI6Ffrh8iS3d4IRrKBXaoHowaCCXS1CWcLoksdM6iyZgc+GxqiGQv7lscQasrods6s2CtLkrlnXIVzgozUCVQ1Nb6UiiIC7B6oiR+IcUL8oAzGdx+NIRgwpRECP3CCDHNE3FxP9d910L2ojd1kVmV797+wEn/I15/9hnR3UJKmdHgKsunM/P0nrlYQC9qhfrs4NlcDRS7MO5bt0ZJUonTgeerwr/6Zz/n18ez0WwFgiTEx880+IgELu7TgF8vusEO8wtoIyT34O5CXBx0DJSsF5ABYz0EB2vwWL/c7CdjAk20K6oSnI1SyoxPnJGzjT/MpZBUUZ+FzzkviH0Qi8tVlTzPpuosQuqc8RLSco0UPL5lp/XK8r4BZyIsh9mS5la1uUU8VgSJVjDKZa63UXMlOlAYLQYEBzkaeyPnQnmiS/Hnx5/eQ0NHs0huMa+5srR/m4tEGzFsiXRY1oo5NplF4DIGuTBvnrwWl4PVoXpPVD1NbYwpB7gXFkR7nhbkvJvspzdVsPfgZ65GB+UQVE1t3JXtqApFM3P13M5fTptejzusCAKlIg6qBg2WQ3nBnYKJYHaYhs7c9eSq5JyZ54lSM1pGjmVi/5B5e/9AHx/oUmSzGtjt1gxdYrNKPL/acX01s1pFUq+sN5Fh0zGsBj9zDSRmOiNlROczpAJixUdM5m5mzA8MuEgR2QqvXm4o2nOe11w923IcZ958OpJLR62R/VQJZebTfWYq7/mLb14QuxdsNoUYoOt97+PxTls2ZNcBBzBFMWFw6ZjPyv3tifPhZEVGipQ8UbJ1tXe7KxMVFxe1FqdjB793wbrYwfjMPjoQPIfC778a8hRAGlusEQ4CFoNDcU0ZWys22rBICNrPuwjiIt7Y1r8FfFvQwCIqXGVxDFhGFFwrrAFmxiIzB7basGe1VU71BkLbHd6wsXRLfLXbem//f3kXdn5XsUsvFWsoeaMvzzaeN42FeXJLvqKMYzZR02oEvmKdLmucWtaFSGQ9JPoQ6eJA6ly4PiVrCjQdLeyzBJROhblYDrqwjcdLTh3FAT0KIqM1LHNi1kSOgxXjOZDdZenNx8LpX/+BGGdSF+jTQJDIMOzpusB6lZjnI7urDUMXvSNu4uDjNLFeB3Y7KxpLicRwxW4A1Uh91VGkQ0NH15lOmkqyeFDtLD3PmTxPZB3t2qgzYiWgwSzoY3QgXjNUa/wGhC6Yy10BiIlchdM0M9fC3cMDm6IQI6HvjRnmLGYrB5SgleojMFZTWi2xWg0M3Ybz8ehntwOu0vI4Z+UQXBQeclVmVbInAF2AVVW2sfDNZy84sOGhFJJ29NFyJlu1GLs12ShctSTDcykWZnTOGZxhAObeICmQuo7gItil2M8so5Ri1zg7U6CUsrBqzS0uuBafAdjNkQIuWloKTLOBIjGGZQRWxK1Vo9nBjvOMaiX1xlapOXtOY/l8cvaYavW8EBM2lo44+JttIDk+rj4ag+anPH4yyDATHVUSGy9ox107N9UKhtCQUCBER0UIKD2q2JxxBdS6gFOeKCWTS2Eulf1BTThFTexCXOl3tzrQR2HoAtebFasE6yHy7GrDZtOZA0QSYoJh6JkOmWkejebc25xM3/ekoWe13TEej6hWYgjEGFzZPC2K58EtGe09GIgcautaKVUzgtmoDatglKmVCez0fcezqy2HF5EyN79RU3Ufx8J5mjmNE9N5NuBhMgu4OVeOd0fynNEyI+HBxicE843frek6QzDXq57NZiCKMp4zqVM+ftrTdx0hKrVM1JrRIZh4YbWAO+fitJpKnmc7IPBuusAybuLUT2MXmNeykf6CB31xqrfff7FA20YCBEE04Fw2W9CoC6lVLzCFWgI1C/NsBfH97YnbuzN1nmz+dIh89vmO1coUt3Md6b37sVoJIXpx7qwIS+js0LA4f9EpWESGsIJzoRk5ahZCdGFI7xQIJqoZgiupumAigil5W2fI9kXbBC3hcOsdqYTogJX4Qd7aTotgkRdt0jB+m+NbDiv/8eUmseQDFriMdkMtgmaYx8qcDaG3rlJBs5Azdr3VAvB5LozTxOP+HvURka9+/isej0fmujLRqBiW93CBQP4h6mTX1/QDBKReOhv+M9qCRPvccumStA74hS1iq6wqPxLoqxIo4vY/2YEJNcS3Ki6I1hITFiS+daSteGjfD8uaCJLskI0BmhWXJKqEZcyiejFhlo6ZEIQhRo6Pj1CV3eqGIpG//+EN3338yGe//AWp6yh5psYKnV2BuSrTXMxPOUSmYuDOVApzKUyl0MXOWBjFwABUibWQ6si6PvA//Y9/xi8/3/Drv31PnQJZDMiKGhdb4RoSihJrJOe8aBkslNbgHctarSuB0fzU0fiGaocYCV6Va62OLVTroDZqdyvm2/18st9acqxqwpLz7ErRIThYIdS5MtK0Gnz3+BliMcMU1IlCdGYW1fRQFsTeQZPq70+wAgmFOc8EsQ5W01dY3oNyAQvErk9ZEmouaxIW4DA4y6M5bTSbz2kcSZ1RGf/8+NN8qIOQVnALIh7/HJRs3VrxIlMk2JhgjG5nJi4Q6rlRA99EGi7XXsniqVaCggZj6TV2xLJHNTguLx5hL+OMte1LezqnhHssDWJnuR0NVCy2UZJpLFSPifZiFPWfKl5MVKVg7FQzLwrU2RkN1RgDSSDFsBRQcYFHlBg8EidBQofSodp5gp7JznQdc+HxofL+8QhUOoHd+oGrTc/VVcduG7naRVbryGbVuRuUzXxrVWrJzKcjQgJ3zwhDR6qJ7PPcMRrt3c6kTAiZqyHxzTfP+OHdV+ynHzjOcJwhyEAJA4/zzG+/f+Q8VZTI6xeB59eRrbhNcGisUL8B/tdQ7DwcTzOnw0yfNhweTnx4d8/j/QNXuy0vnl8zrDp212tS5+dS0KU7KMFsgGOIy7nTAHNxgFxx8U6PWSEIRcTHONob8rPWRy2DGPhsa8qbJ1WpwVhhZsVgOZw6E+dHAvKe14WW4DeLB2k6DOI4i9CEUu2dVHPkCs7ydFAB3x8aMGcMaXnBhY255AtLMmq5oyEvglZBq7lG5QxTreTZRhPyXJgma+yVYkVVrpZT5KKoJHIRGx/VSs4z03SkIxNi4Isvv+Tjx7+j73qzY4yRISU6MQ2U5dI4y7IsCc+F3deum6q5kVkosSIwqzJpIasBMwUTJNSqHGdhpuP0YfZMq5Li7COmI0GULkVCqHT95IW+jzEGK2BXq0w3TCR3A7u9g8yWbui5DsKk0a+HMGfTZxmzkkt1Fqh6c8U+RG15XDCgvTb6fRSkWHxLEpGQ3KIyMKnpYFWEH9684cOHt8xTZr3dEaILv0aLq+2SGWfI9Kq0ncsBYohcbTdc77a8ry6IKA7wNh6AmIONijKXSkGYa2UqmaKFLmd6RoZ85JdfXvPNqy3//kMmpJ4ggzVxa3FHKfM80WiNG235SrX1IsXYu0uBji6Ags7LLiTFSNd1P8qR4AIWzPNsTZVgQvTVc5O+65eGS2sUtZy5NYxKKUvuo2paD4I3QzDgr2gxMczT5PW4XTPTQzS2bC5WjzfXi3maKLXQxaY5Z80daLlhpSxer//Dj58MMtS4dhpGNsRYbVbP0NTGj4LamAvNJakd0L5Ug4CkgGB2OTEZFd3YZdVvkhfmfsNynnk4R7RmqCN9nBDNdBGutys2q0jfCas+0Udhs1pxtTugFPo+MvTCMARDwfqe1ebIOB3pkrElUkyM5zOpS0ixN973vb3vaAsrhkDxIjGE6oh/xTT/CiKZKBWJdkiEODEMGTovzoN5aVenb5VayNWLoyKO+qvpPhzPnE8jx+PI6TRyPk5UDewfC+N0Js+TFTl9YuiEZ893IDNv3n7LdrNj6CMSrJhfrwZW2xXRLT2Pp8o4ATJDPZOSH5aNKreg8pbgBBLn8Whd2RQWoSkTk/JkXCwILsi++GIAjMIVQKMVTMEOBWxsi3EMPNxn7u/PHPaZ6VSIIXG969leD2w30ezIAwgBDSsk2mHVdT6b6sG7usCjeOBpDIsGKhM8BasYiNCQfvGCI0RCdaDMD2fDA4IXpQFos1QVpFyofQvu4gUWYckoJYYnHQJZDlcRUKdpt320IOA49b1AQ/cdugG9WDHlsVBmRTOUCfKkTOdCnpWaDXgo2KFW1LQSTlPm4ZR5d/vA/f6R/f6O3abj+c2OrIHi1NwlgUa5QA1P7m0jzz4BGbKPWmpo2S/+PQN7orjwZjtR5AIKNCBiefZgwTQ3tdsQSCEwl0pVCA6WaLgg6KElVv774olXuy/LHxd+xFF5JEG4HCYami2sdSYrNnOHVj59/MBqtWJ3dc3h4R6AV8+fMapwyMq/+bvf8/O/+o8I/Zo5F0ODxYoDVHye2JOMan+KQlYzLEmti44BfkEzIR9Z10f++V+84F/9J98QOFLziLCxgrpkFxOrPosodL1159phV124sf29OMhgRYoa+p1a+WMHqPmk27puSHqtdogHR9zt123vx+UgtoOtqrHZYoxGHV4Aal1YQdpokN6qLWI6MzH6SEJTNQ5CKVbgkE30rIkfLWBErRQvxtqqqqU4S8nGyyT6wqy+Dxfw6bJeVW28rTZhPnnS3XYGyNLpC2a3W1L2rf30uf78+FN6qBdGIB4/Gn0VrI9nTCmJzmpLJgjbbIkvAMOSMfP0L8uX9QI4qLaRpPoEMWgMQd9f7dhQp4F7V7SomoByvQBz2Rs9TUCxRi/9JRrDAisAgwbXRxFCrTZu4O+t4syqYudrRM3lwOOsvQ+8GLBcIxbb/80mTbAcMUW7NjZ6lqzh1FfPf5zGns0KdpwmTg8z7++P9B+U9SBs1h2rLnLtIpLbVWKz6VkPg9ke655hHYmhEhKm9dBHA1ZiIA4Vib1ruVRC7BEZSGKNyi++eE2h44e3n3jYT2Q1bYRjEf7w4cRc3/Dzz9f88mfXfB56hmROP8kZmM3aQTRRgDzO7B/PHB/PrNc2lnV1tWG1Sqz6nuubLX0vdJ13gO1IJ4+Z+/sHUkpc3yRi1y3H1SLYGeqycBaQHgfDQsAm63w0VHykwplaTYfB/uGsiBjNvjO0pojHNXUXrqYV5SKVbbxPPF+3AlAurAm30lzo4634bvlOi51geVUAlgGHSxReBnPUugcGfJlldpmN1ailUGYos5h1+mxjcjlXZ+7an1wacC6UKkwlMxU4z5lxUg7jyHEcmcvEND7yfNez2XTEPnAej6R0TUjRGNauzZSexPgGOMmy71vTqDFA7FGfrBXjMxi4sNimigE+BRgroJFYO0RMj6wW10/x4BGclCQnu9c1N6tHQYILEIZKkImqI+NUzCGLQtXIrNbYnZWlQWRGKHYvtMUivyMtp1YxB5qc7fyP0cZRI6YfIEQvyIONqgj0Af7pL3/Oduj4/s07xuq6SR5vWyOJaplibXmh2OcxgCwwjSOPzrTEATh7z6Y5ZiCtLOOyxZsoWgqxzPR1Yq1Hvr4Z+Od/8Rk5zpQyUTANG8EYaM1ca1al+Oc25o/HzGwC4BGMreBnRGN/zrMJZZdiIorJc9QUE6Vm0zpYruyT3FfaaJzQp45uNdC1scxal0K/OXfFGDFDMcvVQw2XnKjmJb9JKUGuBix4A11E6DrXlhhHE7SVcBHjzlZzLAyKbK9THVCZp/8fj0vktEVLQWtGQ/WyoWJOstUDZXG6jSM9fgEXapRYgRef2MgkdFHrxEtc81jHdbZ6T+7DorSJmh/omDPQNp4XAAEAAElEQVS3J0zzoanra2WVEkOXUCpdFxh6YbNJXG3WDCkyJEO3t5vB5pmSdaW7LjB2I0Kl603MI7rjRdebj6ukhDbRDkf5tWYq2QO1GugQiukDuOKuicE4RcUXlI1w2sHfuha1rMllS86VacrMU2EcC9BRivDw8MjDw57TcXRBxYrScz7PvHt3T5AjEiJztrGIfuhYbTauNgrv7pR3nzK7DXShMPRekGOFvL01K6bBfGvVEWP/Jguw8KSI9NtsxIVWJ6ugNVGzFb+PD3soM13oqVXIc2H/eOL+4cA0ZVargVefXbG76tmuIzFhwI2WBQjQaJtG1YERtUBmzIEIwYoPnH7Vgv2C5mNFVBMjiq4abQr80T66BxLzlw3eZfDo2ujzIiYkKkZLKqpPaLa2klXqk0v09HB/0mVwgEedfhf8CUIVFiGj2uawoBY1RspsM4XzuVBnH0XKQnbGglazSapVmAqcxsz+PPE4zjwcJ97fH/jjuw+WHNfM6zDQX6348DAx02H2aK5B4cX7oha9rANLPNS7YYIlLlUs8axePIN1vQBPOp1dcFk6tGewg6VRNBWlgR7i9MvgaskGPlQJFxEuecJgkUuxtxSRDWhoStUhmBjpMjaTrHMkYdmn7V3U6kretbC/+0T/8iWRYuypmDBruIp0O/7db/7A7r/6v7OfKvHxwPVwzbAKyyFuM87GyigOMhEFqclsk1KyRK1WpEzEvOc6nvinv7jif/mffsPLrfDu4YHpPILu3C2lunikscJstrJpuJh95Fyr0/rzBfBqCeuPYjWXqmb5RovR9u8FPAjNeeGiIZKd3hc80SylOGMsXYAfBxga6o53YppeiWnuiLMtGlVQFrS9FvvTmBjqlMrqlqCIAVqi8uRAbaMQth+rz1JLdRqxsDxnVUtXmu/0MjrlayqXQnSQpTlfLDoWfxZ+/JN9hEb1xpe+iGNJbT/oAiaINEHCQIYnBd+S+fyDo/JJZ1CFRndXLa4X4merOuDsoNtCF2+HiXebm4ZSVk92fQa9JcTBzCkt2W8JsooLHAuBRKyRoJEqxft2TWWpMGNGl/pE9FD8fYmouTTnghbrcaZQ6FNicAEz9bxQ+AcjmA54dOqN76QL6FLyYMJneWYuIw9j5v5cEC30MtF1sO4iz64HvvriFdtVz/T2lt0msR6Evo+sVrOLakdiF6j9ZDbIogZg9ht02EBJzOeZ1A28uH7G8+stv/3t73l/u6eWhIbEVJW3n2ZOp4nTuVB04OVV5FoCfQlIVmOZiOk6HfZnxvNMionNekXf8qvtGmFFjGLsBcHtLUGIzOeJxw+PnI82Dqo31ZiZrS59Mq5jC9NzE1+XjRmg1vI1TnjrMDecwNmeNsJpeYzpOrQzN9CErZduqI+6QXBWQ2h6lhafBcutfJE/ZfG054l+5oraCJ85BwRPJY1tp1U8X7S8AYXiqvpahVqKddgnc4MrFTQrJVveU6pQnUViWlTBR4GEucBcKqdpZhxnZjWBvx/ef6KQuH3YM+bCaoiInhlWazrpOc2WSKZo1serlOhiIIksIMNS0Inngi03Xc40aR+zQev2ebSBedCcPFSWfjyokBxYagmtaLwwEFWdVWzX2eqQQs6uJZSCsWrq5bw6TzAVa5pgalFLk6uBHk14MSwR7AJgtRxKvW6LDgXFYAL2obq7iH+eGBKh2N4dpPI/+c/+Ob/6+nP+9X/33/N//n/8NepNjxgTu+2GeTQx/cYCbhoH7VFr5XQ+czqfaQKFpcI4m511FzrmXJlUydLG90FKpSsTXT5xHWd+9XLNv/zVz/ji2YrfvH808U8RSmgiqp6JBiEu7hbG7GqDPSG4s9XCLjAdq0p12ZTi43K+dlWfjIFaE2vRtdJqeUlowI7dhDnP6FlNG+IJMGc2sUqerAFf3BazaUQYc9cEqKtrniUfB05dt7AqmuZV+90QW+PH9v5qtQJ1MEExcXTPp2KMPlbyH378ZJBhTBujuUuxC6JOJtHsRUUFis/DCLM2lc6WyLobcBU7FJ8cPlbgWnfaaxJTyDSirs+OgxIx28WwoJTy5IbM2ZCjkwpzCUzjmVwnwHx4u3QgYXoLKQjXuxVX294Q5s7oeJuVj15EYxd2nfkMD13ysYvk1iiWXISUyHNHlUrfDxSBWAdqnTDPZ5tviQFjF3jKUb0aNyp9XdB/pNLFStdVVoPT57UjhB6JkfM5Mp43PmMGdYJPn+45nSOvX/8V41Q4HGd+eHvPh08H9h8OFB1BGhoW6P7uPc+3gRfXietd5Hnu2W0jfefsFHCBIAhdT4wdMUIlu0iPdemb9kILRvb/Fj2nMUNJ1Bp5uD9xd/vI3cc78mQzQzlXokRSl9htt3z2qmO7S6QOUgcxFtcZgOk0WjDqO7rYE4J1gS9+jy4EFKJ3g/QyvuAHoGAkWKvh/XBt3SdLxbx4su+3jSdtbKYWHA1zSjYewbDXqI3i54d+0/QIhq5LO1MRNFpheymqn9hzKkg24KAWQw/NXilTciVPmSmbuI1WoRSQrBfkVgNzFeasHE8zd49n7o8zf/x4z6fjmcOUCcOGY668eZhYr1cMqYcx0R0Sf/ftJzRt6FMiJgOY2hWSJ0mNpYqXzNEOvQgKHYGgSo5YoUwr+A1ESTHR3DYsb68uCN2KWE/SPfGRpYhk0VdQsUOyOtjUuofa3psnQcs9oTmbeCwJJohmJrwX6r76+ItPXNvf1RKllAxN364HbnZroiovb65YbbcEEYZh4OXL18h85te//4G02nIuhXUtJISsZrs2zcX8x4PpZrQLISHRdcbWEDWAocuPfLbJ/PN/9JJ/+Zev+OoapvHAw93M8VCRjYkmotalN2VgGzmbZwcT1BwYQjSLTuTCHGkMhOrUt3nOy7Ws2kYC7BIG3y8xRu8Iekc0mJ1TW/tRTEsEfx7BKIZhmRS3BGKeJwQTZowxWtriVE8wtkWeM1OZyZPZFquzwIJ3SZpmREu+DbjwtLgBCf7tFCLFOwUhBGPK5cJczf4YeepkYeutvRewg9WSj7D8fXmvfh0v3aw/P/4UHyIm4imYEFfrIjZ9BVTMMsztiy2O2boJraAwWPLJcz6psFuH1s+iBi6rFpqonSydKDvLxP/ARePGpQb9+BEHzeTJmFEwyUi1Bo0bUqKxzVVHomSogVAjhQySTVQNNb0Zn79G6xMgBOvaqmLOSpbRJFESASR6kt1GPxw4acVKO/VacaxqgtH+tZrMXlnpmfJgjRZvOuV55Hw+83DIPJ4LaVO5nuAPv3/Pqg9cb3uud2s2u96YDqtIn4QuKn0U+iEQOoh9YV6tyOMVQmKaMtSJv/rlZ1x3mV//9nu+f//AVEGkJ9fIp0Ph9O2e0/SWX3655Zc/2xH7QFLlPE1orfRRKGNlFRPr3WAisk+G+Cx2BiQWL+4EJFFnePx45OHjI6lXuigEqUteo8HGZbStIcHWkTgTzJblUgCW4Ay06MC8oa00nQXB2VuuD4XToZ+e7QErVITo8d9dKvBxHRFjg1KhNsFxG7tpVpftEZzJaiQMK4iNieCaO07VriqmMTbNFufnYudSFUo2YTu1us6AhGqsYGMGKxXTSspZOE6Z41h4PE8cp8LD8cTdfs/+dGSz2xBj4PffvWN3fcP94wlUeBE6ehk45oGhbLh9ND2hKJk+YQBDMMc2J51fQIYnDI3lDBAv4JXlvyCog0vVBdAbKdi2TSvSrem45LE4G8n3Ah6O2riTVpid8ZEaW8l/tRaoNTCrMFOtmYm0Vbnkca02WfLlln95A86WniyAlo0WG0hm7mqN0S4sT6IguUIpDFH55c9ecvvhc/4v/+2/sZhSK4NEnl1d8Sh75nn2D3dp2BnL6zJlbNPdas3sWhlzZSyFLlzY4dXzHtFCqCPDvOflMPOPv7rhX/7qCz6/XnM4PHI4nDhOPaM7c3UxLnVoUWfYLzHdXtPqTmeYoSYEneKyDmK04ikEvNmnS16yuFbR9P8uzJ/oY5uKuVRV1yRbHgrjePbRNtsfpZimw6InVi0v63t3sPDc1fJAy21KKT9yuCAEhmEgz8UqU3+PMUa0lOWe12KNrFKqf0Z+0uOnazJ0O1JQCIUqs82tUE3R35RWvKMf7HBqixGlWQdIVbugqkbpwzoHMYJQ0ej0Kd+spc06lTbjZB8uuoiXKuZDS6CWznzcvUiMMTKOAzlnWyzVDvG5Fs6lUM4zn45HYjoZy0CUVRfYDb35OXdC1wU2q57VqmPdB7abnr4ztkLfmc9ziNYZUC30w0BMgdVmvXTArKrqCMOA2afaNYmCUZPVRDgMxmwRwa01F9pvtLnCFJEh06VilEk6ZA6cj5lhEL75+RcownlSvnx74De/+8jff//A+7uR/TmbHZ70/M13n9ikwutna55tI1+82vLl6yuutoFVL0aLTEonyvl8QpyNYcWgRY5GbaaycIsUhWrCgve3Zx7v90znyuPDgTzN9GlLt4rkeUYZub7e8vz5lu0msl4rqavkMhpihnVdyjhz9/GBopnVbsWz4bkfijgNVN0K0lKusKDxHpG824NcNrogi5uAt31oopUX+yO5UAo9OVy0HlqxuswjXg7nViBLsILb1P4NiYbLQSOWYVmRZNWyqcNOBZ0z03mmFvHl4C4g1f5t40T2LJXk2gXqtpFwdzjz5t0d33868MOnPYcS+OHuSO2MxnZzs6VIx7FGKJHUrajdFWdd8eHuxHp7TT8MoMJ4njzReEKB5MeqDKp4EhzMfaaNj6haN+VJHAn43Kh/0awP7bot1EIHB9TnWSREp8FDo20iVvSXXDyI86SD4Mey+iyyZfOG3Kuv1AolYFS41jLC741EP1ydHuzgys1uy4tnVzx8+si6XyES2K02rNcbplyQ9ZqbZy+QeUQErp9vWd+8QLuBc84++1iZciE5uGW1qYMZ4p2gWpHpRFf2fP1M+M//yRf8J7+85vkmE6c951Pm/btHDsctuvZC29WyWwFvhYLSpW5J5hsjRVJaGAAXhFwXB4/UdT4e0UDj1t33fV5bh9Jt0Opy6byrYzPcS0zArCyrlIWKV2b3qg69LxW3Dw1ON/QDMTvzouYMiIMRxfFDu6et4xGiOPjprJMgJEn+/tVV/u2gb7RT6yhVT+Dsg7QRkKejEnCx2BxSIrhVZZCLHoU1G/8MMPwpP+yoMFA4tW7vIgxqsSWXwlwrqRekqHeMnPPk+1n+P6yFBrReulUtDIoncu1h52hL9sTZRQvRsMW35T1ZHLFd39h6XuxoWIoCE2P2UcdW9ASLyTMzECEatbiSjaMqVhFdXHJKK3eWQlgw8eyYwqKS/uNPfgFElq9oY4y4joNYLmi6wBZvkwQigd5n/KkD87Qi50JMAmnFuXZ8uM9M+UzsApvNwHaduN4M3Fz17FY9m1VglQKrIdAlpeszcZWYJHkHvCB15mYbeP6PPuP5rue//5tv+e7tnlPOaOwpRPZz5td/uOfh9oF5egXhBavtjJKRWoiDWXqGoHSpkvNk16udYyKWukSrYhQhaGS/P/Dpwy3kyrDt6CJIKBBqu8tL8WE+Re3LYTmX2/gMDgAU2hhhpGH3bcU14WohEMWK8/Y6F5DeCtWmk9AKUhO8FD+HBdXojQSl5krNZoddQzageWGbKRTcLlup2X6+FGPCtJFLrZiAnYNZ1ca/raFSLX8xar9rGFR3QJkr9w97Hk4zd/uJ2+OZh/PMp8cTx7lyzJmzC/q9loFVL3x/mLli5DRWkgS6KbBOgbujmE7FDw8MwxZrOhkDOz5paAiXkYLWlRZpu8/2mxXHDjlKq1gFEwO3c1JoeZCd0ReNqpZP6pOv2Zl6EeO052v7iWjjn9X3uq2Z9j6jN2dkKdzb52hiybGNwDvA0BiFragOLWIJBEnMS/4GtYEiohStrmllMSZT+cPbHxjzX3Lz7AbUxN9DsDzj/v6OKTdnuh8FR7u+/nmLNwZUmrZGZUaZKkujBBWCVqRmJB/o856vb4T/5B99wT/9+QterQWdDpwPj9zdP3IYbxijWn3q0Tgm112qhYIV1k6wsfteTdeO4E3kaNpXWuoy+r/kpFziflXLIaZ5XvKQ4FpPRLO5NGtJ+ygN2G35nNWzlueVnAmI1Z3eYA0xOgBuGgot32o5bO9OXV3XmUC65zdV1MFoyyXnaaZkG//sh37Jm+o8U2v5ySwG+B+jyZC2zKFCqFSZkeyBVTpjOIRi1A2JaEjU2llI1IxS0FrsJmoTB7LAJFqJap39hNd4TqXNpTlVNFRM6USIkYXCFyWZfZgE8HGNlExlfYgrAx6edKNEbENP88yYZ1PgL5nzPHOYK58OM6oTBpwU0z1Ikb4XrtaJ9cp0H7abFVe7NX0yYbQuBTarTNdFVutM1UwINqCeOvNH7VfDYvkSOk9Ua4HYZmaKt9IbmmRzRzF0hkxXwAUnzbNWfVBmJMXIMGQkRoZ1JKWdod9hTY2PyGEkA8fDgbEW8qSc3h94+0l5f3vm493Eq+cDnz3fsFtHUj+zWQvzcc9q6FxIShdAvNXuC8QYhJoD4xkOjyPv3hx5vD8Sg7LZ9Ny8vGI19MSIWcGJgTipgxSrjUYkH5OOAjVSZ+H24yOPd0dCgn5tyt9EweuMJVDi70WXQzTaGmugAVZ02Pt+QgETMdqiUzdsXMLEaha8S40epSqWyTVQo/EQ/2Gnqgk04WQL9YSuVMqYKaWCVLRkypwNoS9igo1H64rU3IAJ80VHox+2gVI7shYKcBwLt/szx+NITMbweHN34G9++4Z3DzPHEtB+y30xVkaIgS6b4vZ2c8Pz3Y6r9ZpnmzW7lVnJJgcGci50qfduSUuULsju8mgJgjqo4+yFKBeyhzFc2qXSy4ErgERPzpddaoHcBfqaHZGqpbhhAR+MgVBytiRGoHkDt1dtb6+2d6xY4ezvrR3gIbg/tzsHGI7h8FIwNLgPkd1my7PrZ6xWa6ZcOE9nTqeR1HUINpYQgwXrsXp6Xo31U1TIDnYWrVQXS7JuRLWDcT6h45GrrvBXP9/xn/7jF/zqyzVX6USqB+bxxMdPJ75/98Bx7B1d9oNHq79WK3Iu9P5m4dhEHts4g/jcbNcZ1bBWUzxXrVT33g7NyvVyA61AL97192Is+nNld7Bp87ylGGiaTPDBP38x62FH3z0to07FQSIXl0uJFCPq9k0hBHLpPBkJyyjFXLKtO99vxpywvdkO2pwzJRcDr5Oa33artnymUXxdFR+PKVrpYkdz1+j74UnHiuWgjiHaPR/HRWX6z48/vUeIjU5pDxsR8OKtVuuWaiWESM6zJ/Y2atUo6Np+s/VYLtjpAkK0h+9OL9aDjULQivDlsHHA1HoWxYExm4MWHwG/AL9qG8hiMC13ap3oxvRJqNQlvhnNPHoTxEYnCtk0uCgUH4tVqcRgW16kIppBi9PmAxIhNAaUVYrW5fT3BA7mwxLwLVyrnbOY5ZpIoAYleb4XYkC0YxWNgRSS0XkTiWG3YT4J5wqHx8y7uyO9KOtVx3bVs90kdpuB6+3AdhXYrJR+daDEnuN5ZJ4rUgc6mdnuKtvtNcP6l6z/9i1/+9t3HLKiaYDQMxd4e3tG/+4jIQZ++c2Gm5sV3aCIzG4YVTDtiez3z+jpGqzIC27jjEaqRt6+ecs0ndmuEt0gSJhBbViljUWE0MYNeAIa4QXihdzuS65dVM95WiHo0t0abKxGG0vP17ynnNYQtFVSs53b9pna2KDp9NfiVnm1Qm5sy9mV7wW3OfHfty58zoUyW8PkMjLh7931pzR0VJydUGUpcIrCmDOH08jD/sxpmhjWPV3fUbLw//zbv+ft7cFGPmNklsQhFytAJaIoSeCqJkQHziVSTxOoEPuOXCMSVyhrSu45HJXnzz+nqnAcR98rTZdMnuw5jw9PCsolfqguOad6jmR6UpGEGutEccbRj3MO8ZHdZjbbxgjaedVyT8uVfG+3xqbnvQFrpimBTmxQQmptt4Um5d7AA1tXT3JmX3/aQA7s/TZBUJ1Ng28uhZACFKPXj3Ph6tkzPxczKvBvf/13/OUvvuD1F98AzsAUq79uHx8vzR3CklMouPip6X1NUybngkTT6yq1kl0YWotxWUIthDoR85FtGPnFlyv++V+85q++umYdzsTpyP4wcnt3x6e7PRNXBElUbNTTYmREukjQQKrV+7/W1V/urVyyzOJuYYo1BcWbmjiA2tZHGzltDZKn+VipE0nNenxyMci2HqrHytLuXRuLaCLUfjebHWZjjQXXUJjzTGtsNmCj64xR3gQom8vanIWYLHYDC+MhOONhznkRq/wpj/8Rwo8bAwyS0+VrJFAoxAU00JAoIZlugxjCW3S239NGG7OfDRSbh3clf1Nobq7QgrrFXilevDQMVwSJ+EyuzaKnYJ21FtwiYl2Iznq9pRUSMTjNQ+hzpJsSuVbm2pNnZ0G0oqAW8jxzrMpxrNRj5u39SJQzaKXv9mxWNqdFzWyHgeurDasUWK87hkFY9ULXQ98JMR3pumisiahINNsQc6foWW8Gaq5uc9MQLLOflM6Cc1oNlGm0DuVg6vFTsXGQKIFSRhKd6UtEkGDFR+giX3z5GVfXWz68f8vh4YGH+3vmAlkj40Pl7vzA7l3k82dnXt50vH7W8dmr3oKZuxdIglAwrQAVpFoRmGfhPML9w5HDAwzDjlAHXj/vuN4FhhUmyiQj4p66xiQwcAGBGn58+FIi+/sTt+/3aIa+T6yGzpku1mVGKzVYcrJQqRb0NyzFY6MUPs32nARrdXwwYEODfSabDamQDFhBbbMW36SCjQdZWLHnMdcDD8C1UvOMzgWqUdCLd2PP59FtGIFSoKjTS+0z55PrO4hRXIt2zNk0TgiJw3livx+5PRy4vrnh7//4kW9/+MhpnPnm62v6lXDIwl2BhxrJMlBKYq4JmYVeOtDAulvx/OWGq/WKm/WKq9WaIZqybnaUNYZISN5hflK0e9ZgQUvsfoAX7WqoaUsggzNevM/3BBBqSY89iqiJ8i1dmQtjorbI4fPSJpzoVlwhotEOG8QiS0talhlmz3f8q2hIkJLnYYGUAiGJ0aSDfz7wbk6bT1QOxz1///sTx9NkQmK+JkopEILbYlkhn6sJKoVsY1E2H+ikOzEqXgNdoyqSZ9J8JOU9X9xE/sVfveaf/ZNXfP480ukRyQem6cinT3u+f3vkj7dn8joaAh2b0KiNFxVX3Q3RxpFCMMYHXBSNq7MYOi/yJQQTUq3iIwu+LxyIuXCA7P4UZzeAidbabY3L/RvHkegzgK3YVy4aB7VUs3iL1imJqQNkETOSptNjaILFbWc2zIvdcLxoZYiBBuJDUarKnK3QCTERRZxmG0hJkCREV7vXav7Z7SHBO0wSSCRSSO5U0RgzBua0HbF0odrafQJq//nxp/WoXqFZHFfvzNvfcTBQHDyrCiqmEROCj0+Ij2U5mHap+uACPGhDx/1PK05wzCtQnf3ZEteW0NrMdCttnKmg7V9WpNRWPrSiZ3kpBxOqs7VSt+RZ1U9ERQmSIFVUsmVPamMUbSQ2l2KzyqpmIuigXnKBtOQCy9omzNUF41pc1cuY6xKjlUVoswHRjS0hYh331mVPUUhdZN1FRAOb9RpSx1wwUb/xRK2Z/Vy5P83I7UwXz2z6yG4dudkNbDYTq2thzInHxz35RU+QiRhnhi7yzddbUvqacRr597//xCkr0q0hdNTU8Wk/8vs/HPjs5TN26w7KmVLPPm5VCRLJxfLbENXHNh1A8rUTSOwPB06HI8MQWW8TXVfJ5UwuPR1roCcQljGZy3oQp+BfRkOXCCa61JmxBJSCaqFO2TqRRUxrS+xeFjUWXJCAlupggeXDTbyRarRwanFw1scWKjYq5+5pNbexn3YjvUDxZknViObgeX0E/2y5wOPxZMzPLjDOlcfjifN5Yn86keeJV6+e8f7ugW+//8jtw4l5znz91Qs+e33DMGz4w6cH7o6VD8dCtxuYJHGUwCROTff3WiZB+sR2tWPoejbrFdfrFVerFdfrNbt+Rd+Z8KYEc6bok22iFOIibtrWclvEii6Ni7bnW1OzZTpt1BicueihoDiYErw5hu9XeZIjtfM3NNCu6YP56xjrEB/3YjmrRUxgMYZA54VrU8xrIxEGMl5YVgZk6SLgiAMVBdDWKHHxwdPpxP3jI9cvXhBEuHt8oF9t+Obrr7h7vOfu0wQC728f+Nf/9jf8y3RNCW7VWX3UWcWL2UhziGuj97nUZTTiPGVyMSctgoGZWs2hR+aJUCe6emItI59fJ/7x16/4J98857PrRFeOMJ+YzmfuHx55+/GWDw8T08abDRhrU0J0a1cr6hVIXY+EuugCqjs2BHcIs+a226cr1mhQi4ExRrQq8zyb85+PNJhgr4uag9ezZQGzFqZDjGZIwIW1KSEsThHBQQBt2g7wxFq0Lo2vnG2/pxCY5pngY0rmcDj7OmpgCKS+NwBDdREIFxH6rkPAAZn/8OOngwwSKaK0OaHmLhAcXRUqxESVHmUyXkLNVE3UOgMZyoyIFcSV6H+3HmOuPsNCy26t21gE87v1i5fVPWprRnMlR7ExBOygEzG00wadTCBSSyF2wb2I28T1BTGPwRSZlUAInVnWVcxKsholxgTHLDnPuXComeNBoc6UPAMjXTzQBdPcGTphs46sV8Z+GHphs+rZDYl+ZQuk7yJdEIahYz10mDJ8ouviQtWWANm7q926t44dEKuNUcynEWZTKJ5OR6QfSGkwqn2tHKaZUw2sJdAPPT/7/AVXP3/Nh3fv+OHtJx5PM9MMj3PgMMPt/sjunfL6WcfXD8LVNvLsqmO3DQyrwDolYm/oWcnC48OJ9x/vedwXSunYbJ/x2cs18SqxHZTAEdXRCswgVszj3QwBSZHmNU4UaoZQI4fHiTc/3JLHSheF9SrSJQUxC8rqCz60COt/qhdEDeWXCupAQzsYWifgaRWqtHEAm18LEqxw85+rxZK+KiC1UMbCnDMpQJ0mpnk2hD4rmit1zpQpozXa4VuNwVDb7LrinQmjNBp62kEo5GmkUKkhcs7wuz985MPjibEEbg9H7h72PB7O/Gf/8l/w19/e8+7DnirQvajchESmY9SesWSyJGI3EPvAZrNht1mzW69Yd4l1iuzWA+vVgCCcJ98rnjAT00KTbEHXEuXLfOiC24srAvuZG/4ByNkQ/eXwXJIQEy6rmCgXvicbHbDZQrXv+7SOHYjFC3aRJ+81XN5X9Tsb8L1vRWu/XvPsxUsOpxOn85mQOgv04TLG0aYnlEY/tXs3nY5MBWSqxC6Qfe1oMXBhcsR7mgqkjk6DzV9K8SLbArv4ARtqJuaRVE683FZ+9dUz/tmvXvIX31xxvS5Q9+RxTx7PnB5PvH1/y3dvR94+VuZdYi4zk1aaZRhicU7hx/N8gu0/L4TbTB5cWA4pdSYiGQJdjG7NpMvPXA4g64bNdV7uxdLJUUPpn9o4tg5uYzQ0BkptgFytdNo6V9XVkCMlm8NECKDR2AUGklhMLjmbY0brNBezdetib9RJO21RgbmNoHkhU3KmsRzKQnU1tD62zuAFd6Dveh/RM6+RmrP5hDsbqlY719br9Y+Yc39+/Gk9FCsCqjbVhBam/HyQgMYEoem4+B/1gk/rEiftsQSSJyMCLBoeOGi1gLZcqM2yALhOk3cW6DKCpq0Ux+GBxly4LNzLUMIFFG64hhC9cW0FhAlNuryZVooGaqzWlQ/eJKoGOlStRII1QiighXM2irAkvNvbpOl0KbDBUrPQPkBtXTgrWkNgGY9Vrc6YAGrx2WIbizLL8kAtSj+YGHeqQq8w9YPbtillssI358zDaeT+cOLt7ZHU3bO7OrO6+Zy7h0fG8475dCQMSgqBzaB88VnPP/9nX9KvOv79d584TiOIaWNV7fj4kPl3v/nI+48dm3Wl7yZubqJNj2pknisxVroUSJ03l2pGxASZg2TG4wNffPkSYWa7CcCR8TRSdeaiZ9FKoMuaBKNnl3lkWNlFCu1iaVtf5nRxvH9A80yZZrMhLYKQUIXlVYoLjXp+W3OhEswVjdZtV5o+hxB93tcPZHeyqtUAWZXqsdasoIO/Xi7KNFXmopzH2e0FC/1mx1//+gdOc6WGxGEsHM5nzuPI7d0jQSv/6l/9U354d+C3398zFyWKMBwy3XXhegiciZwlcgYqPZnEXLOD7oE+dmxWiUii056vX/+M7WZlrOTVwLrrDCirNjo4TbZu7MzrF5cqY7vJApY1AE2dblSfrOnmfGQpX9ubspyTOOhoBSG0MZQWO/SC9F+iiQOZIsGFu8WFowO1zFR10WTFlVhao9bpB3IZfmnARcvBDQDTH4PqVChiY4jts6q6EUDlfDpb3qiKmHgbq9SZU1sxxlUIibEIv/nuHW/u/hvORB5PZ7bDjhiSvX40DRgtjY1oTMJcrGlT0MUJQ1WRUpCaCbkQqtKVmUHPvL6Gv/jqBX/19Q0/f7XhKmVkPpDnM9P5zMP+zJsP9/zx4yMf9sqxy8R5omi0ghP7HCGYhlVj0woeq/3eNmtHQUhB3CHH6jNzhFBz+sKaxqmzpsYCFD8BmsXZpMGvPxiDILrWQglt/JSFgd1YEKZPZXlQCLKIj+Z5JqXIXGxkKwSrAUMIFNfewsUmmyhl9VHYtn6Dj1eUUtCcmWdrluaSf3Ke85NBBlWjNxWtZFeYDN5tRMTVZn2eT8XoSXVGa0RLBMm+yJ16p67TYPsDwUcjWgGBbdZcKjZw4TQ/BXI19LRaFzDT1NI96SuFOltQrCUTUBLuCQq+kRsw0RAaXCHci9+g1CDUaii8AQ/RZvjUE3NHrU3VNRtapTBq4f40UQ+zjYNEiEEZYmS7GVgPkVUfWQ+RPkXWfWIVhSTKetWz3a7oAqReTCg4mR911ycPE5Xp/kTqBrsX42SCjONIBaZcySdstlsCGiP7EeLtnud95he/fM3PX/W8/2zF3cPI+9uRHz7uOU+VqQofR+Xh7cT7x0883w3cbBKvX294+bzjlQT3YlX2j0e+/8MHak10/cCr11dcXa0I3BOl0Kfegp6Lqnlm4XHWUVIsGWl+uF4ZsN+feHg8sg6Y/egaJI4IE0pvAV3bz+vyR+QSHMFnDJ065HfcFnQL3NL+Ka7RZKqt8zgbUiqBPGfKVKzTrnYgj+PEPM1ONbNOcS3m4WzzWtjzqVv6EFHCQqG1RDaQJTGTmIpwGmdOhwMfPnxiriPbmytqvOK//O9/z4fDzCkL51zJtSCl8E/o2JfAXu097OdELIPpNIQVq7VAt2F1dY2kxHq9okumbzDNI0GVcxXq2cafoojbc8XFQqhtOTWG4yV3Xv6rT77UAuETzQZH9ls8avvv8iSm2lsUVFzJ2g/aBeCQp4dgex2fI/OEuiW1S4dZwlInNpaKvb6wGno+f/WKD58+MY+jASNLGnc5bO0dLv03VCvnUpmDMQZycY9rvEAu1UBRhFlB1O6VIWfecawFKUrUSl9nVjryfKP88strfvWLa7756opXV5GVTIRyJk9H6unEeDhz+37P+w8nvn33yPv7RHo+ItNETR3qDigmJnQ5HJqCcHFR3OyFeQjWlWiAg6HVlpiraw9Ef45/SNub5mm5/0GctliMUZBsqNpGI8Atjy+q29UT2dj3dCm6Q4WtmxjDcsVLninzbImzd0Zjl5YNq37PbGQiI6r0zoZI/l/r4Bgd04SUoM3FG9/zMpOI2ghd8M6RBHMNaQwHKc5mqpVpGn2dukJzjMaUUKcO//nxJ/xorDd3+pHgYwoec0KbFWiMmQteLcX7zQ7kAT8CqloMuZSOT+aIcdbXJezS5vBbdGpfX5ha7ef8ZxtbwZoorVRxxxYfZbPl3kTlwkW4UoMzFRwR1OKfr/q4qn9fAqTgYpSRoIVQC1Gz6QjQOnX+PkUWoLedwG2Pm1icOvOnqd77QEer3p58ntDGBoTlaxUb3+g6oZNEVQNIDSRRcqoL6zXPianM1JI55cLHd7fo7UzNlcPpBfd391ytemLXETphGJSffblms/2SV6923B0K9/cj+8PIfj9xGs/85vdH/vg9DCtYD5Hn1zO73YqXL3tCUDYrMdeLpPSd0q1c4FwyWSqhK9w831LnM8MQzDHopC6uiWsytmLCAFO3JWOeJ6ZR6YcLME8rCp19UMbM4faR8XgyO2yX/KJEitpaDjF5keuFjxpgoILZ/XohEszIdAHBtJi/QAPPS4Wc7XzItdClwJxPlBo4jpWHxzP3+5H7xwmi8LA/EpMQkvBP/9l/zF9/+5E3d3sbM5RE6Dq6PnI8WDNyXxOH2nEqHaVWugSnGjiWQKeJWZONLnVCDR0p9Wxib2zhvmc19Oy6gVXq2G3WXF9fsep7Yxd7jjLOhZq9kIwBQr80PJoeRBOYlpbxyIVlaev8YsdsCaMXqP48bfM3vg+y7HCWto2PT+kTjYdl00sbI4gszRoNSLxYhgryBCgNzqS0Uauq7bV9L2qLLRZvmosCggmAx8g8zixMB2kj79bs67tE3+9YrwaywvObG4bVmh9+eENVO9+H1Zbt1WuyBO5PE2mz4/5wZLgxUeqSs4+62vt38rRpMChUZ1oY1d/ZxHUmlIlYZwadebnp+EdfvuAvfnbF159veb6FoZxhPC4Aw+Nh5M3HPX/88MCbuyOP0wpS7yYcpg0yzzMSkonZJgPZzL6ygbR2j0JwK0gfPhFRUmw5sC5Nmfbf6OMNbdyh3dVL/LbxmCEma46rLkwKnvysQ0XGeqexHtyJRQqlGJO/+niSMe8qqa2P+eKYJSKkzgwNqru5NE27S41reZJpHvrIrX/tpzx+MshgN9uUNVXAhREsqfIiAMTHyDtU3auURrGNLoZXEPLCtao6GSshqOnc+Mwj4pvSRTFsrzbfdeyICYEQuyW4gqlf4iBAFGzj+TyXqoXEJjanjv42dU5bQC58ompdaq+UBEOJugY+EpzyFpHQU0r2jpjNfk3zzDhl5lysC1uVca7c3yuqZwJmaZlEGJKwcv/dq3XHs+stqy7Q94Ht2lgQfRKGFOj8kNVS6FdrutUGnYwifbrbMyajnM1zROcB05gOzETmIq4+m7nZVZ6tNkh8xu1D4Te/f8fHh5EP96bePOeB26PweMpETlx9PPHqWccvvn7Gi+vEugPJhe2w4ebmiqubDWkArWfmaVrooiYgVS5A1BJkxcZkxJIeC95ACNQsbHY3xPCGLhW220CMZ0eI14hT/qGjAQfic60ucwMOiuWSTVg0WkGkS+XZ/hjNfDpOnI4zdZ7J48Q4TYQaDFSaK7IwENQR+UKeDb2NqbNzwYOPug8yGhb7z6zmJDCOs1uZZo5T5lwi33888ulx4sP9kXGauL19ZLUSfvUXle2LDb/5dOKgPYpTsMoEmjnmylwFlQ6NkUOOhLyC0NFfJ4Zna2LqCb1patS5MJ1nn88yeuohV4dtDI3VKpRsrKK5XnCfS2e/ldzQLNYueEOh2Re1rllLcC3HXfB9nmbfVVvwDn6J7S42zQCQpdtcVelXA8WTGRFzDbgwGArNECEEWbrcnpkiIsx55t3bN5zH2U5cqeDOGE1s0g5xY0vU7D7WtXKeM4TE5CKzBV0YE6VUY1ohaDRtmrlWomsFBFWYM7HMbGTi9S7wl18/55dfX/HVZ2te3AQSZ6IeYR6ZTyfKPDEeJu7ujvzw9pG//+HAb76/Z4qfEYrCNHkm6lm8Ko1mLa2rVYzRZQe5W9Spom5l2XWdz6J7bPcDpCl/N4vIpmKvNDpeeLIWrJufVQmYOFMuGape3Bj8sDW2wkSK3QJOqLIIglqsDUgtdMkK/nGamc4ZmmioYNOkIqSYWDo7Kk71VS8XBYoiyaxmtYFBpRCSME0TnbsFVD+ATdXZgQ8ic87UZDOQ4oh0058oTyiDP+3I/fPj/58fy9jdsp98zTd2VQOpaKMVxjBYHE5gmZeFdiaIn0cOki4LpSVqDrY23Ht5KitjlNpqFdrbi08AjrYfms1cI1GL72dBnsRt3xVOoa6LK1JjJdisVHVE1+Rp7DOLU4lFM6LFxyYjQbLN2Es08D+Ui7DckoSLF2Ct+BIDLeoFpPZUy6+BAxt+LrQiWkK05FkTU03kOltiHgJfffMLdrsbfv+7b9k/PlB0JjfMRANz7phyoOrAWIXD/Zm3H++ppfL9+zt+eL9i3a94zsD2SkgRNoPCVeHV85eE4YqHh5GH/cjb9/e8ffeR02HkcDhbAX038f7DzLDq2W6PdF3letfx/GbF1aZjt1Z2u8R6rpSQ6QdxCnOm1swQBltrRYxtoD7yEtQBYT9zQyDEga5XhGS5tRTXk7LF81QXo6nMVxUfbxDyHMlqNnS1nBlWGyR4I04LtWRUKiFZvB+njBIZJ+U8m4LaPJu95Gmc+XR7z1wrh/PM+XhiuxW++Ow593cPKAN3x8q3P9xzfy7cHSaG7YrH/YFnVz2v1pF/HAKHCh9nF/FzR5BdGJjiRB8mzn7fsybL5zrhPFcOo7KaI6QtQz/wehOJqx2xXxkzLVwcxPqYTLi376ih45wNSMMFKCOY4GgKzob2loaq730f2VaW87IBBAujr43eLbmROpMAz4V0OfN0CSdeuDnNqIWNxjaw3ES9rmJ5L9rGQ1XIqtQYyf5+hcson4llem715H2x5KyXXdgAviTCV68/Z7fb8pvf/c510JQ2Hmt6TcJuu6WiPuotdJsNq82WcRxRzIFvvb3m8y+/ApS0WjGLsNrdoCEw18o4Z6KaiH6pbrmr0IgDouZOJZrRkklS6HRm12VePIv8/LMbvnl1zVevVjzbwBBGpI7oPJKnM+P5xO39kfe3e757f+D7T3v+eH/k1F+x7ga/hMai12DAWXGmRnDXhwYGNfy41oIqnKdxyaOszrkU8Mu5UqoxZATKPC8sSPV7HmJcivfaXXQSmlZCSunCmHzCQi21onm+NIXsADIh0y4u70tcDLIxa0RszNdio4nUTuO4vFYIkXmeFyaP6ankBcgOMRGfEGX/hx4/HWQAu/gCUqMHMllobcvFBEp1wTM/dLRZoBCRWuxlxd0oSkPn86LqbMvfTqgfH9aR4MVic14IsQMJVJ+LUT/wtTa0EVP81Gw3NYgfl7aRnvrOeg/zyeu1wlRdPEcXVXLFHCJSsOS1Bqc0RlsM51HoY2KuLB3ZUqFSKD7zXkrmnDPjXDmIvc/392dWnya6YLOH63Vi1Qc2q8imiwxdpO8iKQjDKrJeR4YhEUvh/bsHYoIuJqp2HE4d8yjkvKLGnlICQZQkM0OYWK8y/SqxXXf06YapJm4fJr5/s+ePb4/cPUycp8KMMD1m9ufM3WPmalBebCJ/+YtXvHq5YRiELih1nimlKcErZR58nqqY4GOoCxW5eiEqCS7ZUnChwEqpE5tdx/U6srlSaj1RZwWdESlL8taoXsu/PWgrwjwrj48j6dlAF8OPf+ZJ2iUq1P3E44c78jmDwjwXdDagAtehiMEtLQM211oDtWaEZCojpZK1UKsYHfCcOZ5HHg8Tp7GQq/Dx4wOvXqx4dpP4dL8nhzX/7ncfef848Wk/MazWnI7KcwKfq5BizzlE6Db03ZrpcPKOc2R/mgndwGYXGNZriD1VtqgkVrtIFUvIpnl2HQg7CPp+RYqRLgkhBVZdInq3p4pZYTadCwNvw7I3Gorq28f+q5cRGD9KQbwQDeEiEPgEYnj6JB2teG2WT2YdJC6wZK9h9pwB4S9+/gvev//Ax9tbK1gXgMptCv0VTLlYn4Ae4uui8PHTLY2xIRWkYHoh6qh/VdfZUBd4M6BzrkqX7EAvxSiSVdXs6yqoGsi4SIN4Mh5qpaewTZWXV4G/+OIZv/p6x88+3/L8JtHFmaB78ngkjydkzkzHkfN55O7uxJv3B37/5oG//vYjt6eIvNyRup7QdeRgB2PzNG90uuD6J1rtwGxuEq1r1ZgKuRYfMQoL9qOqi46D3UMT/grNxtFBoKqXbk7O2WN5bEF8sYJt4EL7b87ZDlR1dRRnDCEFghIUulr42atXiMAf3n3gNBa06x1osI6G+k5WzBo5iDF1tBRmzYtys5baliUh2nVSrQa8POk2qODz9WG5DqgyTRNwoTC20Yp2DYpf58W54s+PP82HehenVQBu06gKFWfctey3nWX+e0/Qg2UdNFcXX3mOrz8tQry48N+TBlYv6K4V6G2do177tOdxoKP671a5FBj2DFbUNKX75R2KUagl6pL7WFFqgKqheNm0l6TtMP/cGlC1IlS0NZlsFDVHzDFMGqswW67WXlxdBBwDsi0uN9UBE6esWG7ZZs6tgFFi33P1/HMO50y/2VH6Aa0T15/3lHHPL/7il/zs659zfLhnPj1SKkQ18XHEbMm7YLzWjsTDKGRGDvnMr79/x4ud0Icbct6gFTa7NQEYYma1G1hfR14835Drhm++Gfj4cWA6w2E/8fbtHXd3Z/b7meOk3O2tQHj/aWLdH1l3ge06cHOz4uZmTbcSrnadKQuJksdMkIEyd8xToowRLU9HFC8VlxUFI0GUvo/eGKuEYA46UXDx4cKYR/qrNfSJ82Gi5oToih++/cjDYeSwPzJNmWF1JHWR7bpD60zOI11SfvbVF5yOZ3737R+p0nP7OPLpYST2A4fTTNevGafCD+8/QQycThN1mvjy8w3d7obb/Qgx8HAWfnicOJXAQ4Zn3Ybh1RZiZSp7dyJIkAYr6NWaD7kItQ5IFM5Z0ZjoVju6IZmOmRb2c4ecYHXzGSENVOnNB13EYnLOC4OoqglwZhWOJx8WcUBasMK6CASnKIqY+lNQ0xxBmraP7cOmWNXyJQMiI41D8IQcYndS2mCTb2+vJdR1SxDLWxqToDpAWcV0CZpodQMU0bBoYWV1toXYmRxDcMtne/0mft0awj8CR7houkiw500e17RezC6fNgvNRUzpOmGzXnMap0Wg8zhNLHwHiUgU1rtre60USalnvbumIsxzZsyFTgNRgzEZCBYtVQlaiLUQ9Ax1pJfMzSry1YsNX75a89WrNZ89X3E9QF8PSD0jeabmmek8cdyfeLjf8+7TA29uT3z/8cTv3t7x7lGIn6+QZE2W6sCrCSC2GByWJhNiwpBW/Lu7QhtBbWeH5wZPrU2b29Al3/G/N5aCAw0xmB5bPZ89Pl8chtqo6eLy4EB29ftjozxWi8ZkDMzq9Wr2eCvVGjBS1R38vMfmoEM7t1qe1pivjaW/5O8OhjQXrv/Q46dbWLZCTm2+xAoLMSqVS7UHByE06OXMDYZ2m/BjRKW4YrEnY8FBg2DBMkjrQLmwnlTfHoaitIOpiZA0JF6rzx1pmy80cbEWAGIQV6APSxJRnywAU+T09+JIntlEKg2rbJ9fnvy7WWpWLJFe2BMxWGBSK81aQYtEtLdZ5+wiIlS7yaa8mznXyjkrdarIabaNnCBJpQtClyIpBjbrA8+ud/ziF18ieeTu4yOxE4YYibHjrCtKGZjPB0YqK+1Iu55YZ0QLSZQgha5Trq+UYdXzxWdbvnq95WcvH/n2D594f3dknIXTlJmr8PFx4u5+5rZT+tWGSaHvKn1n1pda8zJHGcXoc7UqqbOiIAYDCDRCh6CxIAsYK4hmE03KD7z+YsvNNhD1kcfHjE4VyUrItiGrBEQN4LnQBWW55xCRMIDYzBdPPGktmAKeCDUKltaKaERKokxQNJErHE8jUEhrK2I1GxsnlzOvP7vm2+8+8nCcuH08E0JiLsq7T3vuj2c+Pk4UrWzWOz69u+U/+tUzftld8/FwIq07HubKYwkca6BLa3Iwj/KzJsaaKEX44qvP+Oqrr/ndr/+OaQxQBqapcnN9zfPnHdvdzqlYgePZkNJSZ7QaW6bvklHfHBQTlCiVIQZ+8bOvmI8nPn34aIlLdF94aSCDHz7LmANPDh37r+1SP6r8x66udrx6+ZLvv/8jeXbalYWpJ0CDLMg44kFTxPa81kWIDUfzEVMVb/OnKpG5qomIkiA0a0hD7CXEpTiwQqEl5sE7CVZcN7cw1aYJ7orFQA0mbluLdRuTGBWtRSYc3NRq8Sf4yEGqE6uusOt7XlwnXm4SX7/c8M3rLV+8WvFsJ3Qpk9KIUKjzkTLumY8nZFbOx4l3Hx/5/v0jv//+gb/9wx1/vMvk/jNC6CnqgohLlLLko4l1znNehA1ztkOxFc9LN375DNpOHFCbi661LLoOycV+cNC12V0u1l1+N9vhlFJiNQwOBhiQ25x+AnZQzdXSnkCAmHx9FGKpbGrh1TTzn+82aN+xf/vGRN2Ca6X4wixO70PELDx1RmqL7fa+YrRjrjEx2nyiakZk+HEyQDsDZAFIUko2IoK4irR1d2O0dVmdRqilktuIx58ff5KPgG2FZVcpXljjdFmPYKq4dzLyJOa188cKGr00Kdq3G9j5tHP4JMnzJ3cQ7PKc7d0Jlti3OCYt4fffCXhDRC602svvPxm/ELzwMtCw1uJfE4+DrjVBfbI3FNVigs/IRXyZSHS3L/ECKaJEKQQ1w7wFaGiILmaT2WwwG8DQPnTxnNZyukxKgasXL/nP/zf/Ww6nyur6JTXbOGoIM3/7r/9LagwM2wFlJgQDM4Lm5RKE6KwMCaCJvuuIXaLOAx8eZ/7m2w9IPjONz9AqfCaBfggu3lggjEiEJMJVrKyGnqCJkrd89dXAYV95fJzZHypjDuwPI3d3DxwOR+73M7f3mR/eHeiHRN8HrnYrrraJ3S5Ty8iXn6+JRO4/WZNutUtIJ4TORrFq53G3FKbTSCQsc9WoAT7WdWcBhiYp3Hzxgqsg3H26J/XPqPWG/+v/8d/x29/fcz63YhC2m8jLFxs0T5TpxM0mcPX6Sz7en/j195/MLvTuzMfHQlwNnM4zV9fKXJRP+8J605ueQwk8p+NQO+Y4EGJPFht5nrVStBBT4H/xP/8v+P7b33H73a8Z1Zoi6/WWpMYUXXUDfezo1x1X20DJgfWw4esvN/TrHg3BCtsYOcyRsNqCRlb9ipAip9PJtI/EwPEUEzEmumDaCtrOO3TZvxWMjVddrWARdTTwTMTdUhzQa4LSQhsn/dHGs3pELyMJftrSbKtbQ0IbK9D/3l63qNUsRWFugB4QfLQAByiWXMx1q4bVwIvrG/aPB/aH01LFGKQP4ixAK2lMg8HSK6HZr+daeP/xI5/u77xB4Xp8eP4cAc+Dzrma3W2M5FyYx8Y6DFAxZwisqVs1UOdKauCH7/NSKzqbm42oa9BUYy2sQ2XbF242wufPdvzs9ZYvX2243nXmFiMjsYyQj9RsjOTxPHPYj3y6feDDh0fe3h5485j57fd3vLk9MaYXDGFFLtAVtYanA3pzLea8ElpTxsYuyxMNqMZsM8FsZ2j6TaxP8omcM220pPrzB2d3trNAxJ0h1PS6WmOm/WzLq5ZTogEQqoSUSNGEzGNKhGQucoteleJjnEKeZx/tk0U7pDl4LSKvwDRNzN4MX0AN135oTfn4BFT/H3r8ZJAhawPOfaF66zAgy8GiaptC2zx0tMJfiyNnvknMZsU1GYI9jwbzM8dROHWf9CqNcG/ep6WKzwGbGEhLBIK2ze4HsTsgBMRtIyMSu0X8rNlztBl9ywsvsKM2NKqVQ5cWAE04xYLQJaEMtNlwQ/tiq42aVRRO/0tGQCw1UDq/BWJz07N3HEuxg7+UQq6VicpclUOp1NHQ+e5x5uZUePH1z3l8OPE3v37vHerAejWwvX7G3PeczkeO05ENW8q0ZjxGcjQVaGolRWVIlSFlQgysg7Dr1tysrzmN19w9THz//oFTDUxZmaZEmUZ++8Mt3396JDq1bdUFuoAJWnaJ28ce1BRVN6vOLCsjZpGYKqtqxUzqzAOETqjT7EXJxLDpWG0i49mo+5oD4wFLHtadwcjtnoTgYjwGHtQgpL5nGwYrBqT6eIws99vQyUrWjGxh8/kGeZwoo9Cx5ftvb3n/8ZHDVHjYn8i1MGxW5iIwZeZxourE/+rnv+C//uvfc3+GHz4e2e42hNTz8X7PWAP3Z1NufSmwn5SHLBwyzBoQSWQCWatb2hQqlUzifh+Ia+iHNa+ur/j8es30cgdsWHUD19sdKUZiTKyGFZXEw2mkzgbgDWrjPMEFDUN072RHAGOwALDqEiEF+qDLGE8D26xLfVFHtgT6ArzRZhEbsNcObZQ+Cc+ut7z9Xt07WLh894Kq11b3uwDkpfunFneww08xIaDf/f73nM9n1CmghuaCRmiOIdX1Y7TNI1azOsougGTPbpZrCN49Kca8aoG/xQqxwyCrWd42S0r1uQybT8b0V0om5YlVrLza9Xz1as1Xn2/4+ZdbXl51XG8Sm5WQYiYwGbNrNjGtcjpSTyM6Fs6nmQ+f9nz35p7ffH/P3353y5vbmdI/J66uqKljUtBc0M4KdNFmsWTXuR1CS+G8qKhxKWoaGiwsCPzCdkB+PDsoArUiCwp+YQAAPgt9KcKaMFHr6qZoIq80BkVxMdTQRtgAKaSSeTkr/2Jf+Ed//S3py1f8rQqfusjkjImWrKgqKYXlc8zThDHcXMjIP5taKUStT2ykHAiupVzOJTHaYXP+EQkeg4sdsiYeYknHfLFHDgRq8EP+zyDDn+zjaVfqR0CogwOtyaBVnyCwrSC57K0Ky/oLzkywGNHiH0uck3BRB3/a9qzSXtu7Rk2Uq80ns/SWLI+US3yuIpd9+yQmtDdgLLU2yBCsC66XH2MZ+TOQ8iKjY0OwZqlswII5QFi3rIgXuA1QCNYOqo3RQEWsOvFiyoqK6pfXNFsaU1ER9ZNHYPv8OT//L/5nFN0hYQW1EkpmPn7kt3/31+zHE6NmDueDAdS1ohi7yREUszvHzr6uD4unfBHh017527+/hQJBemJMXN10rLcdrdGl0fKMoJU0VBIZqh0RN9eJnDtKDkjsOZ8Lnz6uub078fAwcXicedyfOZ0nHveF+8cztU6EYGDxH94KXSic9ye+OW7RUFlvI8MKY+l1xVgLtVCm2QoUF1dUzSa2IAJaoHNqdhLi9Zp+lUh6ZtheMc4v+Pcf9vzbPzySsZnsWkaupo5PBMgzYT7z/Aj7WXiYKkeN3I/K3QRnOkpJnEqhZhinwpFAH3tqKExlZCqRx1NlrpEUo9kMBncSih2aC8lB2zn2vN/PyLDmZtWTEaIIN5stqxgZYmK76hhW0S1NEyElHs8ztxxNDFCtSItEvv7yM7abLb//9vcu2M1ypjXqe/Amw9M93xiRImqppX89IsakEYjJ2KveoVhymZZPmBMHS0MSr0/+3/Omdv5iDbIuetypl/0qIFUpYoxaVXkSB1ouK4jzKXJV1zUQhmHFs+cvyKVyOJ+tuUZjKciP34drOCh1CXOIkGvlMI/E4kNYT+o/c4yxOFRUfYw0UiUwAlOt1kyO0XLsUplVDF/0puLsVpRLnMkFdCLWTNTMOsFuiLy4Gnj9fMvnLwc+ezbwfBfYripDl4lYgybkDHkkTyPzOHM8nNk/nrm/H/n4ac+b2xPffTjw3acjP9xOjDoQtjdIWFOy2thpBUneVAnCMAw2iqkXV67GBm2gUM6mQ5NzMU0/Bx5aIR6aFpeoOQoKS84Q3eHMJ0IurN3GaGggQgiL8HZbr42tWfz7uK0yakK3UcIyUrE0ws0Cg/RU8PtJjtZeY/laafE9LMwHw2iDOzn+tMdPF358oqJNNJsRa35VqCbAY1Q701RY5qsrTqMOF3RevOirtmjN3dQpHXoZTSj+J1Mo2tkcoDMPTGu0LuVCxawrbZTCiprgUH+zMbR5qibMUT0vtRtk1ddlRFuDhQ7xBdAEbvBEoYETSPHRreqv7cmEL4YYsEMJE+N4qsDeGB8NnSwB+iL2mXN1mlNYRFBUPeEtlVwU0UwVc/TYz/B+P5sCrFZCOLO6mwmbiTcPE9TE81Xk9Ji5/TCR5gTXHUEqIUU6lE6su51jYbuqvLgOpLThs5eVZ88iw/aGTORwnjieJk7nmceHI8fDmXHKPB5nNOeFor5+cyIEpesim1XPqu/oO6FP0PVwfQ27bWCziahmhsHmhLqUmU4zJ4TN0HMeI/tjgBw4H/esdpUXXyZkSGhUtBbPyAwrbCwFzdnsGGN/mZ8VFwzENz6VzEx6NrD6bEd6PDPtle3Nz/jvfv2O/+Zvfsd58pUWA13/iIjpFoynkS5U5rjhj58O7MvAu8PMmpluiDzmQAmJKRRUKyPCHIRjVm4PhVwSNSeyRgjCetuTVmt2qWe1GZh0w+NZ+Pz1l8Sq7D984NVuy2a9ooudMwuEKWfOpxNVIzVXOh9TUAJSXRG9fWZfz8togypv3rwxpd5WkKlC+PHvXH55OZkvzyaXWrWVpALs93u++8N3zCVTvTO2PBeue4Brtyygn14CrgRa3i3aArty97h3llQiFzvkFKMFtg5ArS1+VAc4lMmZEaLmpUxINMFYgi8ft0QFjLLbZrL9owexzjXUBcmVmum0sI7K9lnk9dU1X7xY8dWrDV+8WnNzDbu10oVCkBPGmCmoz+JqNk7jeDpx3p847M+8e3fPH98/8Ju3j/zmzSNvH5Qad8TVc+L2hqlfm6OGitMoK1EDARadgJx9ROzJQfIjOp//bCuymyNE01+QZazAPmdjAsRo1pmCsF6vQeB4ONAUpwW3N/LCvYEMsalzuwBvJCHBhJ9s1KISFfqivDxVfnVX2Py3/4b8zWdsf3nDcL1hJFHOs0X8BiKgrlgf6GK3UBhVKjnPDgoJpRaaI7g4SB69q5VztkPZuxSlNGspU8wfuh4Q5poXwV/bBsGcJ7wLGxYu+58ff4qPZbxmARv8j8cjPEkXT7YIbT090ZOprYCWpXBpRYMd/7rE0QZsLYvGX2tJKJsYoz8F+uS/y8OZTNZbMYH0JVeRy+ysyCU38TB3AT4uQIQuT+9ssRCo/hOhynIN7GdNw0q0EqQQAjiv0+KclfRotS4Y2iJ+09my61x9pMPYrPEJwGGC4bVUSIG42RLjMwprogbIJ2o98+XPf049vYd157Pr/nwhLeeO/GhrGuAQg2m6dH1H6IVD3vPbP95zHicOpxPffP2M11/s6LYDfZ2R2vQhqllTYkzMrhaSVFadqcrHrlKvhOtNzxeve86jMJ2Uw3HicX/m8TBxv5/5cLvneJ45Z+UP7w5QM6HAVPY8Ht4406GjG5TNume37QhBmcfZbOZSYLPxIkOqFZNBjMkR3W7P7+esimTlYT9ynDKjQIkDdIksypQGTtIjQNKOcy0cJuUwKxORsVZmiWiXbDQzFmbUHCpiMJemlY1znLPw5sMjXRcYiEyS6Lc3dKFjq7Aeer797g1zUVY3r3hzf2B1dcM69UiI7DYbbtYrBgl0YnploGiZTWNtnpCqbPqe2Vecr1jWXWKVEn1I1rxSXbZNcNZb8D27AAv+fQmuv0YDHwxwiEHYXe949eoF33//vY0SetKjDjDW1kT0vdY2a0uVFvcrXV7MzswgXF/fsNtsuP30gXnKF6ZTsL0ZJFLUO9zWSllEBkXbGFak1kxAGM8z7z98YjxPVnzS8piWy8iFrr8AksG39cVB0M462yeKeGOnZdoG7hQ1gLI4GJlVmAuUXAkJJDoAgtc7Jdto5zxBMX+uUGaSFrZd4MWu49XNhpfXK17drPn8xZpn14mrrTDEAvUEekbqaF3/WqBUptOZ83HiYX/i7u7A/nHi/nHi/aczf//unt+9f+TjSZnDFuk3dOsb+pW5W4QQXRshkfqefhjo+84sKqfZXEq0+mdwfQJw9wklpQsAEBsroJTFrauJOi66VJ4fgQk4NlAaZyxUVdOCqvWSo8FlRKG6nk2MJmorgkYlirF1FBuXEcT1Q+xs656cLUsk9M+T/fWaLkNM0ePoZbyijc5cRqD/w4+fDDIswhdBEJ+NxkW0LuB7O3hcaRv/uhcH9vWW4LVZP994kvwQdlxHrfNYtVIwbYcg1Qt8NeTWDzFpR6RUm2cKBiJUrUSJaCguQukQWrUCvqhtUNTA3/YpDEOwE3jZlOIzjQsIAiHoE1FDtxSphjYjpi9hQEO0Aw17bYsxCxJxQQSBLCbaE8TcLOyyKhLMnWCaC6WYkCQlsF31VnCKsFqvIPSEFBkzvN+P3H/8xMNUSCrcrHs+ScfbfkTHQJkG5mlFP0S6vke6SgiW6CecnbCKpK4iMfHyiyvSasNczVt7zpWP7+54vN+bJ31InI8Td7ePHA4jea6cx5n9ceT2fjY0VCFIJXbCZnNkt+m52vTEkNluI32qbFbw+FgYVhWRgXm+5tOnR8bjmXl84NXnyub6mqRC6pOpRwuWkFQTukShzJOBEv0zU0L2znOb9RKtiFSIEDYd3XpFUqUmGF694H6ufPdxD9Lx85//gs16xQ9vfzCUN1fGHFlHZZwDRYRTLtSUmNXsbDI2w5VSZ64CAtvdhqkE3t1NdN2KLvSk9XNu1onQr4yWXiF2EYmJ4wR9v2E8Z45V2G0GpjkwTwY2VQ3MrRPdqHhtFEDbadZ6921EBAfQ7KePxzNJhM47DUi02bSWkXrPrIESbY+3Pe+SYObA0r4rwpQLnx4eDMgLEfwwtEBtIomlAWgt7sklYUbNwlJdHalpqdjnth/KaiwrRKAo6laRpq3gM2y+z0oLVB6zBKBa4SDL9fAIIN6BrJaoW9woRKmEXBApRuPrAjfrwGfXG754tuazm47XLwZudoFVX+m7TAiZSIY6o9XmQ3FLoDJlylyZx8L93SMPt0c+3R347s0tv/7+jr//cOR+DGj/jDQ8I62vYdjSb66Iw5a4WlNTQjrrvIeYFpGePJvAZ31yGGi1MbScLwrDzfGhjVQsGjWt6+MHIw4ga75Q5roukbpEcTVk1GJlOwx791puB1gVMSpkCKQuucZDcFZEIGnlZU18eT5z9fGB4d0jrNekr64QnJ3iNk3NelOKd3ojPk9p7/OJcL8ngw3Iss9Vis0axhRdWMmA5Oozl1b/2dfa2MiSQPhzVme7CUKXOmL0LvefH3+6D1+jwBMBBECrKfPLZV9cHr72l8TdY6RCbKKRC4+6TUH7y/m/GshrndZLMqgN4TTTSN/P9Qn66+AGT/CH9vYtlTIATFqh1X6xcSEMjG0AXHv/Ru90QKSJceMdVUJL6zD3l4IxvuxnW4yHbBo54qzPai4SdmI0e/PkWl0+LuufNygESQRRVCZi8pxMIhpXqAazhEzJxh5KhD4Z3ZtWA/rrOsKgbQxO7QRbpUDuYehgvero9JqH/QMPf/+B28cDYymEpKRUiSHb+EQXvXHk52sQ7Oh8MrYWbQSzGzKxi2y2NoJZ60AuW45j5nAqPB5e8vA48fg4cf9wZn84MZ8j53Hmuz98IogxQ1MS1kNkt+vpB0Froe8i282Km2eRFGb6PtP1QjdY869OJ053R8b7E3VccbqH/f2J9x+O5Gmm76AkA1jQTN8NROktpqYVJRYeJuHjsXAsA6c8U6QjdAOK5YZd31NlInUrtlfP6GOgTCNdEHIIxK5jlB76jquX16i7/AStHMeCsCL0Pbf7GQ0dQTr6bqBLvQllV2PwojYqQrWGTRF1AUSbRW/d/Ajc3z3weL831p22PbZslOUcaABDcxSKDWxwHA2x/RPFtsKqS1xfXfE2Ruf4t6aFn3m0QtOee2Er+KoIDb6QFlcuKF/XRdbrgTsBa/djYCYWP5BIUpMHawwLXcancFFtXfSE5ly4vX9oG3nJq1oOZenVE40YMSZVSxkdPmP2uaUYPXNTB+ydeVsRNwWws73FiKLuGKaVUI3FLuqC/7XYSO55YugT21Xi+lnHy+sdnz/b8tnzNV88X3O1jqw6WHWVLs1EqWidmOcDms9QXQPOHUGOhxMPjyOf7g487EceT5W3H458+8M9v3//wO1YKf0zZLhGhiu6zQ3degupM5Z7SlgTxRwl5jmDmnj/OI6L/lSpdWGoSTCWQMCaGKEBCVUv2jYe78s8X9aE/ljToIlMAj8q9EsxrcKU0kWQWoJpTCXxMXAzYSjlspZiaOall8aYOLuz5WKtAVSrOfAsIIO4w4avs1orocbl78vX/gFY8f/t8ZNBBlPCNIRavfinGGVjESuKjV7rNObijAbsIK1Uo7I7TdBo2F4MuI+5zdGDyEXdU5Nt2losQRcXe1vsjETt8NLCWIvR6AQXalGW+esaCP5+XYrX0CV9euy3Q5UnAkj2XwGkeoHiPsBtQy4giyy/7oesCcWINnrSJeC1mailZPP4Y2MeXnh5ch86Q6WSBB+lUCjCJg0kDXQqXG3WbK92/LN/+Z8iq2v+T//1v+G7f/tbHidgyvzx/T35UcmnxP6zDad5zYvzzGoIDENimjI3z65tvqcoXQh0Uqkhsxoq61UhrDOdmACi1MAqRvLLFUqiH9bMM3z6uOF8nJgnZRyzMQE0MZ4Ld/cj+8OZKWc+3U18+Hi2hCNUui7QJWHoErVWhtXMsw8ralF++ONsYnAhElYrXh16VqqUKVNLZfJD3QrBQi2Qp0weR8gZRCm5+jCbWuALSnIVHwObzNYpV6WUzN3+yFSFmHpunr20a/SHP5h7RxGmam4fh1NeRGPSsHbxnUhI1g1fDyuCFq52W9bPn9OHQAlC6reM0jNcXZMkUoN1VAsFnUGqzaSZiGlAijAdZkI0aprSZvdcVdz1TVRlUR9eEHpPXjW00COolgWFxq3MAgGks0QuqAt0qo3S+j5ua93Q7OIMBqFKIGPU/6gO5rgrhzZAoVSaD7OBC56pOtDm6TPilKLqgh32Wv471ceyTNoGJTot/jJbiG0rajbQLw6ddx/EhKAqdJ3R5W33OwvGO2BBbalQDXSLqlBnomauN4nPnq95/nzNy5uB59eR51eJZ9vEuq/EWAgyEihIne0Na6XmkVpsRELnzHyeOB3Mhuzh4cT7DweOR+WPHx/493+45bv3B/Y5Qbej669J3Y7YbcihJ3YbYloRQkdcDaSh8y4szHM2ipt3ZVusafToPE0Ut7ZMXbf83IJML2i7zeFd5lItESmu81BL4XA0sGGes1ML/fVCoxMacNEcKlpS1xhveTbl3+pMMCSQY+U+Vd6vlS/6SJfMRmzMmdHXT3AgD60UWdK7BTAAZ7WltLAqzMI4Lk5C6s8VMcbGPE/GaiiFkjO5lIVyKJ2vJ8WsedVYETnP1FLMAkorefzpCP+fH///+Wi1s411eiLoMaiUTOriBax2MEAcVGiipOLe78BCd700LrBk9gmzqzoFmva6rWjRyxhPA6/EQdVlnS3Ar8Xo6vu0PfQJohGfjEwtBY9awe9PbPbitPPCE2CrbrzID0DBdd+sw4qdpxU1AVYsFgc1NhBiDR9ZRhStOaSSPPezkiU4yyyUTBRn5aWB0LmFrejiYS+KAQoRHh4+sh6MIWHdQNMKKNXPUU++KnZ2ZFWiBK42KwJnUlRWndDFAS0bjjnz/ccz6bc/oJxAXxHKxHoQYh+tgxmE2HeEzmbYQzIRcoIxMxQXsQ0Wp0JXbHRDI8NGubquvK4Deeo5nwr7/YbjqbB/nLi7O3HcjxyOM+excjwrn+4n9N1ogreGkLMZBm6ujmxXyvV1ZLsLXF0Htjul1pn725Gr3cRqu+H0aeDdh1vujopm6EMirQc0JMiFdbdhiD1zEPpuA7Hw7lF5vw+MckNOptEz9GsbT+gTsYvMJRNE2K12iCpTPzolOHJGKbM5LhUVtFg3O9ZKKMYKCWKjZvj46DwFHvJMFCWFQCKYNTLeqFClhmq/U6FRBaKPxcxTodaJyzg2C9jQ1n4ba257N0owkMHzCNv2DcizXz6eDnz/x+8o87RMW7c9bq/f2jy+Edvu/gdfanlLsU2FKjw+3DOeDkzT2bXj5MmvWkHhO2bRcGjMgtYcqajFK6fol9rghAtrVLE6vzWgWvMUBykV0xYCc0Obs+3v6ILWlsst2ZLlcth4amtmtHwxeOkUSkHqhOYzUWfWSXm+W/Pq2YovXt/w+rMtz5/33OwGblY9614Z5GwNHfxelJkyzdagmGfqPFLnkWmcOZ8nxnHm8XDm9v7E46lwf8r88f2e3/zhE2/vzxxmgbShG7bQr5HVluhs0JA6QurRaLl/zpVSJkrN5pSHjRR1XYcgJhYtgItA4yCAzm3UNFCkQBPMxpoq2YUUTb+rLpoMVZXiTZ9aK4TAahgMIFBd3CRccYWUOnf9YmHC2x0zQLXJBLCw6lyUe5oQEXKx2rXvzd41L7mgwQHWoMrO/ISSM/M8e6PJxl6zO5X9lMdPBhmm2S56wBwV2sVtDl4XVwcvIpaNwKVL6R1mo6m34sjp0TEs4wZNFEVixbE7L0DsNSW6ejFiugW+mYRKlQxiYi92tprav7ljRIY42JS2FkNvqpq1iNNijd6CPx92aCzFi0KKrkTRGA1WVGU/cCmm8FuKLhR99QTCajldtBkapaopPwtGyxK1YFWqUd383LJORBBqiFRJKJG03vI4QmbF519+w2dfPucv//EvuPny5/zb3/6A/s131BDQTvl4zpxOZx72yofDgZ/dbXl91fHFyx27deBwnhESV5sV82RBHh2JkklJkViRkAlBCNGAgfV6pKSZINbV1EHoVKg3nY3R6BpCh8SBaRYe9iP7+zPjOXM+mc3n8TzxeBo5nEaO48jDcWaeKkUz8e07RAOHg123GJSTzuynW652cH2V6FMkBGWz6ZFgQlNlnphz5nycWHUzw6DMUyUGbc16ohbkOHMm01Uh6sD5fgQZYIKpKFk6KsIf3r5HS+F4rvSrgdM8cyqVEBPvHmZmVoRhMBsqAsNqbfarRFYrW3ProaeLkek0URSyWnIxF6WEAghVi4FSSegCRBewNAtFzEqzqLF1BA87uuiABEc6xf8dW/uqAYHWxlkYOb7ULenTSAwwyQUgMHC6dZfasSlLcKzSAAtjNBRPtq3A9sPPE4umk7CIO7qt7SJy5F1AdS6vqLUUWo7cxiJysSJa/X2Yf3VtZzFDF01wVCvzYc84jdy8eGHXKhj99zyejf0z2BhWrRXRQqSSgpBQOgp9KLy4WvHq+Zab3ZbdNvL82Zrn1z27bWIYIKVC1JkYTiZkVjOUDPNM1WzyM7VAng1omEfGw5nD44mH+zOnUbh7nPjjuwPv7yd++8Mn3tydmUNPWl0ZwDBsCP2aGgckrZHYmXtIUWTOzh6AacrkyUQKl1jaDpsWhsU0C6K7Iygm7BjdRqkh8aWq24M6MByCxdYAVEGTdXRzziQXm/SlZvczm21m0ykwMcWlrCKInyGqpBiZa2Geznw3j5w3hfRVz/r9ms0QeKgjxylQJJhasidKQWxGVWtBXNW71moiSjHZe3Zl5mmcEAlM40hxRkkIga7r3M7JEqYQAuJfKzmTus4EH1WcolgpuRBDU3/2JLAh/38GGf5kH/EfFPqNaWjJG086BJaTxNYoaD/jST4hef7wBAzw54v++4ZMPAH32o85sKGllQWXPbUEcy65w+U1LJtqr2udKNMQMNqmC+A+eSbUgAYbDbICXcHH0zw782ZomzwvqFO07X9VzGZTnZcbxRo+UQKqpr8AlYwJ56naqKsAGlrpZOe2SjaQNiUixhjTUCiaOR1OlP2BdH0maGd6OumMhImvf/45662JHBeUqcB5thzMOoMOsCxCwB0pCdu1jXh1MTFEE0wb1mtUhPH4yLfv9ozTyRpWObBZQZdkaYStNgNdL8Q+0K0GVuuN5YKx0aQzEhPm/9fAI4jRFPOVjAywW8Hzq0QpiXHqOewHTqfK6QSHIxzOlbuHEx8/7Xk8nTmeZxNcPGZ+uD/RBaXvKpsN3Dwbef3Kzv67+8qHhzuGQbm9P/PHd+95nKFfv+Z5H5FhQ1W4vhKud1u7fquZGCBG5c1dYeSatEusVpCJpNDbeRPFGkRO3c4k5mkysexiAJMJBbZCVJd1F1ToMbCuAV8KTDWAj1gHlCTVzmS3QF40U6QzGv+yKp1pLBCrWl7fii7E8u7GKJJAim1UwtkMrQkINH65rSR/71LRuXJ+2KPFC7rGaBMDVCwo1EUcUZdNdmFGNfCvFWwKUCvjODNP85JfKfY+WxGpgjGYG/gQZBkWtwnPQiVSxIWVVU1o2fO++uT5nBiy5FWtMaMe0ywuGGMqK5TqTSIutZnVNcsHBDCRVy3L76OFpJUhFTZr5fl2xecvX/HlqytevFzzfLfm+rpnuxb6ztgSkRHRGZ32lPlM9ZHGOmXKOBNJ1FyYzkfm85nzmHk8nDmfZm4PZz49jtyfCn//wy3fvrvnwz4zh4EwbEn9FXG4QuOGKolcK0FNpyN2HWk1EEJaXBVC6ogucB1d72ABn3EdAzVRUcN3bJy9LiPbYhpOcHHn8p95yiAFIbTRXWx83Vi0lZhMoFa5uKTkWWl6ZO29WeO+gI+e5jzTtHFiYzD4e5iLjXqUUsy+3EUoW644DAO95z+1FObGePA1G0RIXfeTmyk/3cLSO+peddgHmSZXCxdPICOxS0goUAuF4myEdogKjdMXnlD1bB96V4BLIDZrHqO551pNdMeWt81wO1paxQAAu6iGslf1Al0iQcw/tFSY1DcDYZllisGKfyvtHYF+cv1aklGdNqi0mZRK9AIvtmKpBQR1k03xjoAY6mnMRWN7BKcwNoBBQqOKGxWv0mjJECQSxYCSL3/5l3z2y39M2jzn2c9+xiCBPJ4Zrnas+iPHt781f+NaHKyxDoCVHj2f5pnxw8SbT2eerQP/6MuRr1/vuDlmQtowj0qdz8QUWG8GpBNi58BKqBADkqrNU8VqwjgY8IAow1Ad5bIjICSh620/7a4C87MBnXsbKajClJXTVDidM8fjmWmG6Vw5nDyAnGe2/Yr9aWae4cPtxONhstGKQRZByWHdkZLS9wE0+7yWch7PrFaBnCf6AKsumgvHOtLnwqlk4miq9HdvR/rNFolQWENakbXjw/1o9OqwQdKWrAVJhToM/M0f98SbL9mEFb3PgKbOAlZwNwd14cGiyqnkpbs0Vyu8FxBAklEvg2DMCmg03TbTZdRBL/Qdafc8kODFZBDzyjWE2WcQtSHk6t0rR699H4W2JqmEsuDWtHGky4awDoQxEWTRhqhgM8pqMnuGjlejcTlNto1GNH9n8eS4zecuaUjLo6uDlzQVFJ9HdfGnxn8wdlUmhcpnL19xPBy5/XRHArZX1yaGE4JpkAQl60RPYKVnyKaE3kthtxJurgauNh3PNj0vrgdev7ri+cst601i6CH2QoyKkE3QrPofzQasuLox2cAGLZXanGPOJ07HE/vHI3d3Jx4eC48n5bs39/z6+3vuTpmHSalxRep3NjPYrUn9Ghk2hsDvrqmbLQw9pEitUOZMKTPTbGKK4nHLxq7U+Z9Ao/n5mEJKPcFYzB7fDCAVV1VuxVCZzZ8aMdZZQ+S7riOlzlaK+np5olIsjnw3JsOicaF44V4vdpHFDs/UrTimkfDNF9x8/TMOtw886iOkQIcJF2nLlMABM7PFMr/pQuveJEzUDT+Mc85M00QumeggyX6/B6BL1n1OMRJSou97oxl6fMep10ECaWhuGAamnE8n8jyTuvhnkOFP+NEKmYVJ4H8PDuhaRW6xKLpgn+UeLIUXXIqIFp8WyzlkabpcfsbtWL2gaPwGA06d7g9enFvzxoDl5qhkOZEFSJ+hdfC35Re4xpRqa/7qIgJsRUkbK7CxJXUROduyrfHi77tYIdX0SmwfBBPpbkLAasBv0AZc2F4XaWMZy5AdhLrkSUZdz5htaHI9rgktHe9/+MR/9X/437N7/TO+/Cf/lK/+yV+i9cR4+JbAI6f7A7J9xlgys1pBbMmmF4Ti402SgGR6DKnQd511zIM1f7qU0PWaGuB0qHz/6cT/7W/e8/Fu5rMXO66vXNBaC0MSNqvA9c2K9SYxrveoQLcaSFEIXYCuJ8SeSkE6d9wRaxTgOWAIQuoqJVdSD6uVUEui1ESpPXOOnE7K/cOJh0Pm8Vy4fTga6HCYmabK4Vz4NML3jyO/e/+JGIS5TAzv9nS9sD9nHo6QCaxffk3UwBwC6mLqIZrFfBc6u09BOY2Kakeh2b8HZluc1vHWi4L9TKaqAK6VhTc81HKP2JobgEj1cWW5nAk0lqStxwBN49DYLUEdMPC8wceQDbg28CsVddci38eeVxmHLdDskie5iNb7rm4vbI1U36uX71kjkeX9B0zA9BI3bG+ZhlqrUSo8eW0v4uUyutCEIpvd4EVIPviewq9NIONiygLS9mATbXwCGsRsTPNcqjsI2DhvG6jSZfex6AywfNLLdVOw3/OxVMXO7yVL8xcVVTrNvidmuiBsVolnVyteP1vz+ec7Xj7fcHMVeXa14nod6ftqYpqhIGRroApQMzqfmcdH5vPZzm2JSBXqWJmzcjiMnA4HjueJ47lwOBmw9e7Tmd+9+cj3H4+8fzhxzFC7NaHbEPsdXX8F3Q66LZIG6AfTNPBxzTybM55iTOZcSrtLpu1WjcW5sM5CIKVIyJnq+lVND6uBCUuD2fMuFfHRzAaW+fpHlhGMru+pNXvDGWq1PdYaRWq2d5Rq+g9ttkeBPM/M0+hyBTYNEFIitTElEXcfqwuzt+tsPCTEsOR1qFmYm96EAQpNCD3GSCmX8dr/0ON/lPCjoTF2E2qpXkx19n23Msm5MM+TH1K+CBs44RToS8ESl4NKMAqTnQltcNxs1KzAkEWszp5XaBYuLHOw6r7DkWBljr+WAQe1VjKmMh7d8kPVEPam6toYEnG5ceqeqBfEtPili9FCgcjFH71t9tBFpwcajb11jRsNHG2UOvygCcvv2qF82fghGEIPSkyBr/7yr/gX/+v/Hd2zLxn69aJTEVPg+PFveff3f01/PjHnmRgiSQIaXawu9fT9CpGJ43zidMjsv7vl42Hk2Try8X7i68+es4kQQ+X5sx3bq4Fum8jTRNeLKT6LUOqMBH/vFVQqEiEk7860mX7jmxNjoaszcahoh28EcaGnjlI65tmKJi0wFWGcCudT5nwsPD5OnM6Z07lwPBsoMc0Tj4+zHXR3hjwjjhyWQhRh/f0HhgGoM1ebgefbFZt1Yr3t2FwnSghwDDycCr//duLx9IaJez4+RFa7zymaCF3nRVcipoF+pfSipNjx/hG6zWsi/y/2/qRJliTJ88R+zCKiaubub4mIzKys7q6eXgYHrIQLiOZDAJ8ThDMuc0AfgANoaDAzGAIamKWrq5fsqsqK7S3ubmaqsjAOzKLmicag44ADkiiVyDNevudubqaLCPOf/4uRgW6KmTKa0aVBDbGQedEl04lL3AchBQtIAIbFpuRfJjDCkM7mc3S/M6YaKO5y3mwj4g2hDQdLVaDLsYiKzcgiaLVF05yciWCEa3U8mG/uRb9HY5OMzVLe3Pfz/dvwxbH26ijtXEccTYvPMX9ucg/vYKSvc9NjAmdyxOdyh/8ROkFHo8UG0jvdNr7/63/nkqna+dXTAx8/fuDvfvzez3dSTovwm8fCr7954NffvuPdQ+aUKqe083RKnE7GeU2czwuP786cHhbyYqA1GgT3WzEGaTSQSq+v0JrLKuLeo3Zs22h1Z7/s3G6V55crr9fG10vj9z9e+Jsfr/zND1f+9ucLL/tg5ExaH1iXMyk/IOWMpBOUE7I+kM+P6OMT9u6JsSyYZvba2K439n07NpU5wW94wbasC3MGoccdEkBVeDj05ukaS1nAnDHStsoYHdV0uB07W8Ai/cZY1/u0Nx3AD0csUsoZ23dqUAcd7FFqbVgw0VIqERe5IKMzKqy/es93/5N/Qnm+sv8f/k9YG2gSnzqEGd0xxR3+PKgqVNdHmgxaFA8WbImkwrIUUve9ZphvxOu6sq4rqilYEMponYovY701f4/ir12y/3ldvFgppdDazr7dDnfmPx1/fIfjXRIFVfqDgQcYqk6J32vQRdPwSODZJE1tMlOy5slJPlSIumZWJW9ZDCIO9MY+7zMdxfrdPQed/+ayrpnd7rjHnMTa2y0i/uYOZhw57cxfewdSpiHrW/YoEfMnbzYZjQnvIQEdM9YtnilcitDt7pflO1NH8T3Zt+kB1pisNcXCfwG6JWqAE0rB7MzXrzv/4j/7Lzg9/T8x+8Tf+49P0Cv1y+/4l/+3/5x1Sfz9f/w/ZmuNHjp2mX47U/Y1hu/NJrRuWAfNIb3wgtAn6ElZlhNtdOpV+eufdj49/8C78yc+PBaUjorx4Wnl4+OJP/um8u2HlfVBSanz8LCSYziT14KW4gDD4yMiPowbw2F3yQnVHOCNMwNSMjLznAQA+1H59a9PNAqkB55fK9//8JUvX258/rLz06er08VvlctN3K+IFWmKbo2tGdXWgwUXpZJP+YfvnzIaq3S++/DI5frCVnevZ1CyqUtkJCGSmZN4n3SPyIDzuiPx1gfjjnEfN6e51mGCYfPL5vQlpu+xRfmzMqIGVw72xFBxVoJ4Ld4NwrDJU5BM4yy63SjmgxSbSUzzuhNTDfM1IMtkNQXjYTbW0YMIIf2Q+ZP+QW04M08O5g/BfObO0Jwgg/8lQniZdN78m7OOVASLmeeIx/FgEAWY4nes10LZwo+jdzfcJtK1jppQjmGnVzB35pIcUZjH6CaYsbMuMzSeaXWHO4RBFuPxLA4qfPctv/nmPb/59RPv3xXOi3F+FE6LUtJA0yAHYwnxYTS9EycIqxttu2C3G7bvfk6tw1DaNvjy9ZWfPj3z8rrxundeLp2vl86PX2/869//yKeXxrVDlxOyrqT1AcknND2gyyNWzrA+sDy+o5+eGKm4z0LvdBto6m64aONgtEd75WxPuQMJy7KASJjx9+P+dmncm+SG1hgxkLlLdOQNg8BvollftVrpvVOHg7CH4WfU+kvJpOweDClluujdf2o4mOtLt9+Tow160qjPCpo0huJzK/KaBiEAhE7dawyGZi0jx2AnqdJb49/baP4Hjl8MMqAF1cVjGc2nO9Y6zbzYnIYQFhoQ1NFajnrLVzRNU/87YyD1eIAHEhq/iZR5vmoPhJBAU4imnXA19vfndGdHLS30Vf5gOLBRfPETp4l3G+FoH6+DeYyT40Fh6GLhiJxAU0wF/O88oq0fi5FKQrMbW/Y+o808Xk1JaJqbHUjQBTGfVgiEgdi9UZuv7wX+/TKpKvnhkcff/Jby4c+dUilOx7NWyWXh3W++w5IjZuecfKAaxh0lCe/OZ5Jkxlqcul13/u0PF/6awb/522d+8+Fn/t437/jmw8qfNeXbNlhvQhfjvC+kArkklx5gTtHZdtLpFGtUP5owNFxUtfq9YM0ZD+o6d8+sn7GksC52h2QBs0Trmd6N/bZQ90Gtxl6dQnjbBper+z7UZly2yut15/W6c711btW43oRxbbQG+vPGQ9pZC6ynzMP7M01gR8nLV54vjVsVqgibfcPDhw/uhR3sk4F6DGI0wX0Mtq1RVMhA7Y02mm8wdmewEP4hmpQI8ML1he6v0YfvJC5xcJ6PAkOn1ECChhWUXBy0OqJbJTYx8Q1PRZwKFq+XkkQhFZuHxDm2wS4NscESJmcJB09EHXUXdTlEC52rBD7vC1ucG4spkTl6zjBab9z2jcfHEpvwOAo+XxHu1EGD8EfgoIG5SaphMsEG1+I/xOQrjZsvnF4+Q+oM27H9mVNJvP/uke8+Lnz8IPzTf/Ar3r1/5OnDA09PTtF7XOChGMVu7K8/sVgl645IJ+WElg3RDbETNlZfq46EGPFCpTdsv9Jfnr3wMUVap9XGvm3cLlf2rXJ53Xl5rnx53vn0WvnrTxf+1e9f+P7Lxq1CM8/5znllWU+kvEA6Qz5DekCWd+j5CXl4RNeFpjERFTumrEXd8EdzQkI3N2xeS8VCOzsRIQ0Dq72GY3cUJyMcyZ214EyDpEpO6dAP5iU5OycYbYhQ8sKQ7mBapFp0vCDsYdI49hFGpe7ZY70xNJFtTjFBWvdiSQ1+lVi+fYeeFH12NhDJYj2FvKz+nmrHusUG6RnPKbuPUA3N4TRqKili1XqnpMRScsS8CiWmDE5BFY+qDd+F80m4bBf2fSdl12ie1tXlIEkpUsAGOguOPx1/fIcEoCBvimsAohaIhn4CCxLPYKxg3nzPgcEcFkyqIvchwuHw/Zb1Ivd2fJqowr0emhvjfRgxQdqZrhLPUDTLEyDRA8y4NxBvf795p3kU08P7laMR8p/UY3Dg0eRvT48zH8bxvVHXBFNNRcPrx/2p4tX8jA31otghQnp3Xf4EbKKu9pSgfoXLC308sz1/D9vPDl5+/Yn2emH/Mvh8+pFeBZPFa01hznR8EmkwhjirsMf6ZhpeYRLmf763JlWWckYtwdgZ1vly2/lyvbJvO5jxcNo4nV75+LTw3cdwwn+XeDw1TouzZNc1k0tiPa+cHq/oklxWUTtpyeR1Qc9L1AReM4cftU/qDchel2pukCAtlcez8eHhRNvPtJ4ddPj+mR+/3PjysvPpa+XT68alhq5eYtI+DPfUMFaBLG7yvYrxkOHbx5XffHfmemlcNnjddm4tEgS6N9KmbgA4TLDkgJIzNQmdfvglHXv9/d4f8d/eewwFfc2NqphDeh3XYN4vEvf5W4NGIEwFfdCA2UFNz6x+a80xuRBsSgea5hubwh+/8gkduB+0OjCl+Jxstvfz+eoGQ94+r1EXQNDw79DffCTng+MshAlgzOduMpB8aDfmUNW8VlLz6VsbDQ3fobk2TCZ3a52iUGtlHxa154w/nEDosdS5JHoCELzpx8zlS947OQMpq1CSsWbh3bnwzfsz33x84Jv3Jz5+OPH0mHn3WHg6F87FWUL0Dah+Q0swAGTeSD7YkDCNNhtYbdheadvO6D7Mvtw2WhVeX6/8/OmZLy87z9fGp0vld7//yg+fX/myDS7VGCyQVsgraVlJywnRFc2P6PKEnD/AwxP58QO2PtAkDB/NaHtD8vTJcDNECSPpSbhBxP2XplF2AAaTZdBnHxzDj8NPIYbQqndJp0X/M+/DKdOYIMZozqwQEQdjIqUwifB0Wuh7pw4BUR+Kx3Ud7oKPaglGxXA/CXFwYf7+ebjpth0yjtm/ppRo3Z+nOYwh7tG36Rj/oeOXMxlkAfHFqvdBb3dkMdCD0HdH9xLPtReqEwyY8/mg7EygIVD2+VpjbqIjFjYkEDUJdE0CRY+lyDzAEpuUuNDEuONiYG7xNlWimfEIIr/YPuUPIYQjS8PBCLEUOMWc/g6M7IuPzgcRjE5t09Qumue4gVo3+oz8GA5KzMVKj0UmQIbRA+10NNP35pAdvJk6DHFfBvGMTJenWEfPT3z3D/6c6/OV81p4ty6MPti6671W9UViUWegUDL7Xti2jeu+cd07P12e+Xc/X/n1N4/8+vPG3//1E7/6WNhb591LQlNlKcqyZNro5LKw742P337ERqe2jbK6c3zKqzNBevbFfuwESoQkCaaPQQoKXArkubmmX4CluE5vLR4d6UBPmNL0ROuFYc4jqF247J2Xy871NqitUE15vVz59PNnLtdKrx6Y+rUNPv1s7MOoBiSnXXXLDFX2LofOVCQdkash9CRbp8jOWQa/fvfI0/nEpy+fHRwyBxKm+7FKiofcm7zD/NDM5T7HFD8YLwZTMwiT2uYLioTXiTfa8UTFAz83cxF/uFWElB3ISVOOIIJYP767ZS+Yc45H14ySY0KX7IhNuu2N2rzpb32wtUlLP37r8WcRo+gAKuvYQ+oxn/7EG2/ut6uM394KWPf4IxVny2QH+RaNZjoJJSfODyfWtbAuyvlcOJ09oeTx4cTj44nTQ2FZE7pAWQupJJYiFB3I2GB7pr1+odSvpBq6YBtIVyQVbL9R6wvOYCkkzTF59ybaWmW/vTBuO0pidKXdKtfrzut14/ly5boNPj83fv5c+Zufnvnrn575/usexV8ml4V1OaN5RbS41CavkM+MdHZ63+mJsZ7R5USX7ETjgZvzCB7BFr4GBqHf88JnsgXE3C/GN/SpkY0I0AnsYtTmk0ob/dDLSjABMhw5yz0AiD2a+n3fHbjAEfEck9HRu+fR4xtVqw3R7O7NoyPJqG33mEgTFkmUsnDdL/zw6feQH5wGeBuwLlhrXJs7NS+zKYo9wvo0iBNa3aOgNDcuGsOzr0uhLAsPBzvJfSXabaNeb3f5BlCDEpiTaxS3baO1Rm2Nbduo++7JPOKReCJCCXbfn44/vuNeq92fk3n/g9/73eT+XEy379nEIyEzcNDOzHyPPtbG2EO4g6vHYeZsn8k2MDtYilPK5IdEC8cssoLBFryBCTDomyYGi4mZzRLi8OPxZAh/P1OGagGyTaAifm28Bf8s3hTPhi+GQLHGq3lto8EGcSA6nlELQNyixvGq3Ce+0wg4zowHYEIX54OYCaNt/Px3X/jhX/6O7XLhv/nP/q/8d//Vv2K7Nb77VxeuL2Ccoi6LhncyGIbQDtBTYPhZG8No1kO3biFVMbIYaUmU9OAswL7TR6fple228/Nt0K47f/08OP2w8c3TM7/9ZuXXH554OBWSNM5LYl2Ep/PGhw831pNSyjOKsD6eaQ8nziMA+l69uVUlp4LmNDEYB4wURx+kklLnfO5wdgD4w0fl/fvCX+yZ7Wa8XIyfPu18fq48XwefLzufXzYut40OrLlwXjPvzyvffTjxzbuF94+Jjx9Wvvn4AAptdG63ja0bNpR969TNDTW32tn3znUfvF43N83rxrZXtr3TYzDRu3sk9JDgzPq+NQcMbHi2yVEzGQfgMJssZfanf+iDYvJGFuHaT/poNBsk1A16hyeOmGt8oo7vTI5BPC5HBSPxrI0xSCElFGvHADGXfEibze7yJwK8Oxr6A2QM9oINpt/DlIPM53UMN1zMObu0yexuKhnv75QTdW9st5unSZUSYOAdMNyuF+povhYMSOuCW2f6c2xzHfIGC8FlCyIOqjiwBTkNyqIsS+LhtPL4uPDx/RMf3p1497jy7jHzeFaezkLJxlIspKgVZUdGQy0YC6NBGIT72ti8SXUn8PizS4Va7dTrxn6r1Da4XCufPr/w5fnGy+vGZRt8vXS+//TKD1+u/Py1sps507OsqC5oOiF5JZWVtCxoOqPlifzwgTHrqLw6GJESpi51KEvB1MFINT0AnDG8f2u9Ozszpv0zoWuaQ896a/o0TC+syVjovbOWQm01ZKyDfduAmUCopHXBzE2o996P+3LUhoqz1U9k/qNvvuH15YW/+fLi7ZQW/z6LWpm57ropZYo9aFg/aj2ba3odjO4RlhLDmN47y7r6Wh5/N2WqvbfjtX/J8cuNH/uI0WqPRS8dGg1iQbYReqOQRDhNz6UKjjG8bUeEuS1OlHsK/ybIOGKakBj3v5xTzYCWpq7RnzOl4/n1PWQYyGzqHXF0+utwQzMfm4VBpdNM7NDchF5YnDbpppTzVdywZ6A0OjI6Gu7MItxzqRnuIDvFjTg4csR0EhuxOsVck4RZoP9zmsvCG0doX6Tm1psQXY7XMhZGXtmsu+dAEp5KohcvJIY11pQ46WBVAclIEdacWXNhW1ZHpEfnp9uVTz+88K9/fuXffP+Jf/irB/7x1/f8+XcPrLmxLspSMvu2kUpyIOPzjqiBdNYHv7HX9eRI2rqg2d1bc1l8AV/ivauQPHfpiIgbdEaXA30Do0m/6xh7JydCL6oHldTRXeG6K30UyvoezSt1q/z0w8q+GbUl2kh8vWx8+frK88vGdTdu1bi1QR2Va93pXVniqtuYsYj+oGY671f41UPiNx9W/uIffOSbb57Y6wfMjL019l4D2Eq+oFrGTN3Ax3zjHYJPhsQn46OP4x6ptZI1OY1t3HV8IyJMBc8eb+aAgAXa6DKwxD2lJM5NGCYKXiTEQ+nFloIx2SaCJotiJwA+lL30KAqV2hp7c0RedYJ/gbqPmF5JZlgiR/bwpN96A5qikHI/Ddclw7pkcnEA8XxeSBnSklhPBU1wKsqyeFElWZ0plIWco8lLAxFjzQmPhzZIA1KKf+s+FbGB7a9st0+M+upOxgVoU6oB0gWRQe8bYxgSnjNpgpit025XtpdnL556Yb/B7dZ5fa18eWl8uQ1+eN75dz++8tffP/Pp9catDUyUVFZO+eQSgeIAg2ig73mF8oCUR2w5w3qmpYW6N4TdtZllAfVVQoLZoGE4Nrq7v082w/xQNloASvnQ7ZkZ+76FXM2NgdxRfk5AnBp4Op3+AMya7siaEkWEVivbth1TiZwyy7piy+KGXJpYHh7ZU/VpmnnR2Xqj90YV8UmKGlsz/vKv/g18/kvevfuO/doQc53j1nf26r4KOYcHSl5Y19NB+c5LwcQoKtRt43a7eU68eDzTsiz+GcL5v0daxmzkWqD5Xmgqkn3ieloXWk7U5t4XfXSkVmxUSk6UnIMu/6fjj/FQCWpx1A59WAxEnNE1ZNKdo4noM5hSg8l1n8LCfViCOmdRkEMCetRDb5gNh4RiAhyzIPyDgs4brT8AHozQl9u9HCKARJn11vw7n96OYLrJTH2YiIHG9781YsDrOWvNDeCiGUwWPe8BznidZKKMQ95ByDs64813OZ0x4WVoNGNKTKQ91UdwhqM3nYUxoFXld//ie078n6n9xr/+F/+W15fEaflIr49M/aAxn+txlGBOsgu6uPi02U3UYm9l6tV9DUtBB1+KA9uqZ3oYy16XjdqMvXW6QTXjxy+VT8+v/KtlZ82ZrIOHJfNwUr59f+K7j5V3jwmRyjkn3r3feXrcePz8gqqREixLJi8ZfXg8zHYlL35DqrNqD78dndBWI9ng4Tx4PBnjsfPrj8aff5eofWWkB5qe+OHHZ77//hNtq3z79MTHD2eezpnzycipYmw8fcic3w9kcbB2a4O0ZAzou9G7IFLc9F0yvSdutSOSIRXqrXK7blhX6l7Ztkprg7p39tppbdBa53q90fug7oNtc3p468O/t83B3HwuNVKN4A+ZkxMIE3Q466eruO9GeEg4AObXtuGv614FPVgzkyfhIEPGSH0E+GUBSJj7KvVGSqv3I+om8JOBx5Q/c58q29tnNIQLMT6KdcSHm616nLIcQ1C/gyWei6zw7bt3XC+Dx/LEAG7bTorP7AwKo5jx+vLM+6dHFhUUb/YT4mxWEfegi9STXIyHx4XHh5XzmlnXxMPDwtPTmXePZx4eFtY1sSzKUpSc3RC06CDRSDocpLRxgAbixZBfp9FicNpmcRj0dE+s6TUM+Otg2wcvrzcur1f2DV4uO19ule8/vfDDpwtfX3dnMVw6t73F0G0JxsIKumBRQ2k+kdYTsviQJi8PpPM7RjmBZmrtDDZkXY/+wVlbg1Qyy5z42/AkrrkWihypCulgtdphhNhaO/yn7uyWAJHG4LbdYuARbP5Zi/URsfSd9Xx2WedpOXxiaIm0b3zoxm9L4j/5+/+Q/+Yv/wWf1OgpuMU5fND6GyDBoo6X7BLSMah1v8tk5uvjYF7J+WCgzsQLokbSkLkiwno6O1PsFxy/GGRowzWIioTZimd6yRjhHv9GhyrewPsm2J1SNXxTkQAeHJX05bETxjN4Q+TRkIpL0Z1me99jD/jChQbm3hB+oow7EW8cXHILcFyUAEIi9YLpejsw6bhRjRveqLsioTE99kbTAQanrxCfOZopYb6joKKMAy0MuxqM4QBHNANZ7qY1jpEEW8ErkJCamH+O+ckD7ZbREWsgET2lRi5CZ/DwbuHWdhYdnNJgSz0WVWPJRonP59iWI1SpeM77EGgYljK365XX1vibzxuX686nLxf+7NsH3p+Vx3NhLYntdnXfhyRkfebhvPBwzjw9LZQCS9qAwXLyKfLAnUm7Dcpp8WY0CTycaRjrckKG0usNSRmTjOQws6Jj4nFXJhFFKh4n5WoaB4cyA80+nSnLTsowTp1VBZUCupDymW3rvHxdubxubLfhXg/VeLlVvlw6L5vQKXQTLq83rteN2gfrKfHt+4W/+PUjf/HrE9+9T3z8duX8/gTlCZEU+vN+xOX12hEpiCltCJqncZxGRm+iR3M3G5/tdmVdFr93epiHSaLtjTGcBt+HsddOWpJPz2qLBcOj+5J4tCQM+r5TcoIx2PfNF36B0c3jt4iGq4Hm2Xw5KIAJ+1a9IU6up++Bkuac6MMbPgFarw4slFgjDPLqcqVeGyWnw1jQ15IA3FRd66648U0WNxpVSEkwa4gMNMsfsDnu04M5X3NzR3cyh6GK6upTEQZWK71eGbdXrO2+qYtP0ix16K6GdTqjP4fSXN3YanUwqDX6tlO3jcvlSq3uAv78any9Dn74vPPj542fXxs/Pl/5+bVy2QammVROLLmQUyHnlZxXSMU3yHwir4+wPGDLA5QHei6MtNBMqR3GXtlbJfVGWc5HY917942y9/B48fM6G/+Erzdu1xDshtgn3HwMWuQiZVUkfGuIyerMToZ7GoWE/4MAfVm43a7efHfPXL5er9Raqb2R1EHHpDliJo0ZnVfr7tFdCG00ttb5+frCP//r3/H48AOt/BbV7E3KlLbjIEpKxeOhrpeY4Awezmcv37qDISmet6lJ772zhbFUH8PZCDi4cNs29lr951Sx7gbGOaiPpaS4RwPU7NVBq/Cq+VOE5R/vIXo3XvQmdE6AopE2C9NUZjKtTzMD8PWhhwYDzo9hFiZ5kx57UAL+gM0g80sk/tlCqsabRsWPow2ZDCSR47Umw0CnOWo8s5Oh478rfu8bM++7YWQ6mqUZt3YAKGaHl9J80x6LPOsTwxmlIxgL/rvcyDjYX2/M444o8fhU7l0VYKMQsiU/2d0UteSyla5IVZIpp3wmfXji3btfUdYHni/fM1r1czGgjeQgvYDo1JT7oeHToGaMEeDDIUM2zDyGMolRFHIATbpkj6K2RO3RNzEYrbHVjZfaed7d7FfYURmc1xceHxMfHhZOyXj/WPjuQ+W3377n46Mz8JailCLkJXF+bJwed2+iHrpPrk28Rk3eoPqlDaZIpIDpcHo7qbEuztHN5zP5ceHv/fYjt5cz2+uFRWApkGRHpFHrla1dyGlE7bIgRTDZyOcHB12y0YNFjEXsnSodcc+aUhg10faEdGG0ldY6koqXtNEHMGC7bYwwn9urN1i1Dv/au4MM3dk9mHC93mi10/bGHuDF3jw9bvQBXal9UCvU7reWN3Mu4WsDah/sY3dp4DD/jJrdTUK8lF7Em+mcFS3OIhZV9lrZq+NiruGPeygiUs1GlOz+fL9NcnGsYBzPseDyjhGNrA33fctm0RfBNJanu6n1z9+/oGIsRd3zZHhakuYYBiXFHpRvzs5EyUnRZKTkvhVrVpYsnNfEsginU+H87syH797z+O7EkpWUjVTmZN1rBTfFj6SXAEpk9OPL/QeaO+sPBxwkvmx0N4zuHbOK7Y1eB23vDizcnB27VePl2vjpy6sbv9fET59f+f75wu8/v/LTlxuXzeUBRibpSs7LUT9pWRHJDF0gLUg+o6cH9PTEKA9oOdFTYWjBkrv1dQv/E4h6ckoeOq23o9YRUUrRuxTepuQ+JAQRYT0HpHOdn/X8TGYY8b3zHsjJWc5JU0gpYlDY2mFICt4Pl6XwsXX+6WvnH3y58ef//e9Z7cS/M+MFjxAfez3MT+Y+MlO9zPrxHAzz4UifiRGxNyxlcRbmMJZlPST/rbUDoR0aUlgsarj/8PGLQQZl/MH/9ylPpB90/wAq86T45NmRLF9YDNeEOzNgtveTUhfIsxynZz6G8ZDaAQ5a/Euffg/RxDnidze8801qHPQ9fFjrbAGMNggNtS/ak8KPdKZZUTJzGZFGKoTcp5zINDibzIop3bhPKRw4Cbq8QlIvAVJsWk53jIIg6I1TyxN1QhT5zg6xADJ+/N2/5K/+q/8jH//in/Dd3/8nLI+PmA22lx/oL39Jff47pGeU3c0W8+CEI3M53LDR5HKQOMNJg/GRlAaMcSJJoo+K1Z2XfaP+fOPHl8qaPSawJOV6u6GinJbCuiQeToWns/Lx3cr7h8LTunAqiaU0cpGjSNPkzI2chGVRtsdXRIW2rAjuipzPJ7QUkpzRLPR+w8yjWTTNYi4jJRwMbDJp7J4HnjsmFaSynNwrQpO/n7U0ijTePwBd6N2dZq974tag24Ks70nLI1+fr3z99JV923h8Wvn248o3T4nHddDrV949gZwaeg7Ke/UsWknupdCbg1OCJz6U0xL3iQMOlExv/qzMhep2qZyDsjRGnDdRap2ItKOOe1VKya427OoTd1H2ukf0k1/zthmn0xKoqYB1VJ12X5ZHX6SHU0dTzgzrtD68kDCh3jZycQf/Go1rXkpcr+a6L/O4W82JlNWrPPPnDoHaXBN/AIHHgmKkMHDyjRcvlGJdEInEEhsul5I5YQjjJFyOIXRHzt262peOrmhR9zYZnbFdsesL1Bt5+PM+xt1l3TB37+27L86tMWpldOO67zFtaWy3yr51Xm+NL8+Vnz5Vvv+88ePXnR++bjxfG80SJgmTwnJKpLyQUiGl4pq5VNDkGyNpcfbC8gTrI6Os9FTommiWqAY2JzrmSHPO3Wmt1TeNSd1U0cNIaPTmUZeqLu3TQLCDTm2BxKsmRDuqPZgoPr3fto0+POFnDL+vU+/OCChO09PQLuZcSDpo4nnSe61UIo0Ib8pyKlStjN6CKQMpu68LEn4vDZSFjx/+jGV95POWQzLkus6cMypCbXeqN0DO6WiE9n0jp8S6LJScj5jKY+MNHWVrce++ya32JdyO5soizqmUwrBg48zJQWjSW3Ug5U+ODH/Mh4UWN3FIDWQOR3jT8AfQOesSuf/8GwzhGInMZl1V3hSQbwHS+X33n7t/39SMB15gUyJ0Hz5A3K8Qgxc3qBYbx/TJ5jCDey0T9r8HFiGqh4n3sMkCCEPlWS9EqoXYNMHzdzibNP+edPfLMnD3upjcjpk+5TJELIZMhjfQsZbfOzQHy4vBKcE6YoIqQrJCkkRTQVJiaKKibObxmYYwtIMOp3OLN0uY05X7G2+hWQKOQ6fuZ2yMN3IVc1NAkpHVWZOrKT38MYYV9r6w1U7tg23b2fvOXhuvt85P20b+eSePwbvHzDfvrvzZz5WPD5mnNfF4KizFKEV4eLjw9Ljw8LRyelg4P5xZ14W0FGeH0d1QMiSYZtUHMqYRmxz+SdKRUkllY1Vj0c4VT99as9K2jV4ro97o+5WxF3oxZB0ImXp75XRa4zqPuF8aWPII0hxDwGKuaVe/jhhoH6Q+yGkw9eeqXses2wDLEXUanj1jxo5CIsWD5Q9Z3Z0BZx16c3O62hpjwL51xJyVcN0q29YcVGjuvdG7cds7l8vmw5jhkcZjgGhxCY1TZnw2aXOYEc+6Cn1k9opPg1tEzFvcrxqsQcwHqhNDi/7GoueZiU133gRMn6yk6sO/NI3ng6kJiC7ORF4yDw+nSPFzP6H1tJJzYsmJnAWl83gqKBXr13h+jKzGmoQ1GKDlvJBOK+XxRCoeAz/lHARAczy7w7/GmA3nwIZLEOnNGQvdm2+zCT547dF23+tH3amb10z7Zuw3l0Ncq7FV4fPLzg+fr3x9rXx5bXz68sqnbee1QQ3WgpSFpJmSM0teSGklpQVNBTRj+QTljJUTsjzA8gBppWmmi5skOvjprKAjsQ1i3bv3c9MbwSO4vVWeNZVqYkmJFgPBFr5Pd+aKmzKKiEsu39QUvrYrKeU7KKHKklIkUOBr2XDT9t4qReFDh3/yCv/4pxv7P/9nLP/wNzz9owf2vrGNFMbdHLLNCSo74zQxTVVTThQtsV5FtOXw9bTuNcAPOd7nZFuoxiBzjEixqPyS45cbP+IP1+hh5gUHUoMF4ShnR/SORWNuFP7AmFhQSUO7N7VKMh+2WFCOjfeO5sQTy+GGjGHiU9KpfRLmJjEfb7mDExGFlwhKnr4xiJQpX2iBQjojIQgVd/pj0MGd427MUUaQzmOqqu7Wi6NkglPudJpOSsdFG8c7PKawE0Wxea5tnjmOIkAFfvg3f8nt9X/H42//nP/kf/O/5tf/8T+F0Wif/yV/+X/5Z6TxzK9++z+i7a9AIyVlwVMbkgRzI+Qnrmfz9ycKqJ/HtShi2SUhOUErlCQMMV5746VBG8br7vpvEZ9+lnxjzXAuwuOp8OG88O3jiW/fnSiLMEZ1LeKaSDJYsvD0WHj/dCJnhfHsRYcay+ONvGba6UJalNY2B46yU780ZSRi6PzkR0Mt90LOVTWDoYaU5NdQOpa7T7FT8+uTieJssC7dTaxUWD7A+v5E3Vf21xWrG9jw/GbbSFSer1+8gR5GFmcs1NEo6eTsGese+TlZPZqhnHAhkMAiSDJaIN2Kx75KMVhiItRBkyFqvsGrOtDWQ9pT8J1ZO5pis8KOyY2IU1slu5dIEY95RQQZhp684ZMB0o1cwhCmD3KJJ14HOXd/dtXR61KCOjw6uThCnLKhqYNMoxj3hDAxj1ZN8fyOSU5Vl+1lf46cRhZT9gkc2jRBCjprbxg1tuqgyIsxRsNqc88JsWMN6nvCUnY0ea9oa9ArtJ2+N8be2DcvGC4vF3r36XrrlaRKve20LmzN6baXOni9DH76cuP7zxf+7ucrn587z1vn1l2bqloiInIlJd9UkmZEfUN0eYSzF7SsWD4z8glZHhnLiSaJLcybhgimCU05aLsCxcG6Ppy9kbKDbW+zkWckkqf9DPa9enHUu2eiN7htm8e4FSGn7BMp88V0BOuGMY7c5NYGY9zIyRv9YU4fXpcVFWFdV06nExasAMTNxmxYGP+610Ep5UDXE85i0SReLOHRokbBuUkwVJDkz/eyODMh937sGR4Z1cnZTS5LmkkRiVOAdS28KVpzZl4OE6eDvbEsnFY3k2y1HmZcNsZhKClhgqlRnKRSaNW4bZsDMX9iMvzRHiP8CCT+PIZFgyCHr4xnn3uhaQfA5c+gNxkdi8m5I3DBktJoON7+woOryr2ZDZ+dWegZI27x4MaHM+MRDc6sYe1IG5L50oKvuwfTy+VtiDfTw8wTj+I9SBiFTZPHaSas85cIOAVdjreOHZxQppTEMQk5mqoeNccc1jjICaLu1aLh+i82yPE5JJo9zFeAD4vwfnVjvofFY3P7yLSRudZK2hpZYejCLk6ZN1HcqNx14WJ7pHxAZ9KK58QywAUVMJ/mWioOXGtyfb9/HJK45ETFXenTgGkweCqZWjK1D25LYmuFrTZvZM0HDpdt4/rS+Xzr/P7zznkpPK6Zx7VwXoXzAucV3j9kfv3dI0+L8M2HM+/fn1lOmeXkxsxpWcnrSirF1/TVU29oKaSPsZcmi2HaYIydVq+uIsyP1L3Rrjdq3Wh1o51vSAZbABbq8yv28J6kmbY7Sq0RiXfU9+qNEUG/1pLDBJ7wVXrTVutRiHqTGvMgDYBKbJpeGjICsTMjPbhBQlYFMq0qY2SX0Q03QM/lRAv2PpLuk2dRWoPL5RJ7Vujea6XkEpp7YLj0t9Yan9HCo8RZp9Pk3iYLu9mxD7ThqQSEoeNMwUuxTozhtbjN3iEeUm84Iw4R3ENLJGogRZI48F6C7bksDvRn/5mcsjMPJJLsRkO2C/X6hf3aPPJwdNQaWZ0FIVnRvAE30tiQ4UaSol6TihQf+JjXCmqDsd0YdYuBbgAJuIyEMbDmf251c0+ogXtI7Dv7vrPdKmaJ1oTLNvjytfJyrXx+2fjyvPPzlxufXjZerp2tC/swhiQsF5bkAxkfzng6YEkLmj0e1jQjaYFIj+hpZaSFoQuDHIOeSGKYS6gZ2jrNdmdlHiwAQSVD4tjL5Rh8RdRrLH36BmQYb+qJ3vvhxTC/L8ego9XKaI2675jhsvKysCwLJWoPlz/D3iqMxNI7H2/w+P0zD3/3he2v/4ZRMvkfnLFsYfYdMlkJiae67CuCZALg1kmMd28xzeQ0WdWRGKYSKZH16E/dGHw/jCmnNOSXHL/ck2Gvh2ZVfEfEtxs7GnYJJN4C4et9BHvhjkx7g+vT/NkcWmxcc8+cVLZJBZvxSb6PBhI4d6EJZJh34DaR9MChDXeBFfWT3OeNZD69JzY25lQ0fB6GES1MaCzFkOmSII7u+3YatpIam7mOoKYMVCMnWsaxYRvzwriGfBpjeta7lwGt24GuzxxrYmHIBv2y8fVv/h2Xl5+oX/4XWHuHtBv967/l+nd/Tbu8cpZv6NtGyco6ElinmTgjZThFfoj5psBbkCPQLu9hGGSKCpTMmoOhMhxY2fugpRxosbAbbKPzund0N9JLZdHKKV14PBdKlnA6FZYknIrwUIQPjwvfvHvg6bwwesX6Rs7C+bTw4d0Dp9ONrOYMkEVx6Z8bUKal0R/N2QO5uPeDjkOf6gZZgkki5XQARWZuuCh5kBOoSWwckFMURdpJuZHKjiRItsNe3YTFoL6+omNnXF/p++Ju+EMwGvvtShJfQKx5gaOxwJkq6MDEkVSyOLXPwvRoYl5ZkZxC4gMhpkOaHHnsqKBNwgvDDvbrtGF2m9LAxRJYmArN1ArU6OJRm0ld/+qUn3FMqohFqqeBRxdF1WkjABz/Pb6a+DMpEe8yuh3v9e7G7o2mzMQNolBPUeR1V0/SOr3XWBMixcZAJGG9YbbPVcH/q2Dbzqh7rEfNQYfhxb/ENfY7PjH2yvZy4/a60Tfj7/7uZx4ePvDDDz+BFj9/w5uJy3WjDuW1Ci/Xnb/7vPHz143PL5XLLRJJSGE4lFhXyKosecWkoCrkVPxkasHEgYahC5ZPjOVEzycsnV03WE5YUPXJC5pWVIszGRhuJqcaJmZzemBAd9qeeAE/LCYkWamtUkRIfcR59fWy5GAxkGgtZF5RHE6UnQAuSimYCPu2HTq9w6A0UHEV4fHxkaFKMdfN9tbDn4Y3jAKXMdxuV7AwmRyDftuw/cbr8wuvy+B9fgRRl3QkpawrKsq+bRyRYuFDMWJtWnOYP04HaDjAgtbasUGWZWHJDj7V6vfawNlOqnqYgE2DJ1V1c8t4rW3b/G5KSgkJyb37+tPxx3aMMXy6Dge13ydfeDMdaybANDMc0kMdeZc+MHXaRy0SfgwEcDEb9j7i2++6VyK9gYNu/eaV7K6iPPbt+S1RG83G3v/eawh/9gJkML2bSA+jTVDM7DBuPZhmNif6kUgUco9D5mGxx8YU0H/Y66g5O+nBGBLPt4612deXrM6Wkjm+HsEfEA6mkeB7/7cfTnz3CKNVB+It0w10ecKuFxqFJCuSz/Sk9DAD98bVwVUlIzijYUgLwKST1NdQl7QK1t0Xyv2MgiqN3v2NQtPvLvx+Ybw58aFHUaWmaCByoa8rHaEPl6Td9htb3Rk2eB2d2yZ8ujUYNxSjpMFS4MM58Zsfbnz3UPj1h41v31/55v3C01NB1L2YTk9nnt49sfXK+enRhy8oSV3KIOqsE9oO206/XmnXF1o1HkQZtxv1cqHvO9Z3xmWja0ekIq3Qnr/QTw+QF/ZtQ3OhnB8REtrywQCxpIguvp/HHW+aPHIyDD0FRxNMPI54BDATd1swW+KZiKlab8GWGD64sxTR3dYCQFJSd5lzyilGDl4TuQn8BJCU8+bSXkRoXWg9mJU2wbOCDdhro2hxPzARWh8B3I/DhR+B0Rt69Cdey3uaAEf8bJIUNPWGZnVgfb5GyLFcOqoBYkW9hw90h7ikWdIb9o+AZAcVsIbaBNthvL7Q6ie0v7CmDaHT2Z05iHugiCm2C6PCvntNmpZCSgvdlFxOvl51l4dqb/TLawCS/jlHDJ1p7gvWd6Ntne12pdVGa8a2dbbeeb1WXm8ds8LzbfDj5wvff37l80vl5bWy1cEWsglTJeUVsnt3pbKQUnYjVE1eC2hGp5dVPjMkQ1kZWhjpTNeFrplBQtLiDKcobUzVzRGT+ycdvgriRqCtRw2VnO3tXgR3SYOKm10bdjA4DxYDHI24iPtUaUosy+JyYpHD56lWT67qw0Gt0Tsa3y/qvdI0+N3r4Aa8Pi1cXhKnrFBysB7cU2KRgo3mxp05+zAumGwSw6AejIXRu0f4jplyMRml9/rFEw59D+jDjS9nLTRNLX/J8cs9GXrQn6Kg1Ow6b4YdRkYWVDfXcod7vUhcWX8diWQImA2H/9kCyZ/aRU1uohXqr+Nn4W6Ycvd3uE8f3uoOsSgabDhVNzb6hFMd537B8Mxc88o6EPXQ60ksHtPAccwL4jfgmKYnUYD4opb8Z+dUI9gMifCPmKwOCLYDuC3NjItxs0lnM0R0IbAPn6Im8Zi8Xirbl2e4vTLaxr/85/8t/4//+q/48a+/8Gd//5mff7qEX4I7+ZrOgBr7Az32RIrF7CiwFJ9qzuIlMVhLIr+hcG6tU9RjDZtlt8ex7hILc6kJvXNpjctLBUn+MDkS5foyhfOSeFi/cl6Sa+GS8f6h8LAWfvVe+PhucC6QrLvWPlIpclFO54X1tLOUzLJmyqqk7B4AmjOprFha2PbK4+MDmOfLurEhWESLWjSgvqi44ZckDR+P6udnbDA2qJWkmevtQm+VvlXq5eILlFZSXmmvV7pksgj79UoG0vlEEoW+Q74i0hyBZSA5U0ZovHDGDckXL8YI1o77DGi6b0IAllyjqVgA5A5s6YygDDBwAOg06ZkPCMwUaSwSnVNID8QCafdrPmnmPnXwzcDNw107KOryI0m+wU+jcJGwPFPIQeF0qUO7a3BbdwBMYrrRqstjekWGMdQ39EmflNHAajiwOEvExBhbZWwVG51hlTZ2RnfamQ33T/HBo3J5rvzN737EeiEtj/zN3zbKuvO3P2zsfacjXK47eSnuEfD1xpfLzq27o3ZtwjA3X9WcKckTKHJyqUgSo1AwPJtYUwZZQBdGKlhIJUZasbLSpEBaMV3oQ+hdMF1AnA6YykoXkNFJSdGS2VtjWNAW436orTIuLnUBTzs4n8/AjDNV0tBjray1HUV9G+1YH92TIKO901sNAEFJJXNa1zfUQG+SerADXl9f2fedZXFpTUr5wILnZjw9eFz+4s279cGSAkCqO+eSoW08P1+4SqcXc6rwGNRWnTkVUZRjDPcviQ1/jMHLy4tvtvvuQEsph5nRNGq67TtLaAv3bTv+/q1UIgUjgigapuTCl05/xvZ9p43B+XS6N5p/Ov7ojplTPw5zOEdt7/pcBx4Zgx5mjtMFfsqP+pzIRu0yG3KbQP4bEGCCC6P3eHrlYFsCAfQqQyaV3AtFGCRJ8VJysMW83JoV02R6cvgqwARS5PBSgaDUBiAAFhLWkDFIZAHZeDPZu9dv0yiMoOh7u+YS0SmBnc33mzNx1FxiiuE1pKjAEJp5IyPm7veeM698980jt1c3VF5OT/TdSOWGacMkO0UeZWiiD28mPekifr95gPSwegA9khKxIToDJbbHHt4TTmH2+2LbnXru1HcHIxO8qXfn5TWKGCOFDNXEmRwJelZO+cStupcM4r4Jte7UHW5153lvjFfjhy+dv/m58W5NfPeY+dX7lb//q3d8+/4UsrbKt9++47e/KbR25fr5iolxOhUezicoMaioG+12pe+775HXV6waV4x22xnblb77vtkuN1I+kdaMaIe60V8/o2WlXa5+TrcLpIzYO7StWDfGvngcYFJozX0YiPssueRyDhcQDSaI1xoz0pFgkRjO4BMDctwvemcXY+aMiBGJUBrTX3Xps9+bXmPeU1LCr8IjLRyUCKBmBIs1BeNiKYMkfq0lKdK7113MZyTexxjxbHit1Nsgl+QgwkFWCPhxxjjiDdustOfQxZ97iX7Cn98QKh1pBRDyp4M5IgdjhdoZrdIuX2n1BaWSpWN5RL8U/grSI6EjBjd7pWPIniEtDElQGkLUqqNSL69sL185lxNDhD6MVj0ycwxj23a2HerN3C+jDrZqXG+d52vl+5+/8um58uW18nIbvGyda2s08xjUpMVZmurTsFwWUs5kLQEoZJcfaHEwSAuUBSkPLo+QzJDkTAUtdHXvNQsfhi5+bxgO7GjydA7fy+UYDkwZ2L7vDDop+z08rGG4RLXWGvdIIYUPmaqy7TszXWpZFuCexDXXzWVZKLkEw1LvzIDoe2dq1TQ3ddZuZwf+29J5/nbnqom/+NvBxyK87jc4+WtZrD+tdfbd18hhMW9M+YDAW61cbxtmkDUf9cw0CV7X5ailUnzVAD/yjOMMv6pfcvxyT4aUQwdrWK/0urn2GSEXnwaJzZPZGN0bs979RGkUtw5USvTrsQmK+BBTnA6rMqkeHKyCGXN5bPhHsoQeSKUXsr5p6WyGdfoyeATStGjQaJ57FIkhJANC1oDTCS3QfEf9HX31jTsABwuwJUQTE5Vtfns4mGH9kEv496XQOPlmlMXI4sZyLqfo/l41oal4k2b31+5B8bu9bvyz//1/yj/+q3/B7Xbln//n/zX75yv7zfj8eac1peRAYaXPbdyLJOm0gDe8GBnHeZmxQH4TGfTmzAYV1uzutGJCUShS2GvHJNFEGEPoMY3AAvlt3T1hDEwyezOGdbbeuNbBcxfSbYBV5ma/LolVhfeP1/B2ED6cFkpWjy9MypIgK6wLvH848fhYSNJ5fCg8PJ3IS2I5nUgps+0b/XUlF6dePz490bpDT2ld3Lx0WDj7Ot+0q/o1bDvWOmwbtm9sLy9IXtkvF2gda536emVBad2QUpHrhqHUfWO/vrIbvOvvw28gkUeF8GeQUkg5MZqzCQg9noM00WRHljBFsNocVRfCdEvRtCIMeqvYcDfZsW8et5Q0NqEdhhdUUv3qe6c30A4uF1LEUrAXWoBwvpAmqXF/Joa62Z3La0Kz1xtiidRq3Pu4XwtAynEreRNrMjBasBgStg2sJLCK9caoN4ZtWPPpsogh4Y9gZtAao1UnKFtE8GBIhVFDyjU6W9uhQx9Cq8JWHWwwW3h5Nb7/tHDZM3/742f++9/9yNfL3/HltTHAjVRrp5QMyWhRvqckJFkOndrU9uVJ50sZU/H7XzKQGbIw8uKpEVqQVFwikTLkgqVM0ZVOplvy5wgvikczeruR20BKZm/Vr2usN6qKJG/yJ9o8zDdB8A1wmvdINPmT2peLf7W6U3t13EnEY0LD7wZGbIrZN9+bezTkAJ1moR0XCg2TLMcYhxcH5trEdV2ioRkEnknJPulhdJbTiSW55GZ9GbQvF7Z2Rd8tLFmpuxs9llwY3E2J5oRg9MbpfGL0xrKUY+LixBufjNbWjpzn1hqvMbko2c1Se3fD1mlu2Xunx+8Z5nTdZV0REdbTCRX3nxh2l2H86fjjPGbSQLSXHLzKcW+OCbo1x2QssIPhoIOze3S+4AEszHkLcK9j8Ees14YmjXDfO5A//XQc8Bp3Jo11ss6p7WzZ/WcGEqNlOeY7khKJu9/IBBkwBzh6249C243I0p1xEMMkG2H2Jdn9rHCwb4xBEXV2IdHAxXMi6OG9QgDCIlNmEoaaSMxpYvJsE8SQIFN4YsBefX0zsqc6vDb2Bo0EutAtDADFwdERclqNZg8JQzpcSndco3l+KQwZNJuJN85EFY21RRxYGELIXvxcq4T3UtSjTjuOQZvd09NmmLpiSPJBgqXkEdaSaHlhL5naClur7NUYQ3kdnctr48fXG3/1441v/vaVj48LD4tyKsZf/Lnw9VIotpPT4OlxoT1U2nJFl0RZcwClsfa3gdRGuzWet0oawtgGbRvsrdL6oJs3Jn3r2HUg547qYBmdba+uQU9CrxdYV9+DU0aSm4a23khlwTRBSpSyHg+KCe6eOAwobtSXMnqkJfVjrXambYunYlI8Q2ZpsdmLe1CJ6OErcLdyiD9I3M3ibFwmgKEZSx5vaRim/syl8GERcOBCLaLG36wVYs6exO5sZnBmk1jsbwJ0H8AwDkYU5pLcFJ9n9MFIk3VC9CnBEAr5lRzrwvShSvGYD/r1Sr1doW6M/ernTIYzZs3ZhaQ75yIssgLoh2yg1bz2so624Wkae6W3jdvrC32vpJMDDJfbxrYN9jbY6mCvOJuzZ2pPfP5a+f7HL/zND1/5+XnnZWvUFgCsJnoSRFY0K4sqWQtJl/CvmOkpyU0dU4FUkLQg2aURXQuNDLoCp8NvAc1ILj4sDekn2T2ZBubPW3TekjNpTCma+4q17syYI7sUb9p7v9+TUz7QooaYzAANtuMEGFpIKyeoC56w1XGJqLXJklBn8R6sCa8n+vCetYS5+qaNHx47t7/3DaU2vl4aEhPkYZsbEk+2RfL0sF4r215J2eswglk6fXp8OOmJWy4dyrzUnVtyFuwSgImmdKSeGA5Kn06n/w876L9//GKQIS/FJ452N8MQWcIgyTeMboHKzMgPidZdg+qqzkBAOBzv51PrVCli44OpCSQWj0lPmlooE8EkHpr5wMg0MJpgBOEQHwgWEz2PtJW5BsTjFq98SBdm+jSE7MMxy8NuDrX739vc3sdcwzhycnGqcLWI0JOQgIiQzKijOSVKFcWN+6Z0gkC0opWkYXg8DGDKuA72n17ZbxsrJ5bHldPHBx4eP3L9/Y9cN6eOqwoZR0N76Ee92Wqx8U1amRsJDTP3WghDSwfk7pNxEVhS0M/FFzxE6V1owxWqvqhnKIsbmJhwWgetO/th7zUeaomfrWxtZ2+Dl0snA5+uN0q+kpIF08FjD9elsKiQRuNhUT4+rXzzeKbQ+Phu5d3jjdOqfPP+Had1Ya83Pv/+J8o5s5wL/Xajd88TlsdHeodUMnkpJPOmRM2Q/caolbY1xu2G1Z36+gp6hX1j7F6gbdLI+cTp4Qklk3tFq5+3HDq1PUyC9m7QXkGSgy85QUm03nzDSgIkeoX08AD4uRnmxpWjNSjulzELyLE+YaMx2u73uSTabYPsi+6wTts3yAUZg7ZVB/+SI5j58eTPVDg1W85Y8nihXE6AYrWG7CLTWvVpwVKQsTH2gaUFG0Ldb+TkTAZrjS4J1XDvrzvDXG84rPnCZQNrg5wLfVSkN/btBtbcmTiux+iV3jyOaVTzxI7kLKtau9NcG+w3nzi1IR59ZkqrwrXC9QYv150vr698+tr4ejM+XYx/+buf+el1UM3XlaSwdFjy4m7T4s7Mi+JJKtmplDF3jGIoIZoxMh53WjBdyOnMyAssC+QVTQuSFjftCuPEIQnTJaiag9vo7l48Ni/ISmHrDduFvCxs28Z+vcSELTw3oiFOB5CgUSzfDhOiEmkQqk752/ZbFFzG6+U1aHveLD+czzw+PCAI54cHTkvxCX73hnrfdl/7W6e2Th2dXAoPT0/HRny93aLJ99/dWkNSoix+D53PJ7a9eoGYEtvlQspCxgvF8vCIpMLNhO31FX14H8wepyXmkrltW3g1eCM2gh2RYkg0kyNybIx9jIig7Id3xb7vkexjnNaV3j1Srfd+sDZu28aUhfTXVxBxIEfEjemW5ZCC/On44zw0pQD/oxq4Ozoex0yKmvXD21jXg+kQhf39XrA3Q5NwlCcmnapO8w6zSf/2WdBZGEZqAJwxcQtGIwRLIUzKvLayew3CHK3H4GC+djAUBm4CB8FUUw2mqbPfjqrIR7I+OmnerA+LJjsXpmu5q+UlWBVezOa8+ho/pvzA6/eGO7SPoOxisynkaKc85WZxzfrikb5i4iaxFLY+2Fpi6OLMQS2INlQHK1PyFywDCwaea1td6ie4Fj9qIwsAfDalfTaQE8jJPsBKk3cSjaWNALZpAbRMMIeDdWhR06kN1LxGmlLEYb4HLKq0JJzLQl+n4W+N+MZOrxs/b53nbYfm8YT/9vvKn33zzNMqnMvgz759z4f3hW8/nCh58HBKnE6Zp8fVvRGGMnboG7S2YxVahdul8unzF8jC00tj/fEVUaj7hduXwfnh5L4+dUezumn3eiUtmWoNzYllLeSSeL1dkOwDk5wX7HRCJGODIxJ433Zn6GWfOKf15IC7eGQp5gyUQSenhQlTHTd325HeUCmIZrT41Nl69YYr5dgfA7gzIcuboWJypnHSGOiE3FRxKbEzePweEad8xhGgdTzVQXhGULr0ACaCN22EX0EFhjeFw9w3IWQkozs4HhZZvh7Ek9e3zVkRE3xIDhZILzBKJGg0+u2K7BWrDQ0pt9pw5kY3RrPDi8R9MYKt1wejuhSl2cCs0s3YhrlXx767yV/dHPB6vXLbKs+XnVsb3Cpcq/L1defTy85PzzvPrztfX3Zeb42tC90SIicHUVPy/V2cjVzUh4ZJC0kdHBgzl1ULOa3ocmKkFfIazISFIS459bop0yQ5hKcukRoxpPM5ibk1uIYpswhoOtbmYb7GppQPuVgNY8OmnZwzy7JQa/VhQ6zrvTV6a6zr6r5XARKk8CzprXO7Xjmdz8cQ5OX1JWSecng35Jw5r6eD5SkinoYVLPecNJjCiony2//V/5T/6H/5P+PHf/135J9/YG3iw8ow+96rez24wf3CUpY/YLHlSMI4jC0jfetIDIvazeCQkS5vZCHLYUD//2vjR5G7vjcth9v2pL5uvUUkS0y2Ao12faEdppGEY3wKEMJtASRoUzFXnxuOOWbZhqdUHJEc8aiIqJs8Bq1VxRc1sZn/HFpcUiDX983Lxlu9YQAYccON3oPKNVMynB0x4k1NBoUKSCwMkzkh9KAbg3QvNiSMYDwGxqUUvggYah01YSZu3OmEnRkBKvjEoQWqCU4RU4VzOfGQzxjG0/kBK8bjo8co6tRUi2/6yaAJMaVQNAwuRebSY9RRHao3wax7DI5jym5aZMTi64h9Yhb3FgtxP4qR44IO//xD9GC29A5tFNoImqgIvWf2ntmaNwHu9+G0/T4aX+ug7Y3aDbGdZELCWEQ4LRvn9ZXHNfPxfOObp5WPD5nv3hvvTivWK9fthXWF7z4+kX5V6KOx3S5cyhdaq5weH1jX4pqmNTNUSLmQNNOrs3NGG7HwA81o1871trF/vXG5PXB+7iRN7NvO6cFYToO6+SJd1RHpjnF7GQxxymZZC6Uk+ujUVt3jRDPWlHF6QFCadQa+gY3RycknNXOhLLkwzFMEcvEp+7bdqCrcxDX6dd/JmlGGp1dqYqTkBjDb2Ru11qnbzlgEU08UWE8PiCb2/eaAHykowaBLwtpOq93TEQRum/tRlOy016yZoSt0o+3ViRoRddQZMMJ0MRZZ68bl64sveLEeJIRtb9y2jkpmuza/l0Rpw2imIJlWO/sOtReeLzuXrXK9VT5/2Xl+bTxvg5etct07+xC2JuwjcZOMPggFf26WrO4bkhMlJ7Jmd2jORArGctd+IuE+LBiZRqJr9k0xnxE9Y8uCnlaXx5QTook6LCi0zoYwSTSEljppcdlF70YNs92yFAdhk29gS8pUpsu1r3/LyTerGoyPx9NjNCB3HXXvnX13PwtJiZJdPvA+vaO1xu12Y3SPNN326pvf6yut+3rw9PhI1vAKwdfZNgaXl2e6uS728fxAKYXHx0cHTvb9WE/nz+ScqXsNSl5iySfa6wUNicLIhes+KOXMOmmcpzOfXl8oZTkAsmVdGTa4XC8MG+y1si4nkjo7aLvdWJaFFg0e8fuTKr1392XAmR9TEtR6JwdFsJRCq87Mu1yvnNaVNiMw8aK5jkGKc56DQfKn44/vyNlZRJ7+GOBB4ASTXu+dfMAGFikughtBEnv5GMFE8obIRne5YqSiTPnC23SJ+3E3gBSCHWrm1FuRaNgHXSYIQBiFSZg8+msQTc4YXlfkrGG8ek9LeAs6EN4/JRdnNPYa8Wsjis5ozlVQ0ztgEu8zSUIjIagfXgVeu1mYw6G+BrVh7GbHufW1P+qyqOlQ17ObCD0ply58fmnuG5NO5OWMYqQTrOmMqAPvqSws4oC2teqAx2yw1GJq7WeoRVPlUhCOc+6JAc7q05DFdCANIilJMPOByQQAZl2ITJmluexSxgG4zIhQt2/qDkyLj8HacCO6IgkTxS0wE9187eqjM/riIGdtARI3frwIn29XdOwsqfP0eOHdKfGrDyeeVvizj2f+7Lv3fPuNuqG3CF8/b9yuleeXC9YTtQq3befz885IwgdrLKuxpgQt8XJtrOst6kFF1FAx1rORSqeNzdlfS2VJymW7YDRQI+fC6bQesYhTjtn67mbIScMTIMzrVGDJ7kNgiTZgyaeQJ1g8a+66b2bODJRMbhc0rVHHC7JkUEG1uMcSAvvmho44UJYMkqxubGj9YFFTO2nxemZ6Zkrc8x5JET3KiBSxEYPDqN+Y/UgfWOtuvshEEQwZRirJvUtaJ3UHBsxGSE6c+dmuLxDTdXXUBdSb6Nb6AawL3rhba56O0zvSO712ejUYwmW7+X6b3cDzdruyRKpSa81LstjL+/A40e1W6dVr7taV2165bI3nW+WHTy/8/HXjp8+Nl71zrYMWHdBAQdZIMnBmVArpZI7rl8QZQSm8mVQySGJYYoi4NCKt1HxmpAXLnsA1dPH/r9llOJJQ/JnZm6e6kNz3IElY6MzeUcSvHwTzRcnqEtN933zVVB8An89nRnc5zVIKWRM11aM5P8DlGFKU7EzPWr2mmRP/CaKauZm0BAiZREhlYYzB7XbzPlidUZlTDsNGT28EsDogdcpTZv2LX/Ob//k/gv/tf8opJCRbSDlzysfwUYKt2UNOmtRNUg8/LXVDSu39YHLmiPvW+Nn5OacPVy7F5RP/3r71P7Cn/qLvgsBzjRZGasSN7SdTWdaCWDSHvXu+cXME2TcbY2ZQOzhBUKd8emqJg8Insdj3iFIy1cMccVJZzeyg1RxMgxQovA10SOiP/GZOqGvjgl4tsbmmpJN2cJ86BI2QmKYNOl3vU4U/wNpTMAAi+1l6Ra05c0CUIuBmeZMKGWaQdLLcQY+J4s9ECYe47ZBnuQt7Q0SDhthQ9SmumXB6eODh4YHr5epThV6ZsEUfdgAoFkkaRsJITuMS83UzChhVjXOQ/HdCeEi4cZHggo4eoIc/s4pJn5CuN17xv6gEhOHveZiSEpyLxvl1L4s2lNYztQrdFkAwtxzGupvx3GojqR35s9vobHXwdevIq5GkcV6Fh3zl47rw7eON96cVTYnrfuFchD//Vvn5i6HSaNuVh9PKUpTloqxrJatxWpKTMHLmtDqKTh+MNqjXxn4brp+87Xz/+YXWK9/czuQfX0hJaTZYlsaSXxmj+hMk3hhrUfQUZ6R3VIPaJuaTfHNjm96FdfXrGKJFSkrUfWPR7EXHqIzR0Eh2kLSyrCe0KHu9MfDF7LSujDpQazCM7eZo6pzkbucNSe5jUGNSoZIwBvv66nFdo4EmenMzyVIK41TodK6XV0CRlHh9vdGbT3Y9imn3orH7xMSCqeQUWWe/3G4VSR7jaSPz/NULFE2J2ny92Jp7IQyMOhafnjfjcmu00bjeXrneNm6b8bINXq4bt+pI/UEdDhqdavH7NHT9yzpRfgELxD0pWQc5Fv+k+TD9E3HaMOKTTzfxdFQdKaik2BhXds0MKV7oSj6YUF0cRJUABwYZq565rCWTwgfADYCENiIyNECEVAqSfUPB7PAmEBxAEpm5zsOn+OcHpgHQGEZvzmmqdfcmf1m43m485czduV7ChKiT1Zuly83BJhCWdXGpwBicTqfjPezbxr7vLk8ywzo8nM547FYhZWVE424i9NHJ+UQ6r6S2Y72zLg9ofmBoQrpHQnVRPnz4QBudre7crpc3bCihV6dwP56ffJMcw+MrW8dymDmJE6U7HHKLFmBEKcUbupJ9rRqNy2UP2ZFwiujZdV1ZVy9oVZW674eTefmFm++fjv//OxyQ8+J/0t7hznqUaC6jGvA1IpzABzgAABA67xlj7MBaCu2s33tusj/NFzy2bkTNw/A6xfvWmFLNAYj478HcxUnUX3/Mme28/Y6UiiBlDwccnFXqngnR81ByZrrz+MTYp64yBOkCMXENiI4u018hXgDxBigpfTQHaKK+SeI13Ajp5BAC8HP2ks7XiCQIDUNsugMVXQa7Vb5ug9Mts+iDD6xqpxv0lKmhDcfcK6gPN6YGo8fk2t3+U6SGdB8UkAhaAUbDwjwcFRLuCWZUB500Un5sEum92U7i54Mwne1mSFybWWsm1cMbYB6adNroeD1tHQ/fScyTZ+ZDmAV1QGtqxJeFfSmhD/eB0O3WedkHP/28kURZf9j5eE785t3gH72e+e1l8HRWWt/4/e8/se/GbcB177xcK19eXnm57PRhPPxu9+jx7LXw+bSwnApJBknhvGafsJbEoR9XY8nK48NCyp5QtaRMxshpY0nqJtsZPAVLgM1PiXk6UspuvL2sC0txQK6PRlMoGQaz6cn0IW5UmIt7oJ1WynLyYVmPlK3k6QRaXN5mrTKSa+zH6E6vr2emhNCTn5wtmurq+/u81gfd3EElxkCGoIvv09acVeCeD+EttXdo3aUdkRoWSwncvLZn+DNCgAxsHENdrlf/XcfzFgwYcxq/RQOdc/KBRe/sl0rdBmMI12vly6dXTuXMly8v3LaNp3cf2PdGqxsP5zOiwsvrxYGZslJRWh9ct8Hl1thunS+vNz59vfLp5cbzpXGrg9vu4GSXxJAVT3bz9VBTRhGCeEGWaZzsNZRGCpz/Obs3FdmNGyk0lJoUkwXTlRF9pspCKmeX5YiiuhznMyXlXBY83NWf4Y5RrdEa6Ejuw6AZNNLDWgvfGKP15rHvZeG8nmi13a+VWRhki8vY1a/H68sr+77R+mCETALgNjyNSlPifD6zlBKMFDf+dOZ253Z9ZQxjKZmyOigjyQeOsZxFBGtn1I1cdoZeGMszfQyeX7/yfFtgPbuHHniS3lxW7P7n2u6fx8JXoTfv72prtFrZwksr5xzmkb525eKpNZ664qDLL2Vs/nKQYfQoWsdBzU0pe7EWGteEUDT7tNP0aELn1Mg3mfigmg5N4x/8HgutnDpledL6eg9KTzTM01BoyikAetAB50bZA8kQTb7RmcfuecGsLDkfiJOZuwpPzwYmc0EIsziD0YOK6LqkEfQYb6IFGT00Yd6g+EJTA+V0QcZBiJQpHRlHBTMnfDrBDFHXtOG5zoqQ1FgXjYmqo06tdY9pO5253XY3plFBszv7zhghAoSZi5WElhs87cNjXO4TBW9iJ1Dj53jE9MJGNCrDTfU0ztV0p5gjihx6Ft9I+jHFNggdHQcw0Qd0TewCI9gPZPONvxs9K+eSqDac0m0BoPTwewD6cE3Z69643G78+LyT9SVokY0lKR9/3Pj2IbFkoffds4dPKw/nCx+ezjwU5VQSpzz45t2Zh7NRwgm77Z1Pnze+vOx8DVObH54r+z54/HyhJCGrwzu5JJYkvpn1QV4WzJQ6jH146slWPfYoKSzFXWU9CapzvVyclpjnhEwpxR3v14jEbG0DujdHkmm2obKTiy9mZv78LevudLwhB4NmyQmVSqsb58eOqLuQH7o0ekymg/GjA80JxiCrUUrltGZv+K43d6tFuVwrtQmqjnZq8thLXzDdt8PvHQ55zW1rdNvYqrMSrhc3BWo1IpB6p/bGvruytY1MMwcZWg+vlX4H5RyczJhJmHzG5hCFR1bnqY4e68kQkIaSGd2nkjLpfSqIuCaQlJ3KKasXKJrxPHZFIm7JkrsaW7ipN4RmPu0Ye4dmHk+Ks7lsQGVDkk8LO9Bqc5DWHxwkJWp1ScLcNfbe0Fzi/Q+WZfWNJtayFkaZZsY1UOiyLM58GJ7+U5bCaVkPhsOcaqbkoIzOqEYhTJ44Uh5GH+6yPAaaMzm+3wu2djDfSinexHRf3/to7kS97z7JSC4Vulxe6bcrJ+BBHFSqtdFFaSmz753bdUNKdqZHLnEOWzg2J7R4jO3l9fUA0GSMw6h3gslNgpk13Nl5LYUdDoR+jMH1dgERHp+eUFWuQR0tJfaWKDxaa9TaKDmT1PfIPx1/nIeFX5GRoim1o0bxOsHXmG5T932faI743umKP9t1JqgqM+KRe7pDHFPqOfdNH7jMIYofcwKm0wA7mBJ3Y0kHEJlLRDQg8/cM86Ykq0YdEk1yTO0hJBhi4UthBzN0jNlYe80Ff/D2cVaaSwdlDPLBYrQpVmCg9EjHQV0/baLR9DsF3aeL/l+zYICI0iVRJXOpxs0qKobmQh/Gbe/casOCMtx6YxvDvRoCJvABdDSM+ibtAp8qu5mPy4FtMjwkwdjD9E1oozsWMhRShD0FC0GiLvKBjJ9zNaEHc2OuF9NrQsQNKY3JavErPJkOc2I+xMdBKh5X7JfEvXpqEmrUeAMjFyVvlb11eh9camN76fz8+sLvvtz4JlK8SlZvOPfOpXau1bjsvh/04Yy5LS+UoeTqz0TaGunV9f3Ozg32cBjG1TpltV6jqIImI2cJzzHhYV0oRSmLUkryqGIVijr7Z10ya1E3GC876zKimWuUjP+cjvATMFrtKDdvgOg8vjuzLK+YGjV8QCQLOSvLsjpdfgw0G717zyI5I4+PDOv0boxUSDlSicpC0sUbwgEWjdUIAFCi7rDiBoyzv0klBoS1M6rHPzJcLjELlDGfz/ivhkFRazVqfwcEe+shQ1JkRn7G4fWTP6sllxjYwM8/feXr1ysiJzoLXz7vpKT8/POVrXbODzf26lKl00NHs3K5wLZVtrqxd+P1uvHyuvNy3bltnVsL/xOTGKisUDyWu0QdJBE97xN5PfqVHIMu0uJ1VM4Q3gskZzCYLHQpdMlUWb32QyB59HhXZ0KLZDKJRKLjMkU0BN9vfTesH0PqYV5HGKC5kJeVHLW2qJs5t1a9Zwr27F537zlaI+V8GEbPnqkUN8VPWVl1ZbHocYenVyVVlqgbemu8Xi4B5PoS3+rurIP4O0S4TRZESljvlFRAXHrhoJNyve788P3v+eH9Rl6+4+vXL2z1iW2raC6xKPl+0yIu03B7g3VdD7DzrTRiek61YLjebrfDc+F0OrHPz5QzpTjD1mLr+yXHLzd+DFrORAbMcmyAIaFQN0tpzS8mIljRcFgVjwQcftO4PopjJ5x7q08OPNpR1TeEFL4Lokq3Tk52XKgYLTAzeG34xp/E9bhJvdH3Ab3reFMqkRvvFJRuc94/P2cYNFko+STkARb+C4ojaurbOuEcq2butqtybD70FhurF8NmHheIhPuygHg7EiwHP8/ulO9mSDD9KryoSEk4n1bevzuTiycsjO6IuWkildWbuC5IckNBiaZ+TjaIAkKCbqmxsRPF9bwgSdwAEDGy76sc4RJi4cgrx0TEp7IDHbOouuvFCeqhmNPU5vmc7+duiOMLUqinvLCSBNknCSUr3ZQxoA3eGOJ4EdStudlg73G/TgnMoI3Etg9eW+fHVzx6pzdS7qS0syzGObuh5DenE9++W/iHv1W+eYKH7EZAzy83vv/0wpfb4OdL48dPr/z4utGGsyDWJOTYTHJxV3oL0yzSBfCkjzokmCEONmhstBITHcP1WgQbR0zuYyfCQSSAG2FmNQtjQIp7cHoZ+CIDY0jQ7mDNysNpIQns246Nn5nWzQMOo85h4a6MIRlylji3FoCK57/31unNi5S9BQMp7tlhnoYQ78Qppt2BImfZBEUvDBoHSm/+XLn/RwCB4ZFiMtM8PcJH1bG6lATNfoOa4tnBKKperJDC5Tx8RGx4JvwwAnzLQGIkCQBB0SRR1DtzwdQBBVOn61lEUiLp2DStLD7xEpeV7MNw0ZPnNSNgzYsyY5CWBesDsYakxc3NxFk+tbUDhJPkoGhtLcwcO/V6O9ZouVwiPSId4EAqmbVktDrNbwTYICrRFOshIWitH+DmCHBW8KLd6XVCWkpsTC7NIdamOe/JAVTWOqOXdpKFnHWu2WNSuKH2xnW7eXoKhZKSU0wRJAyH2oDah4OKuI/NNFoym/uSHQwN3+g75/PZIzdjl4kV27WVMek64izDyDHHOVERcn7nZlDhKN3ivr9cLoeMYikLZSnxnI/jI/7p+OM8XEaALyjzhiHunwk4HGtxsAOiMe1m9LgHJcy3JE0GJRxD//miwQay4QZd89e9BRZsvif/zsPryIcW/hp3OYPvd3dvBbw+QnBCYUALIg4+qtdVk5fhwMdkbgSzMxph1YSUzKy53jI//SPpwU6dr9mHMw2yJjQLkgwdGsCBr98av7uEW7ngwPhozdd+Va+JSGxVuCZjTRqJE4PaB1vv7LNWas39VvD1d1jkKgkx8XB53pyy+hR+YBayTwxSNCsjNPrmtVqSRJLh9eA0Ew9/mvv58z2WWb+FLNjvn3FcO0I2ybjHxM2SWPDkL01KnglPhKyY5I3VcG8gN82FwUBZWFKmdaON4dHiw2itcjHYXhrfv3xxh/y9MSwi3TWBFKQ4+/a0LpzWhTUnP8+9hgok+b0+OhPmim4Y1X54dNz6oO/dvb+mfxiQdJAmfdf8c6rNutJrzJIcgHDHf/98qsZSlPVUKGqcUnIgAihJ/DVt5927ynLKSDbMumv9M5Qls5YaMkc3FR7D76+8dB4ugolLVRFvILf9hhis65mciz91UQfNvUuGP7tJBdFpEOiad8z/f62VrA5euVyaYNx4j+GzS69xDaG27lGrUWhbb4zWkR6S41gfTBK9C1ttbHtHtPr17MKnzxtfXyq3Nvj8/MqPP17Zm/DyesPNiX/m5dYYapyenKHSamPfGn14b+KARYBfkjAylsQHyzmjmh3UJyRAAVyqRHx8RD4mCT+EFJ5T4lIQk+x1U7A8uyxUSXQt9LQSdql+76Rp7p/owR51Q9FMlxTvDwcZzNc/TBi9OeBmwQrwORJDhBZs1WOokj2KdaZGCZ5GMaKHdKPT5OtUsBBEvN7pIlwuV2zcI4Bn3OWUlM1mntn3RE0kYbDIrKGC8VHWNchcwwGQSBaSbvyX/8X/nZ//qvHhu3/A5y+f2Uuhxe/IaSGnHAxpY11XB0IlkrGClUAAoqUUTuuJGtKjpRSP5Rb36epR9+QwxvbeOcA0Pey+/78evxhkkDAyysVvLokYFZ27pwX642Ntd9uVxGh+p8zJfxJ3OZ5ln8RNamLBPPDrrjMqkqD2JXfm9FipORE4HBX8JgvgQGMhG4EaetyeG7pF4E0YD8GsJEQdXBg239l9M48Vwc9D+EcwR5DzK+oC0RxT2zDqCFrXoIcHwh2xTxjJxtHsO0zeYXhjM7CQifi7nPGTuSTODydy0ajZFYZQygNlDdO7Pg4jlJTFbRYYWG8HswI4Go45XBAFIufaH3K3P8oi5KS+YBINZPGYqNmYeO1h4RUQe8ncWG2exnAGvrvlMMxjgfrUqZLi3/zn1PBzqqAoGacElQAZNKYwfoUS9DQHSKDeiOytsR0FgZIlubHNVh2p642+u99FwljllXfLjd/9uPPNu5XH1al7l2vlx+crn64eM9WrMURZTytyeogkBzfNtJwZxVHQTjuALPOoDl98EadXC848AKbZUcEXH5+eeQkyjbJ6NKg+9ZBYkn2zk+znr7V7cyhtYkwxMdsNvbkvQmtexAlhehXPoN8kEp4p6WD2zAmfn/ER2d9Okx8hc/L7P+hwWQ593CzAsAkNzefN78Ms03jRmJFSGlFtE9AzIvIoii9RIZngGevxemKHm3JKQXNL8/MrDKU1wcxpfcPRRIf8zK2CJEnEmCoJBxi6KCmAhaEacWkrJp7Z3uMzOm3WgTdK8ecjLw5QWDC8cFS8VdeDppx9spgLDS8y19C/jWhgrFdKxDst4lDczC12nwWO6eYACI1dLpmR9ABXDUM1kXO8PsHKSjm0i74BYg5OuVZaDrYTUWi35kwZTQkN7aEDSh4XWdaVnJfDmM6BNGXbb0ETLJzObrTolMDqhXHvlOwTijaTNlIK1oVPqvbuAMw0W0rJwceVhZydCmgBMo8RjdhwM7iS0sG4EhG2bTsmEOuy+Pqm96njvm3H5OF88n+fEy03mnQ5i0+d/oQy/LEeHphlhKAhAF8/3tJDzdFcjmlJNN4uIfViPEfE2ejhKs98NnvclymkEGFGJ28StSKVR5Le64AAWZfk1P9h3timlILmPysijiJSQ3c8Yn1NMoWMhCzhzjzs4543NQkQE+Q43p8XWsf/hzumJqSYKsY+Hq/fu7msc8y93z9zGG05iy8nsqoD1d18EDFrrRg+bM34uu08FuV0UjC47Dt1DCyviDm1vtqgWfjQKMRoL1LBfCgSHCjfL2SECWRQ5vF6kRz7lCka0ZdmxMBsvKlhwh9sjJCIxr4m0ZjqPFd2DHBGl5hCj5Cc3nXiIi7vnVPhydTN4veWAqYuEU7xkq5MdumfdTkArzagm8fqOnut0bmBeRWeRTB8Iu1AirNFzyVxKgkVB2DcayPRLfZM8WGhiANfrTf/vOLxjb03H+Adj4wPMKLU9CFRbxD+J3UMbr1j1bDbvOTO1hx+25PSjmoYi+KpbGt2Vq8/Q6/kVUjFP0fOyrokjzcvhaKJdUnk4nVLycq6Zk5LdWNz60hy6UGtG703Hk47y5JZklBKiojLyVqRYygGMFqnteGyySEh56wxjAEY8chMc1j/7L17/9OGsNfwYotml9GgV6xmajeqWNRbyq0at9q57c1Zfltnr4PrVrl15ctr5YefN76+dOrwus8HoL66pQXKqOhrC+BMycn9p1QzkgcuzgnmQbKQPEwz/hT+cxJ9EYjkAK30GIz6zyaGZIZmhibMnL1AOjE00zTTRCFivdOsiYe4rMLEG35fQWidAH5i9YnmRZxC489QSpQ8GHWj7Xs08oPadug+UFjXlYx7xExHvFmH5JQOT4WZRmWBpKaQyDm7OPH4EIla4XUAHMaIs5nvtR31RFocxNlbZav7m5pKGOGRkSV8NyyAh9OJU3ri3/yr3/FX/+V/x7tvfs2N39JzRG0G+EgAuxaDpt4qBKth6/2INJ+GwpI9CSOlxClAiZmmUfedvVZu5mDoEy4VLaUwWv8f2kb/4PjFIEMKZoFITOktaGb4Itlrpw03yeiR+TksNrTk2v/Rqk+awjfBaWl25Eojjo76guGL+ETCfLqkzmCbyGhMb2cVMNlwphNpnxMu3+gmNDbNhXS2ZnHhJ71xbrJO+Zm9ltPJ5xYi6NE4QxQGw8KfIApb8592o0RhiGc+I46Oh/0jjNlmOXXep83e8JlayDgsCgYj54V1PTmDQ9zdGQSrHZNCN2eOpLyiukGEVWpSNJA1f4NhujgCLRdzyuBRqEQ8U7Au3KAtpCHRSGlQtnxoEkCMmNPl5IBomMogB6f+cEItYr6JqxsizSnLvN98wXFtoMqbRjfB0Hm+474BZ0tZFAgxna9ZOVnyps/8XqqLR+qN4eZ61apPS+ugjs7narx+qvztS3OE1oRajVvrNBVSXimrsiTldCqcTyvnnMlm9FZdV58UK9k31LeFWSzKKclBlR1ROEoYeCWRw3/Epx3BrjF/7mbR6YDVlP3YYXAzadsT9JqTphHMivss3Y5G3g7DrpAtiZKkHNNtZoNsDnOICq25BnK8+Qy+ockh9wivrPAl8GdmggReTET02pBjiuaNMME8iPtyZh3fU6b9WbWZ8CKHftrEn9EcLAbUzytDseEQD6K0rrRoYk2yTwRM3RshO8VXYuO0lLw2Tm4SmAzQcD2ezb0khjorY7KLDOiiqETe8wQ2NR2RtIRO0UEWN7U1G5SyRPPR2Xvl9fXFzT3LgsSmv6wr60w3AHLOjN657ZubG5aCAFu9ewfs+866rOz7Tg8vllKKS2rEjWJn9HDOZ2eTheGviGvNc6xvx4blF9SvULgYOxCk7NuNMdxgaPrjONjihWmrlRRu39UG+62Gv0hG8U389vpCM9dspJKPvOmZ2axRdNhw0GIi9DnlAM6ENfSFOWUk5GDTDKmPweV2I81tRWBZV78XzNkZrXefQMTPiPi0rncHKaYk8E/HH98xYi80CLBMmAtntIl+vMGRdE5ILcU9E8CoKnQHtaz3YK05WD4nVrP2eHv/yZs9YoIXUx7qnip3w0jDgYPe/T1OB/BZz8xnb3QfhrQewIQodTKRgv00wcrW+yGP1fC9mnIKB+WCXXecC9+/bMwpvh6U9jEafdTw8ZqySwcYRIykmSUlSlKsd2c6qidhCYSposvFUhKaCFczbteGWcNJAOFz0Y3ehCaZkXw6rmMwKy2xAdJRXO4n+LUZyYHCgTNNvTyK9Zjku+SUmU4Z7XAg6s4+vAdiwmTuRVNtU1oQ9O5jvfRaJOiizMQR37sC3DGLZn5mpL2pT4+adU5OB2MoXQZ9+N870B2GemNQayKhbNoYb+7hLAE6KJxL4mFR1hJNz5BjjW3m50LFmcsaqWN+lr1G7U1pw+Uufvjo5F6q+2u27jXGZHQOawcjaJrlea1CSFDi1YLZtvXO625QA4VA6C89XodgBuvBLE4ilPAiMetuTr0kZ0NotK8iaPJrnZKwlExWYSnK6VxYygyNtWPSPv3YiOd3zh27SUiqXQLbzRC1GBbizM0+vOYZGlGQsU5EfdYZzoocxra3YPCOME53EKl1c3DO5GCx7EPi34RxWmI9iH4kmECe8kDUmROYcdPBpD6QcPaTR9FanEcLUPWAK0UQzaCK5jUYnnOoq5EAEf9NbmjahtIsYbZAWjHXCKMappcB3rpnmz9LIoKMWbXOXmvQRoOUyKVg5vdyLsWfXOue5JGUyb6urbIsp6g945qLkrIeddK+NZdH9+7Dh6hjbterm+vm5PKDnLjdbqzriYec2Wul1uq3qrmXQq2VJRjNS1mY/eGMh1zawt7dnBHcfmDvnSzp8D9IWSlZedAzy8ffkJfG07e/xvZHfq6FPeSiKdb6AwSOAfYQcZapCLfbjW3bmCEK54eHN2uI/z5d1/vgJUCP6+VCq5XHhwdO53MM2//Dxy8GGYbooTvTGK2b9YiVa7GIJswiUzTc600Eaz3Q2oiFUQH00NETC9dBn5f76tfNsD5ofTD1bMdqPjcDYo1J+mbng0lVG+Y/bz0kFBqFMdC9MyL2ifiaESvhH3+AGSOYBaGHJOqP+KXj8E64MyIER14dxRWf8jMjepwRMtS9IpThJij4uT1uErFDSnFaM49P7ygx0Uul+OSzNcwStRr73gPpU9ycLvSgiE9P0tRW+yZn5u/HWmyYsRxNqr6DOYoNYuI5L1NEk6bZ8obmMLYSPc6/G1QelyWKGQugQWMSQFCdx7CjaAKwFNKSCSI4FoJ/QgcXNK6bHNfE/zAZKkmVxZzRQXxv60rJ0x9AaeY5vr3dqaKTLzNGYzSfSJXFC6OUEkWFkuC0ZM5FOGejqHtKuJRqMEwZVo4mZ7pMu6Rnynre+F2Y3yMzBgze/BsBMshkEvg11TAKHRbFKbN598XVi8CI0RqzSJqTnDipYoyRHciJCYoGYj2pfQZHMUgkf7Sgv3uhxPE5BUA9ZeKYGtl9k5bjyfFnqMfbmhT4aL3/4PvcINPBIl87vHiVeH8T/PJvToiFt4sSmx5EdoxvbBI6tm6++aGQMzP1wdz8BEvZPR1ywro5Wp4SGdcJjhTeMN0p/W7EFBt90KchoSl70zrXKnG5iQVYtIdXgiafiTVfUrwp0cRyWg+34G7jcLyugdRP+r/HVXq6gvvO+Ga6riefuvfO7XLher0wKdetNW7Xm0s0zDitJ09xUKG2nd4GeQnn43itdXH98G3bwDx+VaMRGmOCW+5LUkNvOmLjns3S9Xo9jBSLQBrODur7fmcbJOHp6ZGhbqraR3iHBPg9mRBmAbCE18LlekER8tOTFxAifH1+dm+FXFiWcmgNl2Vhu91cFylCG04JXEqhhmQiF5fGTP1iq9WLAAnn8TemS386/vgOk2nyGoV1FIRKMAJs+PBEOEBaT3rKR1M15QejO4iv4pPl2nqwCaYBqR4Mn5lMMDW6rTfqXp0GHwW0qtwnLAf2EakPsT5MmjXRmEhs1u4qbodsdB8taNk+odc0XB7JZCE66Kwpv3k9B0JSelN3wX24oHNo4OkK7v4vMFMnCFAYc9nqNM0c3hi13evI8WaPqs2ng+cA+mRZkCyc10TdK/u2OaV3NK77Rq/uT1XySs7DfbJ6JY2IF8cB8mQh4xz+u5yB4lHRDpBn/+7m/hzez/j591hMn7Kr2CGpm1JEEQdEBJeiHbRvC0AfCa+BN02qCklylDwuSzYLZk2AUq4vj9hQkUOmQ+xn3ohxeFqAkWwOkKAPJVkO+ZpEnJ9vQxpAfg7zxpyVXILR1n29V4XUXZqRxPX3KU1DvxQAFPQs9JGi1uAAF2ZEqNcGk5UcFdYwEDcJ9rSkYJSY/AHI4HuJ75HTGHUESG/EkGNEw6YxwLN7U7p3nCxsMHpn6W6kPswjsVuvSDSjZvNVFbJLJ/8AXGOED4EPjAgfM3+gA2SYawGzzorPbw6euH8Sd5Bs+qkEA6ciIREY0DvJfBAhMuLm8WdUtLi8MRl0Q4dRbLJk732RyDQ+9to4MYeX4VWVE/mQtPeIvXTAYAJtB8ipHkk666NUTlh+wJ+0aAxCXjrEPRRQj2Vt4h4DpExKC4ThIcEWDZjVGYPxHJj5Z3Vpg8tL2vDeZdDpw9fVbh4ZmrKbcZ/PZ8pSGBi323b44rXeqJdKKZkxjGzFJ/kpUcqJEhLIWuvh3aStYXhM5NdgN/bevf5CA9DtsYY7wNBqpe0OPpSUWdaFrImUEyl73WHVocbRmoMt5mu2p7l5f1NUWFPh/dM71uU3PHz8DZ9fladx5tpgD/+O3sdh+K0BRi6n1feC1jwtKyS3Ywwulws5Zx7OZ7+rx+B6vVKWhad37+4s0Bh2tt55fn4+2Ff/oeMXgwzNgGGUkoMG1V0v1N3WR4FeN984a79nhU6zklhsizqY4AkIPTR3QU2NhWjYuKNkJpHfeqf/za5j5jnPldKBA6frE62MMDe+FJvwna4cvRpKNAgCiC9eHV8Ajg1BYpah6o05YN0w7vo0yU69VrMj3jPNRpnQa9psxEOjPUYkK/t0IYmRrAXKNOg98pBFWHMi5wXV5Nr7Uih5ZUh4ESCs65k2hOuXC9u2H0aZvrZpmKEJ4AWA4hMEC4RwSlQaPeoZ31R7EnryhjHHVAHM5QrRUKZA373OudP8TYJKpU5Vn2aE3h2OY7OdES5DHPEl7oVJY5r6PRio3AtA/1xBV3zDuOnRVKtMScp9OikSiSPW/RyNQfcREzYT6Eycrgb04Rq4HvQ+v99A03CQoSROWZ0aNzfEMPry+1ZiKCWBnHKwdAQLeYweBZtNzH/iW4YzgI72HCarZm4EBLDlfzScyaFHbA0jDFTNgrEyNe0+XSMKHzdrTN7cEsXwIExABSnZJyxKsEamlnnqFSVowC4DyPP1TO/GjAEmOe1OjwKq2TQBm3j//ZA4h/79coBJgst7RhiCHlph8xlLkuQgV3KkfEhi78Y2EpJXkBxxs34e2/Dzt2gUb5qxtEBKSEnk5e7D0IbRh7D37p4Ja47iySec7br5Fe5g1ugitD4znb0Yz+qU6m6GpIJZPRITpvHZenbjqr36662Lbxp1uF6wDY8Wmt4AnsPsjf3X56+cTicQ9844n8/k7A7PJRroMRoa5+m0nBy4qI09IizBn0/rcN09Cut02h2QDX+Cbs4CsqgsHf33p05VWRaPX9v2nRxOzDPeuNWK1Z1rbWRcG5xTRlPh5fLKQDidT+TzifV89tjV2pyBVCs5qRslzftEhfJQeHx45HK5uCeLhRdD3E/X25Xr7Xo0diXAhvP5TOuVdnEkHxHWdUWWhVYrwyognNcS+5cXFqUs/hn/dPwRHz78wO5MPA0PJWzKGuPLQofemu9zmqI+Cfpzd08CgzsNmaCyyt149PBQmAw0M1QyJWqVewMrd+rWBOn1zmDrsQekABrmc3tEqMWApEdBkMri+2hsirW7phyZca7p/tp75e6RIMc+5cedNu+lnjc6XgtouO1Pr6AYTChgg60GuzVkoSP24tGHs0BEKMvqEjFN3Lad2/NOeyhec3af9O4YFUOys85yJA60fr1P3nOK5DKB4abGDE/nypEln3M5ivS9dqeNg8c5d09y8tW7eSKAzL2YmJQHuB5N3WRE3eM+HaQy80n0uCMNaNw/YnF+xc+RBbuV+LwTyJnsF4kBAQFUo+kAemVEo5GETMgeksDOcc5n/eoDkIbRw1fJAXqJ99f6ZGvY8TUHfzkpJXsaVe0DbT6Qm32I8cbn42jTLeoUkCwHA2cMwYcoMdiY6RXRD/RB3Gccz+rsCXwYFkkszO8JpGPEfSjTnLiTiwYlXnB/7s4gmrRu2OhMeGpew+MTiIZ30iBsFfwzitd0k5ExE2MsvFzm8xH2W3ELWfhwBGs7eSucZx0n4oDEgRZMlioBWIaJ3zBqD/q/ORPJBB9aTnCAO9vJXzNAQHWvA6/FlZyKN76IMxJsMpUkvGgUikscuimWzs54DIlTV0FyAV2wGFT1gUd3B7+o+yMIw/sD93hIAXpkl92MQVfmtIUkiVyWAKsMGUrtPnRImh1yGANrfi1a+LNpTrx79977u97vciwJU2sbvLy8eq0ate6yrm7YKB5vLAr7HlGOKdF3B0F7c4+TdV14fn6h9UYuhXVd/VxUB1BrbywsXLYb42Zei43Cy+uLJ4ZFnVRKgd7ZtxsVR+tuqtSxsawb3zyeOJ8fWFon98S+XZ3NsWQ33sXZKIgDF3uwK8yM07qyvHvH5Xrl69evpJRYl4VPnz5RW+Px8ZExBj9/+kRKid47756ePCVjWbhcLpzPZ84BSvyHjl/uyTAdLoeF8Ys3CE6r8cljyYXRB0sufpH78A1Vp57NN47ZkPhmJcdmOOkik7ahgcLNaCLwhZm3AEqgiNPczwxIgcSm4u8zFiLj7vKbstOd6QMb7fh3X6hDKxmT5ForLQp3ES86iMXPl5NorOP9qnHkCXtk4FwEFTXf1HrEz/RoGXNefLGxhrsgN4btdEuuQ1Mv8idtRaVzOj/Qq1HHoI2KSuK6vfD6cuF284mkjXvB76cwudGM+kNk3d9BSgWJldti8t1jAyuaHXVTA/oRr5I0U2Kq6dRNp2/V2o7JtgbldNI4Y18/GsFjpWQi++E+31w/aIAk14ppys56xxkyEJ4Scp8kuEzA70dHasW3iD+YjEfjwyBpeBCIkO3e1M/7UEixUSm1GbXfN865OWQNIAP+4PNNnwGLAsGHCnfH8Lnhzs1GJziAHZT0O5TgruAzZWWeTyYYhx0o+l0L5685kVBL4R4+xMWcw+mqKh4TZtwLGlWX3Mzo2RnTlly8CBgl/CeOYieuhz+C4YbN/T3ZCCdxQlfY5r2nUYX4ubSgfOXQqU0ghAmvmIONphw+Hv78BrhzgI96XNuHx/cMybzsgx1F1hWxTBdXeOZlQYaRJTlq3TuaU4CNKaZ8DuhIABWSClWMoa5TdTm3kJYTJkZJhbw8ODMhUOM66X5R5IzeuO0bpMT1VjmdzqDKvnsEZCmF5XQKppJPaRBh23YHcaOBl/83w6HadncHPp95OJ/Zts0nqaJcr1da8xSF9byyrIWVwm3z5nnbNlLySMt5n03Eewzj/bt3jFMUOW/kPG54ZdSIReKYojlYkpIboS7LwrbvvlEHKHK5XNhuN9acA3xJrOczIsLj4xN1+FRgG53L7cow4/zwxLIulGXhdrnw/PqVMQYPDw8kVfbNgd7zw8PxzI3u7+N9Snz+8gUJAIF4xvrwzOq9+rmvtcbnHn4t1hUVpTWfbswIzONzxXTjT8cf57Gs68GIMcK3wDgSWXyirFgKXXUc7nvgU9aB30dbrRDRsHM1Oor7AOcUdzDXlGNtBZlDlYgQezsxsqhfpsv9lDnMVBcJAGJOdw92H3DE50Vt440fBxDt0cnOkBqEZt4cLC0lHwVnmCQdTVfrjWkmeQDRcpdqSAyI4gS4oe1obnY5Bq32KPqD1q6JBDFVdYPG2AocSFyVYd0lUzkz2o4anMriVOlcnBa9V4yC5sywSqN5tHlM/Y1BN28OTH3PElsY7eaDh5BVpiT0UdGhlFxImslEnCPx3jA32LaZOAZjuLH0cV0I4Hv4yfAzNO6gUYBOadbFolE/8WYPlDDUjt46/NHmkjNisAIgKcx6k8RQyL+cLenmcDKpnUZ4KoQHUY5BUgzG/M60AwibNdekXPfh0pSkYdYYdbuFJsN90OZ9a/HsBEih/toWPhoi46htYNZbEkxp0D7BrEjumtcgZDqIHMMMJ4KU4xy5xFdjD9aoGwGTaHwTR1LMuDMaJiAzzVW91p/67MFkkRjTZ2F+3h6vEeJWCd+rHuyoGIy+hV5ENYZO3Gs6jhbIQaMpH5D59/M8Q+1C7QFmRcNPDJQQdVBB1CXwNhwswf2onEBk1DaCZpsguzX96AK40SI505rLNERWyAVLj7ThAAlJ6SJIxIUbgiaXIvTq7y1rxlqjB6DUevdBYlaKBLC6LNR993M0HFCt24Ztm7O4gw48zM3E98vVwTIVSnY5aco57hNnPJacQ850P6nrGgbOy0LdPdFsBBNhJi6oCqeTMx2e3r0DYL/euF6v4dmg7PvO9MnBjNvt5jJQUT5++HBEQZ7MDi8ss8E3Hz/6/489ZlkW2rZTcuHhtJCzknrjqVdO1x95/vKFv/36r/myvOdVzqhmrvXK3jaent5R8n3YZYTB4+l07D9mRlkWzqcTL6+vbPvOx2++OQYuApxOJ7bbzSUjvfN6ueCeW74PXK9XfsnxyyMsIWhf3kS00ZHeKCLYEHozWiTAe/ShPxyTGqiBxgiQp75bvVmY2Q5zk3Ww/o4QT9feST2cBbdvqPf8T1Ulryspl4Mm75o2jonpPYImimGbUoS7xhGc3t9GOyJATssZQbHhVBl3N7dA52NyGzP3TjToVDLdPSWMQH29wS3FkfgJQvv7cHp7t0HvAuSg5EOSTu8brRt7M5YB2tzpeG+VvW683m68PL9GMzHpkyPAgXuxkszzZV0ikrHkzc5s4lyu4AkNMEhLCg2/q8RydiplbU4RQnxKMRfTlNxICDgW13mkKGjGm+sAvJECRMF2VCXTV8MOR1feADqzyJp6IwufEDP3A0giiI0/+F3HZjsXMq+PILaZ+S1JQrcdiGtKndSM3u4Lvj8XPnlXXA82vWgM0BzV0fx+mXpZZzLk5JsnIp5VG4iqX4cAdN4Y3Pn5dS26iLhzt6ajuEkpRSypgz21voneUS+sDHPH7Cw+LdLEubiDcu9hXiMpaGRO2c/FUxn2Wt3vI7kB2YwzxbwAmbGCc+ah4UBrc8NLbj76dmHxutk320NWY8Lt5jTkP7hGcxJyjADCO0ID8BCL2DBDJDENKy+TtbAqdKWNgukJs8K2N6RrINYpDFVdd+8FxkA0oyXThZheuClPM4PibCJvrsUNGFWo3Xh4eOS8ruzb7kWyObruaQmuNhxjYEk5nRzgUPy+W548GeFyu7E/P/uaobgxIULdd8eLUmJExvG+77x//94nWb2zRda8psRD8cg3N/Zp7PvOvu30W494JTf+OZ/PR1E7gYGUM99+8w2Xy4X1dEJFeH5+5svnz7x7947379+z7zt131nX1ZuHiDZeloUu8Pr64vHBOR/r++vLC6UUPj490U8nZNix5t5io9eUyOtCPq3sdXd6ZDNeL6/HdGhdFp4eH122MQ9xwOTT589H7nMpxVkXEdk034dPbpwquq4r53Ei58T1dou1NCbNQCmZhwdnUzx//cq275xSPgqKPx1/vMdbTxtncEXjKM5UBN/HhwiS9ZAmDfN1VGIdmiahDw+PpJzZ94rg+9FkO7Thaz0xURxjNv52vBf/fX7MlIm3wN9ce8exj05d+Djknn2MoN662Rkqh95/7sMtaLSSlBIR3OPITt94bfthDCblXqhy/N77+wTC/K+TS3E9sjmzo7bNPUvMKNkdz3s3lqV4IdSN0Z3JmXJE0fXGdrmiopzXE2tOLEvhw9MTBWG/XKgRu9bG4NIar3v1z1sKWD8GUJ4aNhDJiHRG8qjffa8kRwYQhCVnmkwWizFqI49BSi4X9mXDQXQZHAlrb0H3GRkqsx6Iin8OD7xBT7HWKtOkdn6j16pR6rypcWBOZ+P+kDdO9lMSiTfJKsZk8GtyhpffPx5jTXhXeUkUA47RsP8Xe38eLUty1ffi3x2ZNZzhzrdvj+pWT5poDSBhQAhJCJAQyLJkQAwC2QYjPzA8zwZ+Rs+AeYP9bB7mh8H2w8t4CXv5/cwyWPgJM0gCDEhYYpZoTd3qebrzmasqM/bvjxgyMjKyKuucOsO9d3/WuvdUZeUQOUXs+MbeO7RC3suRk7MtwoEh0zGaTCYoigL9QY6+7UAakd96SIRu+qgEcNNJqew/bwM6rx6CP3/3rygKOKHB2XzOnmXfyScrUBMoM7Zsj9x1Jh9yap4xs03NY9R19TPrJZmZAcfau0hBCKsVLtwACPtOHWHiwhzR867m7AY92OQCMR65BLAJJWD/bDiTkYxXhRv8YwDQyDNlk+9TrWywg2GmY1GaMFb3LDgPWlcPKJt+383OpszsWAUDbvaXUjMKXUJp47HAykzNrcmIdGWmoImxM9EoJiMMMIDqD6GscDVhhmKNvGe9msCmPhgaD0VFCr3JBFujHcCGOI4nJTDZwaSXYymzIl+em/evZ0I1S62xMx4j7+UYDIdGAFYmPNlN3woGtra3zP0g42Fgpnm3g9oU5P1g4422vrGBvJdj9dgJY6P0etUAQ55jNB5he1tjyQ5+FJMJlpaWsLKyAkVkp8mkKpSTTALgYb/v8xqUdnlRFNgejfwgCSllzqUosLGxAZVlOH7sGIrxGKOdLYx0iWO9HMV4G6PNMba3R9DHB9jc2kG+egxbozFG29uAUlhbWzNTTebm32AwwInjx6Eykz9itLNjZu2ybdfx48d9fa21Rr/fx3AwxMCKDNvb276/PRwOkeW5TyrfBWIZdhEEQRAEQRAEQRAEYQGo2asIgiAIgiAIgiAIgiDMRkQGQRAEQRAEQRAEQRAWgogMgiAIgiAIgiAIgiAsBBEZBEEQBEEQBEEQBEFYCCIyCIIgCIIgCIIgCIKwEERkEARBEARBEARBEARhIYjIIAiCIAiCIAiCIAjCQhCRQRAEQRAEQRAEQRCEhSAigyAIgiAIgiAIgiAIC0FEBkEQBEEQBEEQBEEQFoKIDIIgCIIgCIIgCIIgLAQRGQRBEARBEARBEARBWAgiMgiCIAiCIAiCIAiCsBBEZBAEQRAEQRAEQRAEYSGIyCAI1yk/+7M/CyLCxz72scMuiiAIgiAIwsIRW0cQjiYiMgjCLnENm/s3HA5x22234U1vehN+4id+Auvr6wdSjp/6qZ/Cz/7szy58v08//TS+//u/H1/+5V+OY8eOgYjwG7/xGws/jiAIgiAIR5Pr3db5wAc+gG//9m/HC17wAiwvL+Oee+7BX/2rfxVPP/30wo8lCDcS+WEXQBCudX7kR34Ed999NyaTCZ555hn8xm/8Bv7m3/yb+LEf+zG8733vw8te9rJ9Pf5P/dRP4ezZs/jLf/kvL3S/n/rUp/CP//E/xv3334+XvvSl+PCHP7zQ/QuCIAiCcG1wvdo63/d934dLly7hG77hG3D//ffj4Ycfxk/+5E/iv/7X/4o/+qM/wi233LLQ4wnCjYKIDIKwR9785jfjVa96lf/+Az/wA/jgBz+It7zlLXjrW9+KBx98EEtLS4dYwt3xyle+EhcvXsTp06fx8z//8/iGb/iGwy6SIAiCIAiHwPVq6/zYj/0YXvOa10Cpyrn7q7/6q/G6170OP/mTP4kf/dEfPcTSCcK1i4RLCMI+8IY3vAHvec978Oijj+Lnfu7nar998pOfxNd//dfj9OnTGA6HeNWrXoX3ve99tXWce+Jv/dZv4a/9tb+GM2fO4Pjx43jXu96Fy5cv+/We//zn4xOf+AR+8zd/07syvv71r6/tazQa4W//7b+Nm266CSsrK3j729+O8+fPzzyHY8eO4fTp07u/CIIgCIIgXLdcD7bOa1/72prA4JadPn0aDz744JxXRBAEh4gMgrBPfNu3fRsA4Fd/9Vf9sk984hP44i/+Yjz44IP4/u//fvyzf/bPsLKygre97W34hV/4hcY+vud7vgcPPvggfuiHfgjvete78O///b/H2972NjAzAODHf/zHcccdd+BFL3oR3vve9+K9730v/sE/+Ae1fXzv934v/viP/xj/8B/+Q3zXd30XfumXfgnf8z3fs49nLgiCIAjCjcD1aOtsbGxgY2MDZ8+e3dX2giBIuIQg7Bt33HEHTpw4gYceesgv+xt/42/gzjvvxEc/+lEMBgMAwHd/93fjNa95Db7v+74Pb3/722v76Pf7+MAHPoBerwcAuOuuu/D3//7fxy/90i/hrW99K972trfhB3/wB3H27Fl867d+a7IcZ86cwa/+6q+CiAAAWmv8xE/8BK5evYoTJ07sx6kLgiAIgnADcD3aOj/+4z+O8XiMb/zGb5xrO0EQKsSTQRD2kdXVVZ95+dKlS/jgBz+Id7zjHVhfX8eFCxdw4cIFXLx4EW9605vwmc98Bk8++WRt+3e/+92+0QWA7/qu70Ke53j/+9/fuQzvfve7faMLAF/2ZV+Gsizx6KOP7vHsBEEQBEG40bmebJ3f+q3fwg//8A/jHe94B97whjfMta0gCBXiySAI+8jGxgbOnTsHAPjsZz8LZsZ73vMevOc970mu/9xzz+H222/33++///7a76urq7j11lvxyCOPdC7DnXfeWft+6tQpAKjFOwqCIAiCIOyG68XW+eQnP4m3v/3teOCBB/AzP/MznbcTBKGJiAyCsE888cQTuHr1Ku677z4AxnUPAP7u3/27eNOb3pTcxq27SLIsSy53sY6CIAiCIAi74XqxdR5//HG88Y1vxIkTJ/D+978fx44dW2TxBOGGQ0QGQdgn3vve9wKAb2TvueceAECv18NXfuVXdtrHZz7zGXz5l3+5/76xsYGnn34aX/M1X+OXhe6BgiAIgiAIB8X1YOtcvHgRb3zjGzEajfCBD3wAt956674dSxBuFCQngyDsAx/84Afxj/7RP8Ldd9+Nd77znQCAc+fO4fWvfz3+1b/6V3j66acb26SmWvrX//pfYzKZ+O8//dM/jaIo8OY3v9kvW1lZwZUrVxZ/EoIgCIIgCC1cD7bO5uYmvuZrvgZPPvkk3v/+9zdCNwRB2B3iySAIe+SXf/mX8clPfhJFUeDZZ5/FBz/4Qfzar/0a7rrrLrzvfe/DcDj06/6Lf/Ev8JrXvAYvfelL8Z3f+Z2455578Oyzz+LDH/4wnnjiCfzxH/9xbd/j8Rhf8RVfgXe84x341Kc+hZ/6qZ/Ca17zGrz1rW/167zyla/ET//0T+NHf/RHcd999+HcuXMLS1b0oz/6owDMdFSAGbH47d/+bQDAD/7gDy7kGIIgCIIgHG2uV1vnne98J/7H//gf+PZv/3Y8+OCDePDBB/1vq6ureNvb3rbnYwjCDQkLgrAr/u2//bcMwP/r9/t8yy238Fd91VfxP//n/5zX1taS2z300EP8rne9i2+55Rbu9Xp8++2381ve8hb++Z//+ca+f/M3f5Pf/e5386lTp3h1dZXf+c538sWLF2v7e+aZZ/hrv/Zr+dixYwyAX/e619X28dGPfrS2/oc+9CEGwB/60IdmnmN4fvE/QRAEQRCub653W+euu+5qtXPuuuuuua+XIAgGYpbsb4Jw1PjZn/1Z/JW/8lfw0Y9+FK961asOuziCIAiCIAgLRWwdQbh+kZwMgiAIgiAIgiAIgiAsBBEZBEEQBEEQBEEQBEFYCCIyCIIgCIIgCIIgCIKwECQngyAIgiAIgiAIgiAIC0E8GQRBEARBEARBEARBWAgiMgiCIAiCIAiCIAiCsBBEZBAEQRAEQRAEQRAEYSGIyCAIgiAIgiAIgiAIwkLIu674l9723pnrlIoAsp9zhUvnlt1Xjyo1zjyzBaUl36QgCIJw/fHvfvHbDrsIwi7oYucIgiAIwo1OFzuns8gwiyJXuHjzMnRGAFlpgRlg1ISGY5dHKDNCmZmlvYleVBEEQRAEQRAEQRAEQThEFiIyjPsKV88u1QUGACACoxIaGMCVs0uV6MCMkxe2AQCqZBEcBEEQBEEQBEEQBOEaZiEiw8bJAXRm0zswtwoNTACFURJEuHLTsinIuMTSxsQIEMwY7JQSUiEIgiAIgiAIgiAI1xALC5cAjIDAcRIGwAgNbjmnhYOin2H9lAJp4/Uw2ZpAlYyV9XEjr4MgCIIgCIIgCIIgCEePhYoMSYEhtU6UpyHFaLkHANA54djlkQgNgiAIgiAIgiAIgnDEWfwUljRDDrBeDUl/BqKG+jAaLlQHEQRBEARBEARBEARhn1hID/7ExR1cOrcM3euoWRC1hk3EuMSQgiAIgiAIghCzfmKAySCrLVMl4+RFsSEFQRAOg4WIDFnJc4czdA2byAotoRKCIAiCIAgCAGDSU7h6Zsl/Z0LDk7bMGBduWfHfV9bHWNqcHFQRBUEQbmgWIjJcumkJk65eDHNy8ZYVnH16U4QGQRAEQRCEG5wiI1w5u9Q5PNexcbyPjeN9AMCp57aQlZVHrdiYgiAIi2UxCQ9c7Tyrwq9tY6a2pETUBEMqfEEQBEEQBCHBPPZmYpvL55ZBDBAbO/TU+a2kPSoIgiDsjv1xP9gDrEReEARBEARBEPYZIrAiXAlCLwRBEIS9c+gigwjHgiAIgiAIQheIgWxS7nkfMqQlCIKwfywkXGL+tI/BtgRxURMEQRAE4dDRBGweHzSWL2+MazH8wuGRacbq1RGunl0+7KIIgiAILSxGZEhk9e2KkjZbEARBEIRDhgFcPreCSzevNH47cWELZ57ZRKbFaBEEQRCEWSwm8aMgCIIgCMI1DBO8wNDfKXD80jYAYP3UEFfPLuPkhS1kYxEZBEEQBGEWCxEZjl0Z4cpNCjqbP8WDJvFmEARBEATh8FGFxs1PrCMrSixtFQCApc0JnnnecZy//RhufeSq2CyHTKkIW8eaIS2CIAjC0WEhiR8HoxKkd7FhxxALyforCIIgCMJ+Qgzc/vAVrK6NvMAAABvHByh6ClvHBuBdhoYKi4MJmAyywy6GIAiCMIVDn11iJkQo+ke/mIIgCIIgXLswAefvOFZbdummZVy5aRlsPTUfe8FpPPLC04dRPEEQBEG4ZpCcDIIgCIIgCAB2lnJ89vPO+u9MBKjKe6HoZwAzGDIF4qHCvOuE44BNWG6nsSTNOHlxe2FFEwRBEI6IJ0MjvJESywRBEARBEPYJVgQQgTPl/4UCQ8jnXnI2uVzYf7KSceLiDhiY+m8qsUAhRqcgCMJCOXyRgcgoygHc0qgLgiAIgiDsBw9/3k3dViSSPukhYixE44bAGTX/zWnZsiJcOSu5vwRBEBbJ4YsMgiAIgiAIh0xvp5i9knDdUsoAlyAIwsIQkUEQBEEQhBueOz9z6bCLIMwDw+RmiKH5vRl0Rtg83l9IsQRBEIQFigzDrUm6shcEQRAEQRAEQRAE4YZgYSLD8SsjkGgMgiAIgiAIwn5DmDrDRJzvaxY6I4z72d7KJAiCIAA4wuESrCTZryAIgiAIgjAnNqn4NKGBo5nMdKYwHojIIAiCsAgWKjK4Cn1e9TjJHuY/FgRBEARBEK5jGICeMhzlhAZ0s0uzQpvQX0EQBGHPLNiTgbw4sBChQRAEQRAEQRAiCG46y+l09YwlzchL8aEVBEFYBPsTLhEIDSI2CIIgCIJw3cCMuz598bBLccPTyUOBSDxjBUEQDoGFiQwN7XevFbvMVCEIgiAIwlGDgbwQG+XQmZH4URAEQTg8FiYyPHfHauu8xLtpiknvqTiCIAiCIAiLhRn3ffz8YZdC2AWzbFGZIU0QBGFxLDZcIqUoE3UKmpO6XRAEQRAEQVg4MxI4qJKxsjY6sOIIgiBc7xzYFJZTRQSbAbi2fiYucIIgCIIgCEIEEVjNYScyphqiOiNsnBjsuViCIAiC4WBEBomZEwRBEARBEA4D1c2rVhAEQVgMCxMZljYm05M1UiUkzxCUBUEQBEEQBOFAIAb6o/KwiyEIgnDdsDCR4fiV0fRkjURGSXb/EtNbivAgCIIgCIIgHCSkGcPt4rCLIQiCcN2QL2pHV84MobNmbgXAKMSNxUQAM5hsRl8iMLiZ3ZcZx65IMh5BEARBEARBEARBOOosxJPh6pkhto71jcBAVP83Dft7SpgIGYi6LAiCIAiCIOwSJvGYFQRBOCgWIjJM+hn0PFl+QyQppCAIgiAIh8xjLzh92EUQ9pOOU6oLgiAIe2exs0ukBIPE9JQp3Doplfnizct7KpYgCIIgCMI0JoMOEaRE+NyLz+x/YQRBEAThGmaxIsOU2SU0mX/JmSV82ESLSLFbLwlBEARBEIQFUuYHM/u3kEZCHgRBEI4+C2spGwkbaz9WORpCj4Wm2FD95pDGRBAEQRCE/UaV06bIEo4CDKDoKVy+aamxXKZIFwRBODosTmTovKLxTGiEUDhvhsBrgTPzOSukyRAEQRAEYf+4+88udFqvNy73uSRCG0zApVtW7IxkgbBAxn50NuQ0q1EsSmHRiMAlCE0WMoXlrl4sO2UlouktGU3B4tT5rb0UTxBqz6cE3wiCIAi7ghl3ferSYZfihscJC6lcYH5q9NR2ikCldAWFxeCepDIjH/KdF8YjSmxN4Uans8jAANZPDZO/jZZ6mPSbThHkNiR4JUJpDXLCQiw0EAHMpuHwy+Y6H0FoUDNGuClsCYIgCIJwbdDfLjBammK+WttyahivIOwRhhEXvFeNNSyLXIGYoTRX/R1BuAHpLjIo4Kl7TqZ/m7IdBckglWbkoxL5RAPMaaHBLnNJJLvMTCEI06iNdrR40AiCIAiCcLRRDJy4tIO1UwNsr/Smruusz7itrw1kCcIu0YqanjTOxgShzAhKW7HhUEooCIdL55wMz91+LLk8joWr/SM7Y4T9pxWh7KnmbBHhNJfUFC02j/e7n5EgBCQFsI7TqgqCIAg3DhduXV3oesL+oDRjaWOSnjbdEeT/atgBMmOZsCAYdua8eDDLPnsmjOIwSygIh0dnkeHSzSv+ZQr/tcXEOWq/EMHpeayoUfEzqn2FDcPWMREZhN0x6/kUBEEQBABY2prMXokIV84uzV5POBpQi9AgCAvA25gpO7NtuSDcIHQOl7jrU5fw6AtPz/3CpBI5+t8UgbSt+uMYOhcyYZdfOT3EyUs7KDPCzrAqNnHkhsTA0nYBwCiI40HmfxrslFBamprDQiceBHWIt0PT4R5fEARBODqsXB0ddhFuKJ6+8zhKG79+++eudt6uyAhXzy7VRojLTFn3dTv7RxSCKzkahEWjxSNGEKbSWWRY2pzhmpYiUbFzRlVGR+5e408GGdaP90Fcn+aS4lgnZmzZOD0Gam5xO0vdyz/cmkgHdIF4gSF8hpj98v1IjpMSNTwuySgkLlMQBEEQ9oudpRzPPu94Y3nRU74tfvQFp2u/+XDbBMSMopc1ljvRocwzDDfHzd9RtfesYI0EER+uFxjAZuT53B+V6O/nlLPiqSAIrXQWGT79inMLO6gmgrIeCNp6M/iKH9VkFO5D1SjUfd4o5ZUwJd6+kQtiCi6hkNKM4VYhHdE9QpxI4ulCYwBoG7iT2amlFna9Z8VsioEhCIIgWLJJiVsfW8MT95467KJckzCAJ+47ZbLuo0qOF9tfrtl17uah16kn1X5z6P1qP0araIXAkESwfjSbWbCCzgjrJwZYbfFmERvwaMMANo73G8/MaJhhNDTP1vLGZGFJGMVsFITZdBYZdNY5fUN33GhyGDYBVI1DMMvEmWc2sXFiUNvczYXcdTS61t50KRtMY7WznIvQsEfi9j69EqHMKqFhL8yzB/FmEARBEDwzvCyTAxwCGMCT955E0VNgN+DTIvTXBh3mGQ0OBidcw81dPSEDmzLFpK9w9fQQqnTTD5p1lzYnc/cqw4Gz1HLhgAiera3VHlbWxwsZWJKEjoIwm84iw15pU5xrlX6QhyGcwpLYxM6vrI3rSSBtI0aak/tvJJa0DZ5bv2ujpJVRQ4c7++hydQNTEyCokxwxk84JH2cYHYIgCMKNQ9nL8MR91nU/bhtse3HvJy4cfMGuAZ656zgmA2NWtrXBtSu6C1fz2vYqaL/DsFyaYnOiw8BCFN65vTpn8nFmLG+aJKJlRhgtGc/Y/k6BvNAt24gAse8QGfErzuUmCMK+cCAig5vmMlYP5xlBNi71LYq4XR6KDfFvtWWJssxCBIa9wYnPi5ETBEEQBGFBBMJCmCvIhO9Li9WFWVdpz7M+qWB7Nv8aooIdsIi9HBqes9gHb0YibCWEifEwRzNThEGVGktbxSJLISTYOtZvDYmZh7xkTGQ6dEGYyv6LDC4OLqKto89BHoaw4h8PMoyW8qniQOecC0FsHiDq8UEQGxXh/Z8nV4YgCIIg7Bc+4XMkKLjlwy7TXN7gTBMR9jq4QABYA3ApHBQBTjRoxCakhYZ4HcnNdPRxA40EhmpxBhEE4WhxMOESXnE2+RdSrYwXFJz7ethwAFg7PcRge4Eqr1MgOUgENIUiJ+SFtEK7Qa6aIAiCcE1gxYW2GY9ueXRNBiamMNOLYRH7r7kmpG04P5CREBr8YJaL0N1jmRrlgwxe7QZN7TOKMAGTYQ4qGf3x4U9HP3X2MkEQABxgToYQ00iQT6pjMg+3J1PiIE5/P9zaahmHW9YZD3LkhYxg7AY3qlHLJo0qjEYQBEEQjgLJqavDvFFHmFTRD7rEU5M9zvi9076B2tTknXIZOBuSg+/Muwqd7QJXJquIDXOgFaFMTE0KBLORZIQiJ/TH9Rs36ae3i+mNFjNYqVVzxhRBEOocisgQVvCMlkYdAAjgwO1BKzLq5aIbhQ7uckzApKfQm4if1l4IDQwRGARBEIQjCSdsjSOakyEU7DmIE1eJPFUp2rw29oW9CjXx5nN4I0wbpGIiW7Td32M/uh14ykoYxsEwGmadnq3+qBThRxAOiH2Yl3IOogqhaiQrb4fa76rqnPp/B1BMR5ErFBlBEzDJlf8nbUg3jqqocNDPkSAIgnCECVzwU/+OEk5gYCIzuhoI+VqZZaAZ/xZVlv329Eh5QZAJwZ3Zjrd5V9gkkguzA6Lj6I771Yow7h+uSb5XGPsbRnCtTh1bZtJPEG5MDseTIQWRVaPrU1g6R4a48ayYkdRnDrpMa1RmClpxrRyTnkqq1ZQY9cjKG2vqnLDBIT6iQoNMYykIgiAAPhfUUWyqptGwj4JcWJqmeIy6bRdgR6UGh8Lf9tz+a5gTSR0jzvNFBA1uPW9TFmrYac6ZYSHXxHltdkksSQStFMxJXnuEtp77PO2ZmxeXQg0Aylyh0LyrPGnbKz0sb0wO9P1m238RrxbhRuNAZVOnFGub/NG9b52Uz7Z1FqWcd91Pi5LOKvgXuMzt14jBtYRWJCquIAiCcOSpeSswg3T931GDMKPz4rwappgfB9b52asNFE5dWdsvGtZsMl4+9CqYUhaa8XuKWXbsoi9xqQhFdsRsyj3aulnJUGUHkYVo114zOtufbo94xApCk4P1ZAhGjNklfmwZRZ5LRZ41EO12wunFC8XG4B2xql8QBEEQhFkEs0sA10ZbPtNDwOXBihe3fF40e/VimNl5oxYjD6h7cczjtbgb78a2jm+XBOMRk54yA3IzKAMr/rDyDbRdKU3z5/sgze3eKgCoZBPikixE+3b7STWwWBXDfhWEG5rOIsO+ZC2masccfPUqctdKPph9ov131NdJrL4wl0FxixIEQRCEa46GuHCthNJ16FyFHX3iDiGiC6CL58DMbXc7+ByIK9OOnvr9bIaKsQABAABJREFUSNhwu7xmi76nsy5FOGOYxw4kzmMP+xwigM3mWT8TZ+lnhUYeJWFfXR9j43h/9kH2632msNyNj4JwQ9JdZLAVBQcVwCIqYQZAbnYHRSYeclrsIFpe2i6VcW2dxHEWEZvfYcTgIBr2o8Rcz8m1YtAJgiAI1x2xuEDW1tGKkE/KI9dGGS+Bee0fa9Pp2R3wveCu1K6n+iPUp6vc1T6aXgS1cyYCKwaVeztMl3J0ETyA3bvdj4c5+juLmaIxLktpQzNm2XM+NwibM6WO5+wh6pS/a+b+7Hsb5ypZWR8v9HlvXI6onyFCg3AjcyiJH/1UlPHcxYCfstI3BjZzsFbu++IU5n1rXIMppADU4jjLXCErrs3EPrulcc+mzDl+oyXGFARBEI4mKmi7lWace2JjocnsFgHT7jvxfrt9zDWx67IB8xlokfjDbvAK8EIDMYxo0Xa+wfFY0cJzcLh9zrI9y1ztW+4AR5hDwj3TqbMts3qOhc5hL27GDoafen7uJyHhzdB1u95EY7BTYDTIMRlk/iedEahYoJ0ZiRj130x/5ojpkoJwYBzqfDnJ9y5okHzj5OK6ePfZajn+Z8UL4eDJS0ZW7qNRs297FgRBEK57mOuzDjADzBgNM2lfDgAG2pM8Tt0o+Dqv+NKSTHDR99tPxb7g/TpmJdrWZJJGTnoZJn3zr1RmavYyI0x6ykzXniuUudp7jgMi6EzNLFect2PewcQyrzYmBvKJxmiQ1QQGANhe6RBSMQPXh+hUROlrCDcw3T0ZotF5YG95Grw3Q7S/tu3JNvqdXc3islLTpVCVSE4zKSyOsKHoooC752LveTHa9yD1vSAIgpBEMyiactAlr7t6ZskMelhnRMWM4dZi3dOvF3ZrWXn7bd7OrSJgxuDFvKEK++HNEO7X7bnLmYZ27bQOeNFT6I/SsR9aGREhvrZFT9XDSQj74sUxzWNGK0p7b3T0Zthe7vn1WBG2V3q7LussTH8C3Z/TG3RmOUHYfbhEQnTYU6UUuLO5yih+LZkIUDbGaYbrVahg1xTSIDkLK4C1dDr3E8J8ivSshqjTPnwDkLizC8olIgiCIFx/qHjgwX23Ax3rJ4feZqCSUeTj1h41AVhecAz4frAocT9m3hklfEd6DzkcWmPgEzm34mTf8Wj6fsKqKk+XPAfGu6P7NjGlonbPhMCtPxQaFpEIPWTac0Z2utjQdnfJSbHAWSMWlrPiBp6WXhC6svCcDG7KGqbuoQ2NmP3EbBFMZCso+/OMBJF+O7R3OE3IRLf9CLtHaa55kjDQaWqmPTGl8tdzPJuCIAjCDYqbkpptTHnU6WIFbK322ztizD5hXlYyVtbH+13iXXFQHetO7MU2CLwZ3ICFJmoKR9PEiIOkY7Lxmg1rB+QYswfbHE5gmBo+EgsNcyRC1x0K4fJhuIxksdigGGDNKFNlXGACtd5479N8atUcZBUEoclCRQY/jY3926kzN0clq21jbZJGYqbK6gQGn38hXN9lHD4EnCp9o3R0m5LR7pNB7Rn7vMXP5n6o9oIgCMK1DQEAoxHe6ZiajJoIo2Xjtk3aeEIsb0z2pZxCAttx1oiEhql2Z5AwcgYpzwf3fR5PA2eP6IxQ5Kq2PD3sb23aDoNtWtl9drG5IqGhi13kBYYZo/pOGIG1491AU88mQnc5IRpFQvMUD90bdRdeDEUvW4jAIQjXEotN/OhevI4vYEoJbHRG43VCV/gOb6sXGOxf50p/YHVUoiHrNN2UsH+0ugsKgiAIAqrQuqANT7t5d9wdmbjxrX2MFT9KtIYstq0P7E8q8sBW7GT7RR371DYM0yl2g1is7L9ou87lsyjN9dnHvO2a3lnb+Uz6VbJDH2bRlfBYM4WDbuv5dYJ/7nqNewpjm2xy6n6syOdEv2uNQxtYE4RD5MBnl5ha8YbT5MzIRNu6fxgXxmQehg7ix6xGqNEIzKCrO9uNREqtPgy6uPgJgiAINyauQzPVY7KLFcXV9InXc2eD7SwFu25b93t278j+6yQQ2fxjmurigp+Nwn7WWf1fisH2BKtrI6iyeaJ6N96URN7ejcvr9ln09s/Mn1dIatBxYPL6fWME4fpm30WGRmNDBE5UwE2PhRnVypQQCx/7v5upY4K8AeE/ANA2L8SiOsk3SifXnadv0I+C14Atw41yDwRBEITpPHX3ifk2COyFWTgbZ2c5x8Wbl/10mKl/1zRzeLM6/Dkv2CLlls8hvpTMUGzyRzWmW/Q2JaqReLsxE9VDBsLBsugSuKnYm4VwK5ofdSwcTKPlGRwNzPSU+2Vvxde29i/yGl7YMx2ELO1ln9f8OyYI1wgLT/xYIxX3lhIHovXiHC/JnC8zaq6GF0N8PLTHsqW9Laia7nIRlfYcSXWuC4j8qBAvMFPwnrD3YIE5hQRBEIRrlV22SzNj18P23h5j48QAOG4XBbaAKhknLu8cSo6gQ20LFRZuF4T5MpgIU6csJ4JWQNY2u1UoHgTL2qYydMcLr+k0T1lVMgY7wdSTc1wKVvDTqsZl3VcCsSVebgpWPcR7TbRpXzGAGWVPAQT0JvO7vmwe62N17WgmYBWE640DD5eIVdxOCQFt/FabC1r6QPNXZ5UKS7V/qek6F8UNIzO4aZeARpzroXIUxA5BEATh2oTIhEF0aEoao+PO3glG/3VGuHpmCVvH+ih3GTY6D87mmfQUJj216+OVGe0q39Rccf27ZZ59d103tFUjAYJtGIPPBWaX7yz1UPSU+T11aLjrYcNqupZlkdduXtss8O5oeLBEy/b6LLuBKmKAdjvV+R6v1RGxXAXhmmB/PRlaCD0CGDxfFl5FRmm20xOFb/xcL79rBMJtU/FlYUU2b900q7LexflfiygOwhLYZW2mo+PRIAiCIAgzmDXSP/X3Odp7BjAZZCh6CksbE/TH5cxt4u07rxvZPUVPISt0Y/arqaGFRDh/2yp2lndpUi5yuCsj8HyXa6Ekp2QPrt32at980NX8ZmT/KzMTjlvuYx6FmeyXB42z2feBUpGdjX62TanmyKmWQmdmOlBBEGZzKCKDT+7TUTEl5ppqyURgTk8x1CkJU7wNkBYYEmhFUGW36Y3cbgWL82ZwlhjPSAR6QMj0lYIgCEJMzdVd0fTR08TAx26OE4sRZUa1KZcZ1fR/4fa17x3tmVRRmQjcyxoJrotee/Z/TTAdr3nzMLSEGhwkqfDcveyrM8qG0NiNdKawtdKD0nx405t3tMld3oVdHQKLt7VGS4muTHwu9hlb2pqIrScIB8ThiAyhN9VuKlPXMO0FV7nvKjkkZrcm3GHEoss61wnuPLtcugPnRsuPIQiCIDRYvTLCxsmB+eI6+9Q9nryLYN3oZM0Y4S36GXrjEqpkE5IA26FfRJNlO/iuw0iBkFEE0yB2woVBdg43wIGIC/56J+5jw+sALmFhN0slzkdwFAZN9krnU2gJ+ZjKbrwZQtssyPWgMwWtS+hMmfsIVDm/UL+v18N9EYRrkQMRGRalXLo4N7YVyV5HoKcmAfIrVQ1T2PB0OacwyU+VfMiG8sWNHeoV4fU09aU7N+KmyyVp3vs0SAtAGiFBEIQbm+OXtiuRweFUhk72wozO6W5CJG2iO52xzxFl9jXHPtp2TfWOclyseeyQ0MaZKbQcVHsbXm8yRsjU8vlBp443KLBddqX52OelIX7g8Oy/uY69G9ttDtEOqJ5Bb0fCbG9ygGQ1DxpCJC4A1/WUsYJw1Nl/kSFqVN20Nv7nDrtgIqN62w6pU5pNhXNAI9D2PNznVMPgywvUkwLB2id2UUkEpasFynW03aHY7IVxfYgNXkSIsmsvykUxRBNQZmr+a9Zx5EIQBEG48TiUtpjNcVXJ0BmBQQsrg06FKkQ7Z2ZAzz5vMzVjN3fFttkYDozANYXRnHHCl29OGnYOUAtzmVqecLazXXpWsnVuofB+zTsj2kF4dS7oGKS5Sgaf8KBpC+nYWcqxtFXMvMXf9tVP4/hKAc0KP/2fb99zeQXhRuTgwyW8Er+7SkYTmc0PO87ACQ1ATRWv4gzjIfvwC9dmynCf67MuVALEocboLRji/R/FSLlAXs8cWy7wN775KXzmsSX8P79202EXRxAEQVgQxAxVcjXY0hEdrsv1zm7njj4RWHGj8+o6cCrQFbKSbfK9dg5dYAiZ0tl1g1ld7YjYQ9MMgEXiwQzc8bwNOUXUSnagkx4ulSil9BEKz+3ozRDnQCHN3jPBeTQQzdebKHsZNo8pEDNWNiaN37/u9c/ixXdt4s5zO+jl5r1b+lbgx35uutBgvCai8vONZYsKQszh5GSYxox5jGHDJNxarOCV9nmTPrKLW9gt4VzAqBoHP31RvLprpFsa2EZmXDaGglaEPJHt+VolbCiE+fj2tz2LFz9/q/aeKAJOnyiwvjlnDK0gCILgefZ5xw+7CK30xiXGg2xqB917Jzj8utycHWLO6RGd2FAjDJfX010eWgdgjiK7KaPv5O9ipN57V1jPXzv7Vmovzou3tiyyp7Ry3rJuhcrTxC3PipYE5pEAsC+Jsee4RhT8TYXz8BxeDG7ZZDnH//WeEY4//KhZu9QmV0RZ4tiqxqBvPY1LBinCXbePZ5bT2/y16UzZl1MQbkQOTGToGuc1zSWxpiw7bwirtEObTnxnoWG3LltkJseodfiDXdXmR47Ln2ikSftdJLZh785X5OraFhrczBKoNxSc+L5XUhV63T8kwZFM/GjK9IUPbOKv/IVnAWb0M43MPeM6zP4tUyoJgiDshYOaOnA3MfejYdZNnE91kBfRsY/3wQytAOVsGM2mI0sKyZb8WhEYArrcp32zHGqdVftXURUuPONRDb1lAYDYzI5SKjdKp413Q6wNxbbxfoVQzPBmaJ3Jxc4sx8H1Caev9FvFuTLIXJO73jHGj9z8WRxbK0EnJqYQpTbrhzYVEZAp86+cfrF1QmDw39mFPh9FG1MQ9pej58kwDfuO6oygOEjg44QGoFmxoEMHcxZBz9hNYVn/Peo6tzWkiUaaXXvcVtmS/Y956tSdR5kwRGKqKyXMeuNBhv7ITHS9m3NNbeNV7ZbrrPZp/uZ5ybLq/p47M8E/+uuPA2Aoto1fGFIjs2IIgiBc94yH1kvtKHXSo/ZnWskOu5WaJhbUwhTsekwwLoKxrRft022/aOLyVsJCJS74vGRtffGoXEoz2IXmaqCEArE2I2Zh3zooQ/j9MEgdO5wBxduVkX3JLpZCASonlC/s4Qe/6jxe0T8P6BLEGjTRZh1n+zmBwXlGeJEhA/TuvUTdPeqNy13vQxCuVa4dkcF35F3DRmAKkvakptOxqv/UihgzKtFQiZyx3twNaRBDx3Z+7ZSi7I5NVli51lyvTNmNK2ey7PbaMZmRpDJTGA8yrKyPwTBt4DwNXqOB7rCNzlRjXvCDYjjQ6OWmlP/H33oCy0PbGLlGT2tzErqlfEqh1MDWWMIlBEEQrjuOkrjQAmme3tge4inMtJuIwBnMKLl23gItAxbBZ+08S6bcn7k765F4U/NiyKpBNJdospPIYQepXJJxVtbz1w1iOdPB3UNlczkckcEXRyiAEAP9UWlml8ire8EEZASsrgDH7y/w9V91BV/av1KFRCgAhRUSykBYCP8qZfoPROZzHthWbqAn9KSY5kUdeF3LoJBwo3FoIoNWhGyKSryX/QKoBAbMCMGwOR1iGp39jgl8ONtlS+rmD27EU9SPfa0JDDGhuycHf53AUPQyfw/XTg9BGlhdG6HMFMqMMNwppgpGPl4xWCecInPu6cP2keWlEidWS3zjmy/j5S/aairq/i83G0HXwCnTsj3+3BD/93++5YBKLgiCIOyWts5rq50y5bejgtIzvCwP8STaQiineTe4KctrHg7ud2UUiGnTbzOsl60VL2bamS1lqsoSbRt4iM7E2kXa7p+s7e3O0V8fRTbJZzV7GruCTrGj94upjwwzip6qX38F3H5rhrNnC7znrzLw9MNmecGVOODsqdJ6MoTT09amiK0+03ClOoQ2iVjLPLBlW0KkPQSM+xkGO8U8py8I1zwHIzLsg4I3dW9zJBXUihqrhxXsQTJT8Y6mA70miRRgF2NY9hQm/czEEYa/Z8D6yYGJG2Rz7oNRU2hgAGWuUOTKxvJx1QgnkgIdpsE26Gvc87wRXvGiLbzx1WuVAZISF4B6IxhePy80HHXzUxAEQWglEsZDah6bR5DK0xLT7bzDaqcSdhMrk59g+nW36wFm+vRgW5Ad0JoSGls7phMbwlXKuoARDijFAyXJ8s17Pe290QSgp5BNtLcxGt6fiowQ4Qa/9oMZg3fTvE/85bH/lXmGlzx/BBoC/8tf70M9/Vng6fCGoy4oxIM2YZlcmIQdwGGlsHP2TgBXAJhw7aKn5nqer0UPZEFYBNdOuESEc2eblvmWo89dqwRfwR40M2IAr3VSyjvDXO8iVw2BodrQJfckTPoKxBn647JeaedA/xUZXnEKeOppjac+Q8jLRHbmFqGmNcnQwmC8+gs2Qcw4c7LA27/qCnwIhGvowphAVxwdufPFeM8dSfwoCIJwTTJjUCPudB4ZWTlIvG2KN6VkiVkADgRewMBMYvaGveZi4IyqsiU8WL310nHmhFnoTCE/q7F8inHxch8rT42qfR5iJ7jNNp/mgezQGeHzX6LRP7mN737jBQx4B3hSV9fM23VcH8TRXLe9gLrA4D7b+/DHl84D6Jk9EeYXzK5fs14QptJdZOigrB4YRFXt4yrjqAPrK2Ln7nTEYssECwFlZhIaeWk60aiae27iCscD4/Ew3K48GgZU4K1nP45X3wY8tKzwS5fuwEPPnWw9bNyw0T4n1Pya117F17/pcjUiVaZUdaQFh5igAQQRtscKv/X7R3fqNUEQBGEPhJ1OYKYtdqBdeds21+yyRHn4EF0IF3XYWge/g2Ay85SneaeG+6doR3OINZqAF96b44HVZ9Cnp3HynMZDX/AC/NZ/6AMTuztuKet+e9C4kJRYUJhxXAZw/0tK3Pf8LbzplSPc1LsCFKX1mrZrxN4LTlRwgzvxTBJEVR6G4PpqBj70xBDAHhI3KkImiR+FG5D5RAZgYULDbrP5JyvCuAGubXBI6vk+cNhu/nuFgIYYpG2FTmyzH7uRkZZ7pjPCZJBD3U74+peOcPLXfhd6c4Iel3jVH30O9CfAAydzfOLkEJ/YOIvBTqJiD4Sp/b6ef/71l3HmZIHXfMGGmUc8dtXzQjunBYbwsVZkPBZcY2jPZWec4UP/Q0QGQRCE65YwjHCq28PBtG1JNB+5ZIHz4kf141nDHIcU9sE0nxcDAzh7tsSbX/I0XvTsY3hh8Rz0xUugyxm+7POv4MN4NSagmtARnq2z17zpv1/eDnHYhPXuaDvNyWqG+18+wbd98UXcddMO8knRFCV0ZE+FuRhCccEZ1bHAEIR4M2X4w0+t4mZc3dNpZuW1NzOcIOyV7iKDhsnKepjtx7QYrmlCwhGOZ4xxiSiT4R/XUVwXI3A5dMMc5OIfURMafCOnCFun+/imL76Klzz6Mbzgc2vo0VPgfl1I0FsFXvqKTfzZsMATH+8YM7lg3vBFa3jFi7Zw3/O2sTSIci3MEhfajLRQbQcApTApFf7Vfzq3L+cgCIIgHCK0i3bftm2Mbi7ni4CDcl7TNoqbJpwI7C7eHs5n5r1jBnWc1Io0wFk67LS2SwD9AeOv/sXzuPXXPoq7H19D/sxVlAQz7cJOif4fPYr8la/G+Her8oX7JIafgcIsoAOzo9ueWQZw7myBd339RZw6M8Fdwy1gjKBcVHkpANWsXLEnA1C3wcIcDCoIlQCsrUUYPWy+awUUPZnFSxC60l1kuEZDvl0FuqeGNh5R3k/a1POAa92joUYoDlk3NyKymZitkpyZjMejpRxf90XbePvaR9B/7LPQ2yXKwjQCWY9BLtEwM17w3EN43pVzeAK3TT38oq/lA/dv4x1ffQknVyc4tlxatzz7YyqRIzA90aMjy6pYQZd/QSnokvCpzy0t8AwEQRCEaxo3/fVBDUxcJ96iIYy0fdDZm2AfOuadNI8M+D9f8gGc/b3nkG+tgdet4GRnQ2BmYH2Mv/dtW/jhD6/Y8cM4RNWdef1omsyU4gcNK2D0JSv4+1/yGdxyahzY5Ck7KhrMicNSHWT/U8EMFQ2BwSxbXptgZ5ijNylRZtS4Bjq4fIdxfQThqNJZZPD1ZcuUj3tiQZ14BryY6RqBRTV9R6YJtTdCu1GDwy7PLnBeDFpVMZxkGwy2DRv5KSfhBYYv/4I1fMuV/47s44+h3DECg8oA1tWKlJldZhc2QOvb00WEIGxC22Sfe7med98xwv/0Tc9hZVDW3fLaZonwHgyJhjJEUVNguA6NOkEQhMPklkfX8Mxd6dCzaUmmjySBrSAdn93BisApu+Cg29+OoyHrJ4d478rP4cxjl8GjMrA3YOwMRaboE437/59fBtQ3pJONpwSS4Hk6SLQC/u4/2cLzt57CKTUy5+HKF3suAGlxwYVL+HNBZU+FHgzuN4e1t/JJiTInlLnpMiWvQXR95J0ThDlnlzhAj6k0tWlnuHJt26/jHFGcgHKtGT3JEQAGqGQoZX7TCkZoYAYrgs4IfELhS1/yJL7nmV8HPbsBXbDxcMsZrAkqYyMuOM2CCBhrsDb78I1LIMz7Yvh8Hrv1izTbnDtd4Afe/TR6VAaxfzPEhVCNb/NiUMp4MShVFxiITP4KERsEQRAWgiqnjKB0MICOXILpaFAC2D97gafMuOAGgK5V3F3lXXj0LlTkcWGlmn1ic2e9XHjeKv4j/yxOb10GF9rnGKjl4/ShlgCfX8eT957E7Z+5Ao10HgRnP4X20kHAsNdaEf7Ve57E8mgMpRK2Uy00IhBUpgkMDi8wUDMUNczNABcqQbO9glyI7zU+ECgIi2L3U1juIVYtOT1k1/1x9Df1O82OW5tmLHSNkTs0aiEGR8yo6YhL/KlhTkfDhEUozUZoIALlwOm7Mvzf33wV43/5PvCohC4BXVrvBQD+QQiVfvssLb+Gkf8po3wyODDVNzPLdqee9Xsa/+J/eQxZBoAZVJZVw9fVe6Ht99BlL8uaWY/t5+/+oTvnLrcgCIKwP+y2U9FxsHp+gtAJfyD30wIPo7MZovdRFMRbmv3wXtRyE8zLAkfmiNkoFhl8vg1NABQw/HyN957/dzi7dgkoNerKAsxgjpvBKxiw/9Dn/Wd8y0NfAVXOH1qzn5Zn1gfKr+zj//tFn8NKNjbiXXwt3QwRoecCEAgLzrMBdbuKEAzcBOEScchEsEzZPktjCvbgmabafSZQFK5kh0ZFdBBuKDqLDIvsy3JGkevS/BXxtBfV9zVn7HLa74ddEVzPlVGoJ4WfjcBg1OJTZxRuesMF/C8f/SWMf3IMLti0JxrgoKYnZXMxuBEGZdVoBt618hSezm/HJ7BsV3YuIM0b70Zhujayx1dK/G9/6wnkZHMuhFMiOU+GrqERsTrvTi/LgF6vpSG0m3QrriAIgjAPLTbJvrTNexi06bb/qsRuJq5F52oocmVGvo8SuzFcFYEj+5T3mOuv9Zmxs2l1EjG8w6VNTAlA9YGVeyf4e8/9Cm5du2hn9wDgykvRX6BK4k7A+KNPgO8dgj+zbTrFidWnsmDhiAGsLGl8x/dM8MpzTyLfGVU2k7ZuAZoDcSEQGKYl0Q5nmQs9GOLw01hgIAJIQds8DI3XlKpt3cCXWYdBziPXLhsv5RhsTa7txKiCMCfzzy7hKsMDflGMux2BbAy9y2Ycv/VxI5esAmc1PFbzOKzmkjNKe3u0rY9rQ5AgwN+7GmwFhoxQ9DPcMbiCf/Y/r6D4qV8Aj0srLFiBIZzaWLXcJAIoIxQfewSTSy9CbzLAJMoIvNtrdubEBCvLGt/7Lc9iZVjW3fV01MDVhAY0G8Ba+A+a3hW9vJ71ODg/1/gJgiAI+0Ps5s6KpoZEzN1JCzY8sDY8ENsXYjuws8nawyX8eofgzbDXUAXft9/tDhY1k5VzTrCzTHBOeN4L1/Ht4/+OF155sr4uB3fWzQpHaEy/2VeMf/Pln8B3PHwvVAnAlXOPo4rx1l3uOveB46cYf+VNV/FFp9aA7bIyxN07Vwb2ljtQY9aIhGeDw+W3CgUG/zchMCgCUeZPwu8qJaYpqo5ti0YHOF26IBxFdh8usU/U8gwQwLAjzICvODkjaMVQysSKkZuJwL3ciqBKBvGUin1KHcpqvk7+YXLNT2tpVXlNhLPPYwyWruD/o38ZxU+sgwt2q0AXQFhNk1Xig5C5xL6BpecR1IZ5HrRC4A3AU5+BFLecGePb/vxzeMk9O2YfocdCKDA0ci4gaPwS3g2NFtkJDHEuhvnKKwiCIBwgitKJ9I4gC7EdmJEV2oantjdQdgD+wNowmqOTTAlTgMgM9pRkQjjZ7q82iHWAHcf4+ukMOHZ2B2+59Ad48cbjwUqJbWtltiu4e6U1+Fc+hvtf+nl46I+3/TmZqUj3KDRk7hizBZbBCvC8V2m84QvX8EUn14EiSFoZD97EU1M2vBjCUcfgfF1+q9oMEpFtVRMZ3PcwF1aH80YlLpD54oUGQbjRmE9k2IeXpNHuBKETrJTphHo38lApJ5TKvsSZU3jNdsTG7T4rF6TUH2Gu5cQyZFvNIiO86EUaX/Gyx/CyP/so+hfWwSVXbUYJuLOs7JiWh5EDVwkCvuLlz+Kpx05g7WKO0WD3Po+3nh3hm950AS+5exte0g5d8mZ5L6Rc+UIBIjy5PDdhElkkMBzFmFZBEITrhM3j/U7rtdoV12iOpF1hB3FI48gZIWGndpoNyIqQKj7DDGSZ82smFyfXcU781saibFFWwOqxMb6p91G8bu2T9QMAldgQe/rGy+1I/UqvwP/0eY/j73z8JmSafZJOVVZCg0uIqrPZ3pNu+07nAmB5SeNr3rCFL331Jm6ZbBqRLs5Z5eytWFwI1wHq24X4mSQiT4Xwd/85WDZ1FCtB2wCWFRqKnkJvrI/a6yII+8Z8/taLbj9nudf5dcxId2Ndorqy7KZEnOm2d+TaxBsOp5aXGeGFN13At93+h/jCP/sf6F8yAoOjkeDRfiYXRZBF6QqiG/vyhz+NlZ3tZBm6Ps43nxnjm954AS+7f9Ms0Nq47Wk2f8tgykptG5gwbtDla3DbMarGshZbyECeAYOe+atU3SAQBEEQ9o2rZ23+nilekNNsi5mi/zTvyoNmL22KExi8UO7/Ozo0BqaMt4Uvsp9JwNzTRumtfdn4F9qYXS9hx1FwV+z2HxmrvRG+tf8RfMXGg+l9KgIyAtl/cP/c7AnhXyLQZILhn3wEL3tV4Z+J8L5mpUZWMrKSp8++EhYztMvJJKiMz0sTUJzO8c1vXsPXffElIzCEyRyTdlPkuRDbXOF2TmhwIRJZYCgm8luZz83rg96w0znPhAiTwZFzHheEfeXaeOJtBc27cOFq1MFHbYqpjrBTSGsLr81zITu1ZJkrPP/mK/jW0x/D7Q8/a+d1Nuu4HAx1jAXnPNgawgKjHitHQHlxG8/cuoqdJ3OfkZkArzhztdsamWJ8x9ueBQCcPDbBi+/ernsmhIkeU2ER8efYhS9173q5ERi8agK0GoKxEi8IgiAsjP2qXWfuNxYiaJ+m6gb2NA12WEaKvh9ZiNCIj6DAgEicQ/IaLVgs8tNczgjlHOYTfNfx38bnjx9u91YAjKgQTsdoCh2UH4By9hPhpN7E67JP4k+yB5BHNnJWVs9eVjIoMMzKjMAdvBt8CQKxgb44w/e86Dxed+8aUBSBeIC6gABUtlb4OU76GK/HbHMvZFUeBufREBYsDp2oFVwBq2dx5AQ0QbhGOFyR4ZBU/S4NKtskl7tpfBmmMt1LwqHa8VMCwzWKO5NSEW65aR3fefp3cevOBfDYTf/INYGB3ai/3bgmMMRtgsu5EHi0AMDf+tY+/ulPK+xs6HqujaDxrV9dxt/7S0/ihXdtBeIBmio6MHsqyrZ1wu9EgQdD3sx43CivO18RGQRBEA6a/c6FFNbsjN3ZId0ORPMPVnB6NoSjJDKkkoLvmpZrtLB70jFHVK4YP/j1T+L5H/hc9VCED4fTFJzA4GyhthLbwR4CkI0nuGn0LM694sW4+LHmmTHM7CGAGejLrR2lFSWLTSVXORmAutcHEV75TSW+9s7n8JLhBjBJzAQR57kKl6cGdXTigXSurqEHg4ryK4SzSsR/7eedfgagqISgOYjf3cF2Md8OhAYXblvB+qm6d4nSjLsevHRIJToajJZzPHf/MRx/bgcnnk57bx8GByMyTGnEFlFRx0qzTxQZM48Xw5wde4YVBsLD7UVo6HL83RgIR4Sbljbxd27+EE5vXQEXofqcWDkQmknBKPDut6AhczOfOHXeqfwv/O1fQj7+agC9dGGizvsPvfsx3HnrqF6WVGM2S2CY5r0Qfx/0K4GhzbVyluhwRHn8vlNJF+NT57dw7Mro4At0yFw5s4TL54xb9u0PX0F/VB5yiQRBiKm1386TErim291FEVbnZV4ZPiYBZHN9jjfaZ+IklF1tsf0Uj2Jng3m3vXD/Ku7+vd8x02u6aeDbBAbrXUCtnpDunpC3p+4ZncdX4FP4ueEDGG5N/KqTXjTo4RKtA8gLjQmh5s3gtQ83oEOohU+sfq3Gt93/DG6mnWqQRdsRNRdmGs8i0dV7IUQFIRI174WEfZW6TnbZZnkFWh2DcmEZQDW7HQBqG4yk5qwiqpR8DPMyHmR49CVn/Peip8C96CoWN3Z9PBkoPPHSUyiHCkvrk9kbHCAH5slQ01MXnX05bPRnGAAH9oIv2hDhozVSsFsYwNJwgn/4eb+O4dU1IzBYDwZdBB4Mtakq3YfgcygutF1r6wXBF69i3FfA2AhB8SiMdmLEdyh8x5nP4dxgVNd4ajGA0XGSCR+jUYnUOiFLQ6DfIoCE5zjt+xHliXtPYjzIauV1nj7P3XYMF25ZxW2PXMVg58ZR+LUiTPoZoAhavFGuOx554enDLoKwV+I25ToWFnYTMqGjAZWZ01cCh9dmdb13+32P59h/w3uFgP+w8TPg0baxg9jsz816QVYscAJDTVxo0xkUee9P1ozeeILXvugiHj+3jY/8eg/uoWBQ3bOGgCIj5CWjzFVj2ni/XiL64K997wivPP0sjpUjnzjTrMxBLoZAPJgVkhom3w49E1ySR/fXLatdgJSwEJ6AMSCvjivbpDaYSITltRF2lnvQ1h51gkMoVoX3cnulh+WNCTaP9bG6Pm4eX6hRZoSHXn4TdChkknvmzXdmADnhsy+/Cff98flDKOXhw0QoBgqKgCu3L6O3VeDEMzuHXSwA8yZ+3C/iEd45mTZv9UETl2SvJQu3b2uij87ZT4cB5HmJH/vC92O4sWEW2rmFOQqlc/ipKr0XA1VJjMK8BH55dFCtwRONKy9c8b9pVf+HnPC8t2q86/ZHcW44MsdpJBdyyYeiZXEIRSOmMEhilAq36PdMLoZYbY+JMyG3NeyJf23L9/u5eer5J0xnOi6DNQZ0RkaVln62cD1xjQiAwmz0QdzKAxQvkm1Ax+fVrV9mCkWuMOllmPQzFLkysfkzd7DY89xtOxYmf2zb74ESXxd7P1xbqRXh3/3gJRwf28TTcQGdwNALcw/A/Aun7U79c1h74vSjT+L0+AmoY8Bgp0B/VNrZ3eqCm1aEUT9DmVHr82NmHTG/EzFe+44JXnb6vBEYwum/awkcg+/xoE1sR+lqgKq6YPZzOPV3cE07JeCshU4wfvmxpUaOlDBBptImfMh7NAT3r3YvAT+osHIAAkObzbdo28/t7+rp4cL3z4SawOD2HJvDAKDzG7vd9aJLRq3C3yLpeo+PTuJHW+LdjtYfpcdrUZ2meNS9oW6jUkxTvx01lvMJfvx1/w36/DZQms4/GODCiAysAV0AoGYlQipwBQTa1fkwYzDDNDSa8U//4mP4oT85B+zUFfasR/jmbwa++gVPI1+zcUyu4YrV8thbIV4GdMvR4D4qMlNV5sHUmlPc9qqLUSnzWhFUMCPmznKO7RXjFUEMHLu8Y6ZyJWDt9LD2PA12ilqM4PzPD2OpXxqjgwmjcXVhNaXfAycwtJ6rIFzDaEVHSvQW9oAdeQ7b1rbPe4L3v+0On8iwXvY5EKd4M3ihmswUhgx4I5ad8B8sm1qIBZ8oBzej830Jkj5Tan238ABJ5bhw5ShfkWH8L38JS4kykes022tPLheDw+b2ahCMxrtDgQgoNU59Yg3HntpBaVfqTXQVMoH6dtNwdpbKGF/9F3bwda+4gOWtnUpgiG2sWHgAmgKDI54GPCxXnlezczlvhmmeoPFvQZkYhI890W99nhpT3vqQXfuMBQ8X23s1umWAU5e3sD1KT6k+6JU1fWSzzKE6hgP4QRwA42GOrdWefy9Dr0li4PSzm1NDiHy9MIO1U0Nv1109Y2zM3rjE0uZkT687o9uUqYLhoM3pJ+471Wm9/RcZpiR3jLMTpz4fBAtv+4JOFIMXY0S4TluiUjVCw4KOs0+cPjbGj3zRB8EXt0zCIe8SVyV6ZG0aSVPHs/deUBmAXFnxYcoZpn7SGiDC/b/zURT5W5CDTYVNQG+J8LY3jvCW+88DmxtekDAFoXrDphONX+1vtCy5blTWQQ8Y9hMNIKY3gu53pfDMXcdx82PrAIBJL8PWatAgMmPj1BCrl3ewcXJgnpWs+m20lGPcz6A0Y7BdQPUZN68CNBk3R1dqmN8yYvzvf/nPgDzHxy/ehH/ys7chs/f1uTuOYZyarilx/4peBt4ujuyzu29MqRsPEz/iQnaU5oiUMTlaasuoUob0IfDU80+IZ871RNiGK6pE5ES89cyR+im/7+cggTtqrZPhfiPURLFUCU38eTV9IxPVkvpV8f3VPuJzIdjTdzH4i6LmM12Vt5EUOioXZwpU6mYOhj2GTMRNfFca69pn7fhgGz/6+C9iCZMWRQRViIRP9oj6NU73ZROFNx3/t7zgs3iuPI4PP3b71POYpsUQAGhA94D7vkjjS1+6hpXNYIYu9xyUNh9RW/6qWGDQ0ahbfNBUmAQQ2ORoPn/h9+j6EgHPPrmCFd4yP9vnnlPbAuZaMxuxgQjoAatLjBWaGHvs5BDf/q1X8KXnH8I/+Nf3xZcNAPCdX/Mo7r9tCyCg7Pfx5t/5Utz636+YnBCwYl/LO8SKsHa6OeUm1/okgM6Ay+eWceLCNhTD7xsw3r1uPpH1U4P53ldrY46XjO032C6MDdF9D57xMMNnXn5zp/QZwuKr1ll0nc72QDwZaue9aNfAeH8uRq3r9iljocthux5jgUKDVtRaTqVN5bPbF3o/OXeHxt9/7Z+i95l1cGHfBAZQMrg0FbIJlyAXBgcXIuEFBswQGByBqh82MuX5LeQvGYA/to2yl6G/DLz51SO8/dWXgG3nwRAp6eEUStOEgzjcZ1p+BkeeA4OgAo/dF5PnRrX1qb+MrNC4dG65mpqTjWjjjSkAG6da5nlmhjrGuP00YQgNPED40dcz8mcfA3ZGTYPLuyfayqUwM4IwGNurQ1w+t4yTF7bA1p0SVDVu016vp59/Arc+cgUra+Mj9+zuhiI3ISD5pJnkqbQhImSf89FSDmIG2c58Xhxsb7nIqZEkrcwVto71wQCWtiYYbk78iMckr7Kukq6yjO83k57CpXPLjTAhBrC0OcHShnU/7VBH5JNyT6IEk0lGVYfQGxmvoL3MKiRcwzBm2hHJZyMUL/YJrah6d2wbUT8+Wj3POOigxXVFfWUk4/AdXmjwbqsLqO3b7Mn4/d6lnde5GP448YJoPdsz73Lmt95K+Jv06zg92TShoG40OzQ+sxkCQwcomuqSJxo3L2/ivns0Hn1cYTIx9z25V3c/qVkvMwGD2wif/8A67p1cDX4MBIaUfTUr/0Jsc7myu5DT0A0g5cUQkgqpCMuZ5bj53A7WiaA0+/Apjt4FsgMGDADHMtx3ZoRsPAZuIXzFl+3gdeop9Nc2zT6fNWX/J9/xZ+meobO7rAtv/9NjrJ8Y+BCL8SDznXh3ndvOwV9GK64w7DtKhKKf4eKtKxhsF1hZG9vzALZWe1GIwgwxqeX30VKO8VKO4eYE+SR8IRn5DM8MBvDQAzc1lk67lZoIO0s5hruYwaPIFcqWcAvSjP74iIxiTCF8lCYDE8qU7ZN9Nu5nZmC7AwceLtGo5PcoOvjOVbhMc+cLsCucUdD1EHtUxx3TkisZt3lzjKMkNOhzCl/3liex/KePQ28XxlAZay8wsI4uDQOkjBeDUgByZcIkUF8HAHzcoWt0NdpzFWwVePdbn8bPPHISS7cCr7uvwDf9uYvAzlbTTS92zZsmHLSFT6S8FxxEJn7ShUnUxAPUG4q2PA1EKM7cCZC9plbIAhHIuumxfSZq9mQfuOWuEvf0xuDRDgb3arztFYw7t54GignwKFcNc9jgp66NXWeDhvgY346dlR6u8DLKnsJ4aBvB4Hxid1p258vAM3eewM1PrEGV3IhXLHLlp8/qjwooW6TxcPoQTX+nbKboUIRJv+WaMjCYMdPDeJDV3sHUm7Z1rI+yp7B6ecf/OtwuUCrC2qkh1k9Wos+zdxzDsSs76I1LkGasXq2fe1Zq31BMegpMZNadWsommozHSLOsvWqEM2rBCcDOcg9gILPix+bxyvNGFRora2P0Co0iJ5RZJUD0d8x1TN2jWbWT0rrWqI8GGa6eWUq+2wRgZ6WHHRsiVIuhb7FIVtZG6I01hlsT7Czlfj3SPNVAYZjEXWVOuHp22S/XZDpwJ89viRfD9ULQKKVsjL3s79AJ49Jd+0CVbdZIRhu1SbGQ4Do6xOynLgz7wbVdWXHVvCeJDtY8pwE07ElOLDsQwms07V53tQWPA9/w6udw28e3gYsASm66DrhQFXdsn3/BDMhw2wBHhE8eWZrz4ELjLS96CH/hC0/g3/3mWbz/D5ahNktThviU0X6I4THgzS+/grff9FzTU6EM2tnGFJVo2lOxwBBeQ/c8Z3Y2idCbwf3uCzzleUt29jVe/4JN/MKvriIvdMOrB2Q8SF9+1zZummxCE0PdPcB3f9FzWDl/yeTx2gpyc5ECUAbnxTAKnxMVqDqfIG6YMzLeqBG1BJMdXiVOiFDjQY7J2QyqZC+WpMSD2H6jyLbk+Prae+nCd+0ZAcxYXp+AmNGbNDvvo2GGopfZ5Kb139K3z6xY9hQev+8Ubn/4CpY36zMsbK/06q+OZvTGJUbWTt080cdoKZ18PZ+UOH5pp91uiRfPeOcUM5Y2mzNAbAcJRMHAysb0vB2bx/r2nhMmkZ11+dwy1BZjaWOMpa3J1DpxPMiqGYKYMdxqehUXuarZzVfPLk0tW8ih5WSgqBEHsKuGeDeZkfeLThmWdwsF1XnqTWM2cxYTvEdDbfNDuEajQYaX3fMMbv+DPwJtbdvGhU0+hrLKw2DqW2vo2xAJygDkZDMgRyWPMwkTUBu+j1EEvTXB69Y+h8e++ZW4/cQ6vrJ3Cdi2L3GqMw1U0yi1CQdxQxjvC9F6QCUwDIf1RsWdR5vAEDY+9rNWjEkvQ08XIDZeIOa9Ivu4MIqewotePsFLcAVgRj5kvPALJvii/iawftUc9IoTKOyxyrJ5LeJRBmbfkF/czPCfPjKEIo2t4/3po13ukgCVUaTNs/vsHcdBzDj97KZJqFRqKG3OYWJDL5Y2xsisCr51rDfVaFjaGDfi48tcYbTcMpMHM/SMhEzbq/0qHhmJhjVg49TQJBZloFwboehl2DrWh2L2A23mPNmXcyuK8czHpR8FGC3lYAKGW7Ovr8u1MbIjHlqRH/2I8dPOtfjb7aykr5fOFbaO99HfLjAZZJgEs4gsrRuvlK1j9fOZdc0YRlgJp1DbWu3X3bOn4ASoafXwxgnjQRSGEQFGNFm9Wp9StfYsk0lu1Vb2KzctJ5cL1yYqqsZrJDwod+XFkNpXuHy/O8x2wMTbLtPeTZry7rqRbtfZnTHw1zbV5TzEl4ZQ73TN3N6Ji3sUgBZ9ixjA5514Grd+9HdBW2vNnjzBCgymMx3OEmE8QO15ofKirW0bF9qbNSaBIUqG3iqgPvNJvOMbX43/9/gQk1/fRm9kbqprq1ofWwCDAePrX3sVb/n8C5U4kbIh2hI8pmyr0OvBn4+1i5QCsiAPQ+ghuhsC2+z1txP+4809jNcY6AOrJwrcc26Ee3oj9MZj7AwyvPkLr+Lencug0goIz9hzqc2YwYAuo2Tg4YXjKleGsp3GYPAwhfL3ruU0gt+S7xsRWJn7ztajCdEz0z57yIxrG/eH3P0lwubxPsiG6dYKC2D95ADbqwMjeKjq7NoEBrPc1GHjYY5n7zyOU+e3bJ4TUy+vnRrUvHFUobG0OcHmiaZwE1P0Mly8ecXn1KgJrgjqxEigaUOVGscvNWd+uHp6iNINBDGjOL81dT+Xz634Pl8ReVaqgrFxYoCt1R6OX9pBVmrvKVwqwvZqZdNtr/aN7QaYEK0rO40HajzMsBOLMB0rvv0RGVwlsIsX3G+h2VSRXdzIDwr30jC3ixsLLmut0ZylwpKtu1AfjTAxy7wrMabNYE91JEMVtMgIL735WXzj9u/jLK+bR4JhwiNKtnkZUJtVwh2RMoBysnMco3qx3XOVqtwUVQ99+HP4+ZHn8Je+5ClgZxvYHpmQhbChCxu2pBtfWEyu/6stT6wHew6KzJSVeVYXGGZR6+yYz/0C6N/N2H60h/64tJUcoVxS+OIHtvHywWVkZYEXft4YL8JVY0xpbdwui2CfTmCYJSw0zt3ugzJgHWArisSuhPEzVBMYAG/oGhWXcP72YyBm5GON/qioubdvr/Yb1k1bxb692k8sTRCc39Zqv9UbJsa2be3eRe7yWg+G0DvE1R+Km6MHIUU/Q9GvNyBtnf7asTPzPoyH2cxnbNZ5TKPIFYo4+RWA7WMdr31qn/0MG/2ugcQtdVSH9yoOI9K5wtqZpZkuqNUxnSHbuajCEcfdV8UcjNB323DWY+Cq2XkKcyCP1oz6jl3T2mZgB5jcUB1Lzfa/PdhMrqNTyysRNKvx/avdA0XGozJxjzvfq9ArJFwWd+7n4MW9p/EtVz6M5+UX4UNL3a7DUNCsZRCmZipQuhzRJt7rQcMMroxK8NVtLK1t4IX3DvAnvzMElQVIszkcN9td196pDPiSPz/BW156KQjx4HrHmuNEj6ivF4sLsZ0Vn0vowWBOPPqL5nOWFMuC5dbz4Jbl43jt60e4SzGGxQir/au459QW7qFt9HdG5pw2EHlrxLNlROdl7eGax6y7V85OcOJJC/O0U87zIjVlNoHMYIh9lCh6f3eb783XGUH5wjtIiswghH9X7SCpoiDen1vMZG7cYqUYJRFGSzku3LKKrNQ+DFWV9dBVnatOAoMvK8x10Ki/77Ht1OUa6UwlByRMdci+X3D53ErrPty1dccn5pqnhQ/BBbB+egjS7L0VdEbttrEirJ1emvKuzV9X75snA7kOSwtMVFeQo5EAYvauNc49b2bWbl1/E/bVbS70LOjArjwudttYRQ+CG4Hootjr4IX3y2I395aTcJWT0oyX3nIe33TLH+Hm8dXKmNCcCJOgoMgMlRuBwcwkgUqVVuTXrWUlbhQiOn9rLZAiFE+voff446A8A4aDqlyuEfAZjtFs7OJGEMF3vzwqS7wuEbCybKatrE46Xe5ZLzMzwAW+981XsXW5h5t5BVg7D56MoHPg+ed2cLvaRFYUgIvzD4WattEDrZvrhb+H5SMC93rYOXNbc4CkpUELt681ko2QGAL6po5QrrFouTYMmyl8N0a5rVMa5Z+2SWCUThUYwnsalJmCRoF4f+opF64yjcb0fB0bkJTx0HW7aV4Mu2HefU671LXncYqnBceGbMSBTHso7AvjQY6yp6q20tYpVLIxWkODkhlZwSCt3UzM6TaWAWVHleNfiV1HcD+NFexKrTDPutu+rdEnv24Ypqo0L/xd70JNUIhtzK7XeZrdxZyeCWJBvOB5E3z7857AbQ9eSHSK7V+X6NHZRe46WzvHe5MwgsGSDgd3+9cMnpTQG2PQpz+Nl931SvxBfwVZT0OVJmm3sjZYvFutCA+8vcRffOA5YDyprnkoMIThESHxAAfC8k+x91Q2O/dCY7uWddn/Z46daSxtb+A7X7CNM7yB3toGMB4Bo8BDwZeN6+VNzZjhE56HXrJIiyAAKMuwenWEneUeBtsFdlZ6yEclmFDzHEyehrflURcYUvaTe49V3C8z+d7cgMjcTCmfbyddOQMhxA28pHfRFB680NAzQoDWBEAhgwbtMS9Brc3fR7xg0UF4bfQ+GWDbyWSGnU61WoPJeHB2PosO71JXO2dhIkMsADSfC/ZzygIua3m9IvfPnE7X4o1GA2g0HKoM9j/vScwJW7fATsc5CEPC0jb6EgsdoVEdhnqEiuJcx4U57qn7gC/9/Ku4+TOXwUWVPJFDgSHwYnACA2WopqrMyDSitiH1UzVxdd3rB+fWitqzXQAXrwI3nTTfw4q+Mf8yN+9XTV2Pcza0rOcgMuJCP+Hi3/Uyu/NjBgbLwGQHX7F0FbpfYsAKOL5pkzFqMxpRJMoVN27xuYTlT4kL0TltacJ7n101myHR4Ws0aPAugeG6ruPunkGlGWVGoH6GvNAgLv27T/E75w1drj3fnXDP/JRVap1Pt2xKZ7QhMAT7Cb1H/M8HVS8gIYrMYZjNGjXpdNwp29U6NfPst0NZasJI4r5MvZ+NkcIp2wvXNOpmwNUGpslhMBOYCZqVea19+CqjZIC1c3E1f1KPgi4BXSqUE0I21sgnGr1xgazQRqhwIkVLufYcFmrrVlZpgdR1nBvvX+voaPMQqjSii1aVd8BUg3kfRAhv++xjlbofr7pWQH6Scft9T+P2hz+VDjkkYxfV7CG3POywqfAzdRIaXA4HZgZpAgoNPdagpy/jtX+uh58ZZMi2SyhlBAa2IX6hnT3pK/zw31vBTfRnuKnYNuUKbalQYPDL7cbTBIbYHovDSLOgx+k67F1os1Oc3aM1sLkFtfk53KK1CSMtg3CHWlmj2S+Seb6CbWKb0dm64aCEUsCwj/6OCZnURNBUCXhLmxP0gtxFa6cGdZvdDeQo8m1Y0qaPrl3DHHE92hZxbepg0jTCSx56WxChzDOkH9iUwFDV1wCBe8CEMmRKAxMArM0AaMc+YfJ89kks3a3t0Ngmen04UQd29cpb9CDJfCLDjFi7aWVzo4yuYqptE3oxJNz655l33OcjcBUFsH9q+pzeDHOzG9U8IWaEBnQ4mhK+TDWRwVVIRA2jw428+o5UgCoZRa5wRl/Ay5/+M/A4iO0vGJhocMHQPgyLfMXmEz3aEW1y/4UNqneNrIwuX3FndvrJNjGHAZ5oFE+to3fqGDAp6kaO82QA0h3xNoGhbeqlmDwDloftL0nXZ9Q1PuUEuPQEepNJ1SCGjVnqfMJzSp1P/J7FjWTYENoRk4nO8bu/ewKqmiB6dqVJdbe5MGOzX+bmdmZqKPCsqme4NksATRktnFKWmFTF72bLCEnNUBALDG5ftU7uAbFXUcHvZ5/FBb/uHJnRdyUwtAlCifLFIT8zhYUDHrEV9ofesqnrXFMCGDEBqPcBphFrtrq0scAlwGPCaNLDZEdDK0J/Z2I8ITRavCACG2aPYqR7v3wNGZwMq+ayFNPel0k/w9gmbWM62JlyvAjj+qH7PFtHdeAF2Ji2PbxtfBnf9JnfArvZrmrroPLsdG1R1Ca5wZnag6p1c/aSsKje1OGqkwl7HccF9NURbvro7yG/940Yf9wJ/kA20T4ZMDFj7fQS/vk7n8b9y0+AtnfMMcopAgPQtK1qNkmLwBCiyORhSF17rdtnjmgTcMJtXRknRWVL1coZrBtuF9pXrQJLdC7hlJvOI8MV5/QdADE2j/VNskAin5tpdW3s3zEnJsR54VgRylhgaH1WXRJYqvpgzD6pvI68TTXVn7/dwME+tCJopaAzsmVN77xNYGA2IRMaBGRAyQpUMpQisCbbf2WvmUwdRJ3XDrFo6pYXajfHQrzPlD3TVuXaC9RZQFigPTOHyGDV9iqBeE0tcYb0dJcaNi+C6xwy/A4IbLwCmL2bB4MaDwIjITq4BgbmwXG5S2oNzYIaHaO0TUni1MI8QskiqD/gJubKXFvbeVNRZRRVGL5CaRjXXPN48EdgE/Nz190Z/tY3MHq/uGVFBjtNJQPs8jC4h8XUYmZ7BSDwYnCdWC8whOXLyKjtmusiSKoSDa+7AnhjDL6yAcqyegZioNkZj/+mGsHwtzaITB4Gco29bQDDhtB14t368UvO9oKV1jOkHFflDRvzVDIhX8aorG2N+rTzcgJDbrPy6xx6PdGLb+m81/6GDWKqUmPjxqetEu9HTSrr38QS2p0qTuyHuerkz0F92rb6MqcIa1sJeCHQtYsNA5zsfsj/C5mnLpm1altyp13ve9r9mbGveUSArgJDrYwdhAuzXrVu9ewF68X3K6hH4mvS1sjXyzW1WMIRRin2/TKgVtVMC4/265h9hMsIKmMjNBBMQuOxsXdcvLDHWb9t+w/snK6Exi5pgDM03gUASE0PXWtGErYCE0FnZto4uk3h3D1bWFrdBGBz8BHDJSTXDIw1wJqMtyIZt15dkvF2J/Z94VKTHTC2bs8MOwhhSjQeZRjtZMaOILOPyUhhUihkGYM1YbStsLXZA4+tV6tm5BNt+uk1m4DMrAp7GSxy7SoQGMezNwlXvWmwhf/tJb8J9blm0jVXf7n8C97DE6jbRX598p4J1cNYeZTWHupk4YydhpLBkzHU05fw9776An5kfDO2nyZkRYn+TonB9gRZyRgNM/zT79jGC05sgiZjs/9JEdgkgW3iiPMsTOvAt5UznkWi5VySAkPKvnL/ykAMaRMYOHFOuxEY4vNw9pVdvrOygos3G6/vST8z7521zddPDqwdUtkl5m81OOjf1WDgMFzXX5bAdnK1A9tlZIWG5OVdkNBgwiQqgaF5PGtrUfuyUGgAgJIJnJk6hDSZQSEV2gXNvqUpz5wn4OwEd61Ce3G31yb1yEe2mKvbG/aL9bzjhri4d8NkN2EjnUWGF7/iIvoDjTxjrPYZPXch7e82Dw0AYLMwuf1Wc2DZHmFUmuXbJWE0IoxHGcajzLx/pQLbUHCVMbQmjMcK2xsZxjs5ikKBR1XjoBVB56bRZm068EyE46dHOHtuG3lP4+LDS9APTmqjjXNdnriTOc92LTfzwGzPxvGNAEA2Z4V5iRJGBXV4iFpGqp27E61dQv/9vw4ea9PIlQyeaOgJqjwM4XNvZ5LwYRJhyUJLz8ceug3tnxmDJQ33T2boi1vIji3b5Iu6fiygbjQkp7IMvqf++pOz+1xZMp4WUTmq0akplVGr2IGqMdPBRU256YWNYlhGn4Qp2nf4u7seodqe54BS4EwB1K91/FNvS60iVOEy97l+8tOewNA4qz/nNhYt3mAPFWvYUKfCJUz4FuoqQWKEL9yHiTWsXyVNQJc0h2GHYSrzKOO17Ra3vy7l9OvOKTB03nfQKMfbtgkLzlhAtL6nrTMmwsJ1g7Kd4Pmx7VetD2dqLCc0cAGosUY+1lBazzezgW0v3BazHrnqmW+2LzXxgdFebys7R4HzLLPt8HiYY3RzH7e9YAN3nNhCLwdyZap3Pz6BerNqimGOVA3CcyMdT1yaphNZGc+m6IlnFg8nNnj2wgCf+/gy9JUq5ttcm5YL0JF6KxQsbOvD20OG3y/esYTssYvmmcmanQOydWRDDKLg98x1VN1mQZ2WWTfksExEpu0KRXKY1aAA9BTUUg6UjNf92cfwK9/6OnxkfR2//vgQf/DZ47j06DEozfg7f/E8XnDimcAbOQxDjWyUNnujVXBouYhK1fMwtAkNoZ2VEhjiY7oBm1hg0IFIUwv1iM5pathEsNwJRXkw2OXuXXAuD69fQJmfNYNqM2BVb9e83RXZIKlr0KgLwu06NG7zNH/hHXVldIKl88aoiwzsi1wVm2uf41KYgezEsYmSZW3YVvMMzISXMjXAVPveLA+AxmyLzeEN1OyY1L48LrE+URU6tUjmtKk7iwzPP2t83F2D4AabYxSAYHYMv/4gA0wScgaOuSegOVforD5u2MdzuAkL3PFLBtZPlPizchn4ZNleUU2hFvcddyrjMrltbIdxUfG5jdjzGbSepVU2OVPdRkITI3hdOHuS8b9/5xbGP1+YyjoWGKKKMnR1ojDW0C+kmnpFNYslON8pXiKNRpkBHpfgnRGo36s6zmxU4qava5yvAO2dcvd7eMh+rxIYwoYuJUa1CVSpOaJDFb3msROGeiQat1SoR2tjH5RBKdsg2r+ZmWt05/mfD1eb+wq35fkJpyGsFHXyzxyc4WfXIZjnUWdVjpBWjyAisN14of29sHwJbMrJ1oYLRLWR8fAV6PKONRqVRajR4ZcF7HOeMtbWnSM8osu+kwTPYi08TDU9ShCs1/Uo4TMvXPu4NmmW10JMJSiE+yLbH7F2Acyoe16W6E1KZKVLuDalJvBCtD2OataTQLPO8yYLBQuo+hi+S9xSJ+0c76H3QuDELSPjeQAgzzT6AxOb75wBM9sfcf3c0LYNX4uo+fa/uT5tvH5qXUcefA+FBT/NvO075/byMQGDQYneUGOkMjh310Un3e20v9o9ZZxamuBfjv5D5crtTois/eLCJGJc+Gh0gQh2m3BkWtvOYhk/OAQo9p1YdqNAEwaPSpSbBfTVMco/eg74tc/gC4/18SUveB7O//k34leeW8PNSxO8drCF/jaAsrA5C6JOutlx9TclJgBNeyQeYIk9EsLR//j3kJTQEA+mhN4WoTBQanM9kuG0ifOKbbJwICc8ZpvAEJ6LInzm6hA6U51FT1aUbN/jd762zTRRfQYEmEHexPJk+YJyhiunpgXXisAlgMCLyW9CQf9BweZ3My+N1mYkv3Zc6+GuEmUF2q9NW/nDsrfbhtP3W7sW4VgmmVDcVJ0e76/tqQjt691YJl3vZxc6iwyu8nZJa1OENmOsTruve7XFYlFec/NYGYCVJY07b9vCY58cTO94zClAxK7NnBGo5LkN5q7EfdZp67nyhN9TdEnU0raOU9xiAz1jxpmlNUx+8YPAWJuEjyWDbR7ChsDgZgzJqs8AanGHyRIQqsYVMKEYcVlTSpT/DeBRAb68BZxcBg36QWPijmEMP9YMvT7xz4haykH9rNn4xcdyN02paiYJv9943aBL3ahVgv3XxIWo8Q6f4TbvhWmNee1YwTp+lMA2iE5ksNNFaRDe98glACeDEZzAcE2IDa4RZGU73JHhUBnFtsFw6nxgmLUa1849bAGGozfMFE9/r8nEOKemQAvDNCqhwX7u2KAtSlhw+3Nl2JOHR/y9Q0NaW3cXCWXn/b1yDzWfy8wMs7KPZwZ8CAucsWT3pU290q3DABEYriMqY9ZXRJ22I9chtGjXiSXyVRdrI5Kqkv0UY3HuAPcsMoJ+UVyKoJ4kINnexfWve+59vTRNCFRmVHH1eROcvHuMLKtGDUMvj0SaGj+466czbKk+3Xqh0ACkPbridXcDAxjtZNjZyQEynXk3y4/7/cDe4vCYRPjmb7kA/BefqAq1pI6ADx1tqDThugqAcjNOoHmxFIGgwOGc4W5kzgoNHAoQGQGjEnpcAgCKEYEmGnq0A97+HG7q/zq+7fVfBOr1oM5vAJOdKgQ0xnkHuHNPfZ4mMISvJJGJxclbcjGkbqJTmeLfGt4KoY0VLHPr1v4myhueS2o9hx+woUpgAFwlYr7b9Z/YoLlE7+oY9X5ATVSM1yX46ccBgEpdmxqWYy9ct57W5rSj9nz2zIL1MrAdmC1zZdppmH6MVgTKTJ1cFa/akoK6iDJzIsxslymUoEYSWJ+Qdg4bpF73YiG2W2wXhuUhNzNPS50eH6MtLKNrosfWrpIrU9S+zPsszpX4MUxa644NNDv+7rfw+vrGc4+NRYy7Fxmh5kKnUFe7p9GIiZ5TeOCmv9+e9uc3U+iU+JHtumElUrkhNV+oNoNlKnZ/biTZP7x225vuKvDDD/w+Jr9f1HIxhLNINEpNgRA9owwUJjsKytS48fac/MMWlZM1A5MSenMM1cvg43TcSorAWxPozQJ6Y4TN378EZhPGs/zyM+i/8EztFKbSy+2I/y4eePfMlLoe3xdPA+WohXWgWtftK2www/VjkSJsFF2j18utR0ZWXVsy2db/658uNyqAWgPinsMsejbDskeXyLi62WeXzVRNvXEJGpdRWEt1f1Ndgr1WMxTvcAqhkTqtgTENanW9Qje5RjW0wA6s3/cuYupq27vvU/bTWHdG/TjrEs9S71lVmal9nZcRkBO4Ryj6CmWPkOcMsgaLyjSUYoQjRFoTdEkoJgq8A2QjDR6318HJDpZwzWNGxmw7R6iL4C0YEaF6ApQLL9BWaABAqqoltFKgDGDS5nkt7egVw/S4yf5V3CoIEOwxp4xWGdvA1L/uM1C9J0DVloehRJN+BrWsQDS25+NeQbNyoJnW/ob14LRxl3k69Rx9aLQVCcJXlqyxPZkolCNCHta5ViRGwiBvLeMu7TlfnuB6P/C8Dbzu//2Vdm9MH59vvTzD76FN5FUgJAUGj6aq/KlE7prNANFEo9wuoCcA2HjgkCZTX24UoD98GL1iG3TfLcCwB4Ca4QRaV/YLUB03JS6Ey1MCgz8XFYguLkkitT9oKcKyxd4LexUX4nXD312ZvScoVQKDis7DntvlsRnw263Xe5u44Kaq5Mhli4GaqDCt7Y63rX7gZFLsGmS2d7aSCYFXKHOzTxM6AWSKa14MVX0c1M/kLrOdeSIDQBpc2MSP4awSXfs7ifL6A85gXoEhfTxbTuakKBIfw/2uFFC6XXC1bpdTbi1z2Fa0eIJMo7PI4No836lH07AOi6fRVKUXLTTEdVBcZ/r7Yh/CroecN0xh5v7mOLaHuXM5qqzR9jvBjhSbEQltfRhdHHzlUlXdvbZj1Uc9grlrbUVSZoTBrRpf98rPYvKRJ8CTqsL2ORhazsIbLmFCo5C2k3cNqXsY/ch9tJ71SGgsLxm8XUBnI6jCdlwZ4K0Jio0SxbNbGD87hi7h3a5YEybnR8hvH0Ot9IMLlLB8gEpt3y2usSuDRi8lHPj1E411mKvBN4SJ7RnNZYC5vnnmczCkePaRFQxR1G7VNC8G83sQgpCIfyaCd09i3cyYHK4YVsQAjEgE7Hpu5Phx5fg3d+i4HMFzFp5/wxWZgkfVPb9kE7PuqsTNsiWZ0uh12UenMAjqtm7qWF3CtGrX0rqXamVEBB7YjlRu6jvVY/R6GlnG6CkzSpiKtTdGS7N3oY8bsaHYJPA26p5YBJsYzdYPmqGY4UyZRYXLCQcP2UFElc1Xf3jjFmTaPdcZz6rfe6yBJdPW6YkClwTSyiTLZkbGDJTwYZdghiKuifDmNWmWzYuVtlkMe/pk2/2amEvWpZhh3PSV+b0sFCYTBV4i9E5MrBAXHAfVPnz11XJN2P7n1t+L3efqTHcpQttv1p3y6wb1M9m6Q9k8Va76DjsjQD3PT/j7XsIsXDs4PpHhHzzwUeCjZfok7D1pXDQK/gWheA37Ke7EEEDKPJ/+ogT3BgwzMDQuwTvpMFdmAmtGuT4BPvUc+hlAt58GhoPK3nEd+HCAxB1kmrjgC4L6DXbnklVelLtqLBmRsBDbVmxPGLsTF0K7K17H9Ya9R6i9gWHesVgwIcKFDQWdKRMKCmB3gcy2eJEYw4n2tn1wZDZ+K4aZeaJTmeDzMZTWe4Htc59llcCgaqZ0ahrLuNjcbNd3CZnd2WvQsRKbZf/MGOzxr2bC9vV2ZdsxbN/IecCEwoCrsxvH62CzxeXuenU7iwwpA7tjuRr7WZQNNvskaf7jtanUPEf83iJUlBnHqhv15oObMYKJUOZGGSxyBZ0bxdDEthNCV2LANJiq1DUBoZ6FNnrQCVClcfvcOZbjW+7/I3zp73/UKOC2cWDdfg6k2DZ4qBrL3aDIKjhUtzxqDyuhZiVoBo8YKCfgUQmdKaDUKLYZxcUxJmvaigsqqOtNeSdPbaB3xyr6TmSIG8bQ+nKhBb4MHU7SWUI+pjH0YOjQuDmmJVlqExfCdcIG0XljNMpq9p+XOunaBQQVV+rU7fLUQIRTpZmN2p7ZabOmiYXxe77bjh4DNeOsdlrufJirWx6cjx8JDNZ3o+ymTFVCI3LGPZupobJgn+HxGvVuW71AVT0QF7m9UTE/GqUcppOcsqg7kJzOihLHTokvtQa3OW2uKW9YB1UZqMu+Aq8A2ZCRZwxS2j9ProNUy/0SKb7h7yEKjDzX4CGhmBDK0nTKiIwnBGugLJWpK0oAY7KhYYRyoRK1cJDE3i3doWo0LeoDKfvMZTmhP9C2WqdaVQu4GQFQ1XMEEy8fCB5h6EJ8fABekCCqnn1F7F2LKXjvTXnNdk5UmYwZ4xHQ65U4fmpcGzCOjxZ6MyQHwKLli7D9wibNfZ+6ftAMZj2G6gO6sOds3ZJ9grQg1M4Z+LBCA8JcGDYsb96Rwvg8vubEJ6A/+jn42xtX9qE9AVTxKakBGaAuSET2mhOzoWx9H3pz2Kk/eFSCxyX0WEMX1XUKzQM/alwCem2M8pk10MllUK9XrRCGIYQiA9Ac7feFiGypRlsYPIhd7Kl4UCQeZAk9GaYN4nD4uYP9lZpJw5WnlwdhHhR55abP5+LVHsqMfD6yRU7NOlf+tVmdvXCE27bVc70T1jZi/3yHbXat1J1vP2W2zpunHAkWd8U7Hs92a9rKkRIE4sEsP6tIXHe1bJ8sx6zfO3rFzuXJAOza27a2n732v9uIL4pWprOdT3hqB6VWvinrdCm26wjUbnrK2EYiTKP2o1GM49wIoWtRNfWLWcdlfvbxTXlmFUJVeTAoqokLppPBIK5yDYTT4riTCoUJ0kaUYEV43Wu38LZP/KFpoGzjwtrkYtCpUAl7gSn0Ykg9EK6TOeOiU0ZBoqSgS9V4GFy5XK4Itqo8UO6YkEJdUF21R1BOGJdBvTECjwtQnmjE3IrTYgZnwagaZ98gx40cWjwSKiOpuXzGOu44gLknvdx4MIQCg1+32XWd6tJO1bJYrJpalWmGKoyYZY5hXfzCR9Ped7Zjh/79jRMgdSCZwDnlRRGIWuExOFq/UuhVVenb3xlAxqa8ZSTi1OuO6susTPSxC3QtFKM2glGtq7TLsl4/EYrKMeu4OlNGOWeuzQIUix/JmTrCe0WUvg9hvWUf/TIn8JJCf6lE3tPV4JAb/XBTjNrnrOqAVbsNjZnwAlTf2bu+M6M2sutH/9iIEGWpUEwIRbEfKZ2Fg+Dk6R0zu9WoGjqrqs56pwsw74mZajHo30UWorLCgN+OAbaihBMA/P4o9hzgSoyHFSw6QMF+Q8EBQCU4xNsoxtKSKZtJ7Mj1vHpBbddmB06rMhpV/y6oNTMtTX2M+z3LGDRgFGPlczBV3q8Elxa7NgMQUZUsjqiaiIHsFJEgPxVpXN8m62tbt37d1zLe9gcfQ1ZWnpS1k4xCIeqhETNOOLxIKiirK7w9aS4ZKBi8MwGPNHShgxxa7nmNLrY9AGuGHmsUT25AnbgKtTwE9fN6pzy0YWod8cA7oCX8sXE+LndB6MUQCw7TOhepAZvkjBGBjeW8GuLlaFkW78OVMcvqAkMsyCcFI9sxtLa8a1Nd4ld3uvOQGoRotMXBcxO6hc+cBScIq1D2mjW2cHYFMKWTRdHXsH6cJTBE9a5iuCkryR6zSx+wscdpz1WwHlPQp0sMNDfs32n7S/UZI9vSnYi2/TNrktplNqeFfzzNyl1nM4qnOuV6o9Q6sJiis8iQ8Lyqv9tdd7SPxOdc5oTxIIcqJ35akNRDpoOHw1289ud4ukdD8kFKvdwAylpnwqznvRFUNa1LuI4bwfMGPKrtAATTVFrD33kvkPN0IK94O2cDgn34wv6mG4F1jafbp+1IgDOcvnUH77zw36Gu7gAT6/KnzT8vMDDVZu3xUwEHhlN1nYKLoxmdAtEIVV4Gdtc6uIEa4EKDJ0ah57Eply5hGsqSUE5U1WfXBDcSlHoIxo+so3fLCrJTw/YyhS598wgNocqeSkIUNnjh+m3CQWoO53gdoO66SKg8GFQkMMTnYp9DUlQTzPw7kBze6nY9iABWxvtm0ldQ2kzB6hoLZ9T559qW1ZnOOiPvoePmSXfnl2xslXtH6uds3g34UCP/U8t5VNl8GVop41FkO+Cu80ywnkD2vTLrBJ2aTlcovFjW+Kj7cph4RHufvfuirQNc48n2+jTOo+OhmYyIUvSUP3d3vb1QSXVPh7ZpncLkmPYMqmtePzWzz4yQ9Y3HgYoyUNfFA66JhcnwHESdu+BzuN84y7UrUJYDWmv0+kZwEK5NXnzzBBMNTAplBjphqseCge0CKDSgmVAUBG1F6sk4Q2m/A4HIZduQXl/XxAFTjbuwCqqLFtT2fM5PLLaFISCxmOGXKwCovIHidbzgzk2bEJje5EV2arM5aT2T9O/TxhSS22cM6gM6V1bfDgwTADpD1cYEdl5p927izKsDemGBYacjtcexHhLKBUe78tr1104N8eee/RD6xchciDi0LyUwAL4OTHoxtODWJSLvcg+GmfXr6gg8YeiJNnaS7yNTfbpxcvsJLjYB0EC5UWD84HkMzqyCTq/A9+SAug1jr2nDCyAIV0mfAOp5GNoezDbiY8Y2Vvg9tK9Sggii312Z20IrAGOb9rJ2gQEIG6b6slJjPMrAGaFU4cxa1Xpdna7c9Jauc+o6pllu2s/l1QlOnx7hlmMatywDZ5cKZADWJwoXdhSe2SQ89swAl58dIptU16dhJjOb/lSyB1X3dkgy47ZOExiSdZWz29icb9vsEl0POksocDZea4iv/W16ZWfro6CDre29IwWTVyoz/8rCeDURgvqdAN0jTCjztnJ18I5nH5+zqjxQSVmBvKNXzVyJH4HqGdhNg7FI06vLtSqVwqRvk8axTUYSXxwyL2/VASBABTEs2s4TTWSSOLkaMdEHdTchNTNDTXlSlbtv6CGgVZU/wWXer15KVIlSXMb9aN8UqJ3xQ65VkHwvEB387tmNEle/kYsnBSHLNXq9EnnfjBjedPMW/sb6h3Hno48Ak9Kcx1iDmU3bUlKtj+uLG7YR7pq4Rsw1Xv4GahgpHlNdaHwb5T64g5Uaeqsw7n8TgEs7V7lzV7VlNKF49YbclTl+rsvNApPPXoJ62TnQIPH6KDLK9TxzoMWNWqPxiwSGeaahbFvHf4/KkmcmyaNTg+J74reFj8EkTngBOCVUofYsuudXZYws0+gPNVZWJzh5ZhuvPMd43a0ljp8/D6xtVtNhxUKBPy/tjRSunTuqmEdYYzHPgOEA+swZ/El+E/5/f5zh8aeWoUcEL6TZcms/ssT+HXSGZ3x+MW7+dXeuOqsEQZ/xmU0H3Hlo6JbszXPRKI81Ksu6Ie2ESoe286fHyd+mHipYl8l6VVkjkGHEU3eIMP9GrYGmZlhEsmGmFs8GIqAH5HkJCgSGsJOWEhjcA1+/XKkRkspoaYoP9dFn/5sGqNx9p1A4fG5bcdOMm/eGUYkMhQZ2SqOnFwwUJTDSwPqowNZOjrIIPQDsvwzIbAiPCvbHIOgSKLWdck27cIv5Hx62VnTquXPhH9U7kH6u7Z5qv1U2btPYipekbMD491l0eW9SHYmudrNSNgQqA0omMCmwZjs7mO3EuemywzbBEVb/qAQG5wlKzFBOmYK57359u16Z5/ifV38btz/+KZA2nftkAkYnMLgTdnODhufvHiYbvmE6AImLVCs0oEcFyos70CNT1kpQsG2Etiu78+XKrHJjDcy2zASUawVGv/Mohl9+L2ipX23UFk4QM+v+OQ+GsJ1sPAjRNqkBltBDtKu44MrcsK/idYL9hGVytmAsMHRR4srSJCF2baUiP2gApNtLrQjIgcGwxLHjE5w+vYNbj5d40SmNV5xZwvGdLeCJR8Bb2+ZwzCBoqIzRyzT6GaOvgd6mCforNGPCCuMhYXR7hsnNGej4Kn4tuw2/+AfnsHEp8zYPufI7oaFRSaTPuW4XBJcqaLOn1i0Jb0RmY2NO+qbP1PDO3kfqeezguzDxOrOIE14qpX174rzj8tDMt20JaYLumRwXXiicg2Si4+ieELq3VfOLDDPe7bb1Zi3fDygD9FChGGVQWqOgykW4hntBfCVilwfqIdvv4TYx2iWKDXbTFrfiOha13dhVk/HNqAsV5nj1lZyxb6ZAIT8NDOcEyhhZzhgsFRgulVhanqA/0MgyjbIkjHZyTEYZSk22/1ipZasrE9xztsBqjzHRwPE+8Eb9JO78yFOgcWHOcWIVHA3jcud7/dUFI2WNndCLoe2BCDu3DGMMOK8ObS9y0AATjIjgRgZ4p4DeKaEnbOILS2fImb+6NC8lon63r5G8scW10yACJk9voX/f2ExnGZc/U+kcBim0rosR06ZUmqWs+/K3NOipRjLcH9meoXdJTD+DZl2YdZaWa3lAUsqiVoQiV7jjzi1896vX8AU9BX76YaDUIDaNvoKGQolewRg+wVBFYXpsbrLguLWyRpUvf9i4x9cDML0BZqDsgbev4s5bdpC/6hze+8eEZ55dwmSsfGIhrc2zpH3SArtbf+IJaocMDDWGTwbnLrFZ3V4rMu+6Ivg4vEVWj64emLZPt45/IGZV0Mx1oYCQnIpUu9hf+5s3JLzoEKwbHjIWI5BYj8x17fU1ej09UwyIjZX6erGBMqMTptp+sx06VOEVwrXHSt4cWNaoquPCVcsw+t1YA9tLjKvjCbYKqzFTpXFmthoNvdydRaEZKJm9h0ShCZMgDt6JD7MwoRrTDfE8j3uxadw+wjAJ1xQ3BPeqT+412bbK5iDtvlTOX2O/MfK+RtZjFGxHdaEruy6jyi0dwNTamFGbact3tGwYJWmG1ipcHeVqhr/+wk/hjU88hGxz0tynEwisoNCYyhJAuxcDNyvNuD3WDH1lhOKy8WDwYRGgWlhro2NCqeeLaiuVa2OUDz2H7AU3g/KsxS4JyxIc0NlA4TY1e4TqD2TXzkVqcCbMFWGvSe3YqQGcNo/R+DxiuyPprhuVs61DxQzkGcbjaiCnyBUK935mwHC5wJ33rOENd03whtuWsPLIp4Dzl2E8MI14kJFGNmHkF4H+JethUBZAj6PzYqAAMEnYhjFrz+F29Ri+8d4hPvwVL8X/8d+eB73JPtm2st4sHaouj/eyqF2mqv1O3fLQ4yslPhIx1JChe8buzxaUCHIm1vYAYPo8Ha+Dth6QqaTDYchblqUNRcWMYqKgXd6oFttPz7gxqYSZs9qXacwlMrT2OToc/DDMLuceV/Rcml67PJHrYBpdElzsRSXr6naS8k4A4MWEsqcwWNI4eWaEF9+5hbffT3jZMtCbFPA9IWKAtesK1YQVYyhw0ENSIDLjL5TnQG+AYuUE1IXHQL/+IPTFTbO5TfjIJdswCevFYI0OX1kE/Vc/tDP1hE0OBbKqBLuGXVFdaODK84R3SjMFpRUXTD+WvAeD++xPPYizJWsYVhVb0AGxn5kBXQA7H3saS69/PqiX+TKYm5nIkjXjHBvufK6xCxvIeL0ungvhaIJfL/jscLW4S1bpf6PKAItrembQYBmrZydYv9xHThqnb9vBF79oA2++s8ALN55D79IV82wAIC6RP6ONXlCWzXNKXpfENQKqUR+3DiG4gQis4+p6stbAyCQyy0qNC5sncOGicSdTWejGZzZLu8W3FZPiS1M920070T8uLglr1ZRPd+WLXSPj/UZ2YTU6N638bp2uzyz5msNuXxdLQ7FkqsAQiQnTYgDjPB5u7uxM6dpc2aFA0EVg8IYA1T/H+0icPsKrXdunouTIr3Bt0FOMXvDdVcE6/ssA7Ij4ag84NQC2S2DsBfapznceDaDsmY7xRDN2SoYNlceoAMaFFcYZyZEjIkbuDOro5zxnZIHo4USSSXIQIN5v7Rsw5ZlmmH2Hs90dVFaSVPORcgzwv5EZOCE7pWcJVfNYaLPjkp4NWeh+7WwSeFvs7M07eOCFV/GFN2m86GQP9z76GNRv/B5wYRP+eoaFDS56TWBIeTHYQ9bHcUwurHBbl4uBCw19cRvl1YkNFa13NvxMI9WuvN3THFmOmlp7uPEnL2LpztPAEHA5BWqPTWivhLR1Zmt5GFoOnmq3wsHCwAaoCQxtHqBA87c2WyQlVHiVjuyU30EZUkJJfF7uOJMJMOjj1Q9cxa+sn8XSUOOdf+5ZvEM9YYQEe07EGtlTDPWkfQobHhx2h0V8jarz5ZQdlkrM6W0tYKgLDPU23vTp38FXft4K/uGJL8aHPnQOeaGhy+o6dAnnaIz8B1Sd23R7O81sIQLyngbnVf3Z1a7bDXvpiAPo1BtXLml+y3Hc4rD/AtRv7ywn6+TsRdTdJo6ZK/FjSnRLnWvbOSxKzY7rrjaUYqg+o8yVH1UjBnQwHUpjFG4GqUYonI3BfZ+HWsb5KcRJP8JQi9XTwK+9uwB21oByDDtFgpkSy08bgeovR8ucfB12MG0zDNbAeAc83kC+fRX8Z59C8dwVU7lOSpNEcVJlJK4bMFUl4T0Ywo4r0K7O20qNGdWUhNG8vd5NvmAbY2hmh6h7L9hRIRsewbqKOUyOVlLzZa7aM3Ndym0N3pkAuQK5xiNTJlniPMRiQtwgptZDsL5/GRINZO2v/S9sLENC74u4QQxxoQRliXy8hn/9jsdxUhU4/cwV4NJV05h9zuyD4gY6Pp+Q1Ln5daOyphrFxjBb6Y0JDkSbkgn/5TNL+Def7vvYea2pVom22S4x7vkh4uiZj/WY8J0KPlJ9cZyKLS5CQ4KIv7pnNbAHWlYNDjK9AzFt+9p0RlFhnYrut6Oq690mKMTGRl1csHWFdUXPMkbe0/6n8J4RIRIYarvwZxQLBg2BIWEHxuun9itcu/Szer1UTThAtWrIhzywefdLBpZy642gTWc+pMtbZnMSm/RCbJwD1yeMjcJ4ENpq15NnQD9jnO4DJ/tAPzPN43IOLGWV8GAiygjbJfDEJvDEJmGsm1XoLFLtZdyvqp3zguy9qWVC87pO89kwzbQ2CXmtR1lyZC++f1H9bpZV4aXEADJrYw6B//Luh3DLUxeAi1eAhzaAsoS+so2yKKD7mVF6nE3jhAZniyY71FNOyJ20HXRhtiF7bmbMiUZ5eQfF5UllA3lzb1ovzSX+ZHjVOKoz/ar2HEYffhSD19/rk/N6Um12PNCROi/vxdCyXkwsFLhZJGqzdQXlmSYYxOJCyn4J9+XKp8hO+91iSLjGOfUy+f0zsDPCD5z4LH7gi/4UuLIBPAubAJRQm2qztt0Mu4oI0Lp+90LRJbWd/eqfV2cHa21yXvE6/tdXjvCOp9bxxKeOISMb2spNuyAJwYSJJ2Z8bxW2uuzW9R0CoaL5mCWe4477PgymHZftcxWLMnbLYB9c3ybYd7pPVG2zm/PuLjK07DxdoPkLsmgYABQjGzLGvQxZwVVjANQ/u876rJ1m6RPjEpWrG/NUF8epxkZc50xZz88SQSYpzH/7zgK0cwUo/OTGwcpdrAmu/rptOVimMlA5Bl+6BH3hKniigUKbv6WJLXSHdWEIgK1nbZhEbXrjlmvpjxs+RKWGUyjizhNKBo819NoIGLMZJC+rcAjnVeFzMdiQifD0qheIzShpXu/uefehqM0oPncZvQduMV/mmU3CnZ9rxNoyHwPTwyNqn9G+HC2/xdZh6LYYl9XhPEnKEnjmAu595kJVTm0tJULVcIUNu9tf28hFSM2iT7gkznimWXPlMaEUoBmFyvChK7fg/3zy/kjZrb7MU3c1bQTzdKbsJXPZ3XEiz4fAkKxsuNSUlnX3t/qMENU9NTPARAYCAE6FaKFbB4gTBlNjm0AsCOdm9rNHRPWW20fo8eCPFRN4LChlBAaVSH2Svn/Ns4tt+fp+2AsVbftJjQa4fXZ5vIWjSW7rgnCiHWYze03YZBl727zvLrS+z4SJYvS0y+vQ9GmxA8s1fBh9Ftrxpg06u2QSTl4Zm9AM93tORli4Y4VxdqiRK/biBJEd6HHltc3KKhOGGUGzwhObhKLZmrZislE13yONqm/QfW+Hi8vh4gYfyHduArsg7qvFJ2brepcQOOyr3H7bFs498Rzoyjp4YwvYGRs7ZX1kQjp1cCMdgbdCbdAlyLEwM+Gjdv+patxholFeHRmBQQeDLKG5l+iAkDLJbCuBAYhHwdygUdj5440JsLEDrEaJsWObwn+OPlB1HaBUs7PeJsC4RhaoCwzhv9jGcuv468ftNlVsj8X2VlgeF+IRLvN2j6qSs9RHIur/bJnpuUvmezgIVEbnEwonKdgOtLQN3kTbskZ6X26gy9mA9roTEa6u3gpdjkyuqbassFPgxO2tZnLixm9T9lT7lg5dDNah2SP7KboORB0UsVgQl023eLTW6xR3vRuVnR9M2w1z52RwEOZ+jhaGG3ybdsoE4y6Y9zV01jPx31xvLJpxv5h6UhysViO0PoiSihwA09C4z3GCHp3oVKTWc2UJDXUCsLMOlLHAgHoncZqSHEIKPmQCMJULAUw5sDUCX96CXh+DJyUw0eCSUVovhlBgCKerdDHvKSuEmauH3dbDjXXDys2WmycM3iqhtycmS7L1XghDI5ohEvXZLrxRljFUzoEHQ2ChIbwF1TUrntpA754RcHzZLMgTGV5SOAuAkW78Uup6F3Eh3MYbEYltwuXKxbGELwaqhs4pQ+H2Jao5phvljBo/tzw8bluDmBIQ2j63hQG4BtWVT5lenyaFT/Rvxo+vvWJfGojqFUub2u6YtcuSmeQ8xFUIlw+dcoZWdG7mb+r41coM2EzK8Mlea7shRO6JNo549mk2Dq8DA7BWLGt4hzNMhAKDfzxnCQyRuOD+ztvgtRsSqYuZEhjsTx2end0YLcLRwL0u4WvDzE1hIKyKyFVpjJxNPpIJm0TRDNefrJ7xMqrmwv6RC31YyoC+MsJGP9PoKeeVQFDEyBXQI8bAel6Y/A7G28KJG94Lg4yoScw43gduWWJcGgHrEwq8o/ZeIbZUfQulbaStbd2QwGQyGdprTYubktIJPol6k4EwtKHNlnzrK9ZA65vAZAJaHgCZAo8KcLnupiepVg7tO4LxigTsCD7SwoM/KNovSMHgcYHy8hjFZumn566uSV1o8B/ITPWpcmcHBRfPXQNurx/BjNHvPY7B6+6uziV8wGvHQ7A8sLecwttoC6ZUrLFd4YSFuEOezHMV2UjhfuLOf7x+CJEZbMqjDoCz96Z5LsRlK639HQ/KuLL5v5wckOHgc207v15lS3AjoTa7PVSU1fp+X7lxF3xu6QT+149cxcXnTgIEP3XiPPaEy8mQsCQAzNemur6GMUXDSokb68zc16G15fE16HY1yd+g+vqp86gPfIWvabNN8Npf6PAczZ7TRmeRYZ7K/Shg3neN3lAbNzY9PQGaY5bp2nV+0OSxpo3ez6nYGJdoc1OUZvgEBK5w7oaFlWNc2ZsVg49Rxep+dh2enR2UT11C+dwGeFz6BlNrWDc8qj2fpv86PXnLVOJ3xVhQtrFn6O0CvFUE4gLqYRG2TLoEYA0w1q7BtgaFnQ4m63E0n3n95Qs/E5nnCZpRPHEV+UuWQZlCZ18rdy6uAYkbjbBhiRvFLuJC3Hi1ddT9CSXKFz4/GvCjGN47AfBiVipBUjhaMKuB7io4tCWy9IsCIyGYVYGJcP7mW/HvN78AVy8PkKpEF8EsoSHGzCZjpuZ0U9WaKWcBn+eB2UdWuFEzZS+/n5/d/q295wDKlnCGuH+ugbmndqpNT6mqqXGZgsSzsI9JbRpeqm8fUE1hWU+wGCY9csdcZFtU31cctBIs3wdxSjhaJJthCkbrg6rDNENcS37IzGAQesym7WE7kwG4Shjp2iIK7CpyzRujR8BKrjHMGcNMY6AYmarqAcC8CzbHoGsWodiJDeafIhfiwSiZbDkZNy8zLoxM+EQZ9VHm5SBfh3D8YZ5tHGZ0XpvEaYBvBly4XDgWw4k2yXmbzDrr15/ZAl00MfUAzCwWlzfB24UZmEld74yCTnnXs3PnENT7GmDW4J0S+uoYxbZuCAw+RCJhCrjBFgocM8MxKwofwrYijTX0U1eR3X6qKmR4rPja1r5be4RQeQTMqnjd5rH95MMkIptqSue8YYeF6wNNu8aRZdXU5f5UEqJCzZshsFdSIkgcxhAnBI/KzrELXc1eqpeXQ0+a2vlF5+W+uqTqsPYzAcgUytvO4p8Xr8QffvwUiomy45PWvu5YrzChNpiwl1HzEBfCU+tIz5EybW9t/d7LX8+H2s2mdAMkDeEoXMF9SrxWqTrQe5PEml9Hoaa7yIBudd9RGMQxqr1RZJeWCmS5GeXuct9DD4Gp60xdoXrB9qsRbmgdWteX1Px2w0Z12kVwFZzdlw6zWOXgy+vQnzsP3pp4a0Zbx4myjA+fcDluu7YMMAJvhlTJ2E4fWmqjUI7c7BGoPBdCLwb/uRIXnGXIrhgEZD1Gb6B9RldmIClHhS8kVZe3eHoT+YuDEf+pjWFUsYeNBlB3yUtmNg4qbo5/D5al/ibFhRaBIVneqHzh8Wrfo0bTHz+xrO261MrQfh6cOn/71zWiRAx97/34D5v34sMPHvOnbnY3uyKel3mEBiagzK3HQeCVxMpMWRW+P2Gja+wAu34JZK6XURqxIJUTpp4zBghfVlWi4fKb1J3c8a2rpPNMqE/FSw3viaQ3BZoGaygmmHO3f20H361b92TYe2MekwqTmC0wLL4cwsGjZhm37hmwVU1mBUEH27YsZ0ArhmZCyeavq+pD+56I0VPGwZ3IeDL0lMZqz0wnp8Ctod1VmV3TY8VG29aVzFCRRx6D0FeMW5YYF3eAq5Pw9/nrPVV7f+fefC6czeNL3OGVC+0kl5k9yzW0Vt7eMFp60BGpXe+oLptp8DNuurpltt8ZGaFhcwfls+smpLNMVLKK6h4MwGwvhupw1UVhBmsG75Qo1iYoR7D5pyhomsmbeeH5woaKZrmZdrVWL7tc0F2ruJIx+cxl0OoQ6viwud20QQX3sKuowz6L2sBMi8AQD9K4dd332M6YJS74Mqsqd0TnsgbHrx0P1UBOrXzcOnBTExdiYSH6XrN5Sp1cj50aGtqaLrOrmz4nV6BBjt88/RL84YdPorRezG7mLObmYEabTeH6XCqRONGJsF6MnUoi50JH5+LaNjPb+uZx976P6eUBwns3fccm7Dtcxz4nwQg5oT6tMWDD/myoPyEK+93D+ew6XOIoUmtQYLyWVpZKZAONcpTtX4+/URDrSsdmdHDRU9P5w1iDBsHIfL0j1qjdq3VSLYZfHlWyROCdHeiHn0J5ccsPf5jOvOno1Hbn+q+K25XDrnFbrnbRDCYGCoDHJfRI18Ij/KwWCY8GcKXi+9HhDMj7Gr2BRpZXI6fOxSpOiFS9jNHlHGvo8xvInr8cdNwTNaXbSZioJ1auU2p2SlwIG754Wev36LpOu/ShFZfaT6i0x/tPiQtAUyxpK1tKEEksY908BtvrRghapUEf/2bzFfiFP7RTQSWM4vD2mM8cLJ//za2O0UwIaX6vQqOYjHu1nzlGmd6+UvVElE3sPrzaTT4XCcXxvuGxEw2VVlacsAKFSxxprqU7GsxMGJlCkSs/tOuT0IYHifMk1GaAqBNnLPYig6rEBrOc69cjeM2SuiXXf9utkeKWtW87LUGkcK3RcXDGvz6Bc7L/ZF55s4JmE2qhtfEmAABFGj1lZrLI3T9ia7szsmCmCFfdToNR5SoiWJFMuyVsQyzMfnJiQAHnljQujTLsrAMj70zntpmfIER73+n2Ltt1g88mYaz2/VdtQybqfca6YJS0k5pDPH5bpRh89Yo5wGgbPCnAF9agr4z8TEs2E6f5V8s34AqK+S8kA1xo8FaBYqNEMYbPDWaaz6qSrpl/1q5RVmCgDA2brdYvb3kWfX1r/+NxieKRy+i/5Kaq810fJq3vwFeyTmSg+qOYuumpmRTiGSTachekvsdl853+FtsFMOeWteTjSpU53kc4mMRAMqHjlHBZDoWJ2jEQ3O+EzQh4wYvD7d31cAmz3Sm488iUmVFtkAH3vgD/6ZMnsTWy4ZZ+6kTz7Pkj2X22VmNEkX3AUbsdhTIlacujNB+7ERgOwsPRHcPcyvZ6uipLNRBjhBbzWQV2lcqrQRwXRl6WCmCgKAhlUeV22cv5XbMiQ9UhCJahXgdmBCwvlRgslxhtZmamhYMuJDDbStjt7gEvYjRIHTP0e0uuH1dUdhkDfGUN5eeeM8kebaXkvBjc/K6+XK5zEBjfjZffVWwpoUHDZ0oOy8OFTfIY5l9wM0WUhLIkcJTwERzMAe3Ko4zA0B9qZD1dlZUBPz1mwuCqVSa2o8iaUXzuCrLnn5vd60l1vmNxwS2blvCxLWygi7gQnkybK2J46qmyxmWbdexUIx2XLZUtL9on1/ZZb6jZXT/nnaEIlCusfd7d+I+/QlMf/ZTwEBSidbtpmbnr+6lEq3qVQGBiH2oAUDV63zodUr1TWytDZvZVTqnlyJUjvFcKpiMSuWHatsoclazHQk8ZUSIWEvx5p8vdFBMYzjMWQG1u5po3Q+ARFV/TqQNHtdfXZlzvUPnX66luAoOIC9cP2by5PmDeHQ85+57dV4BNArIeaQwzEwLRU2zzLLDvnMePkdsD175H64RWILu+mflA7Oods6W2+8rBQAbcfazE5iTDU9uEyYzTnjYta9DU74lZ4w6xiJGyAadBZMMBvCeUG61jq//XLci4o81Mvk5K1f3MjOFSafJ/jcZm4eYOysvb4J2i3hkMTyZWaALRYWayR3tclIxyY4Jys0Q5SYRHBM2uVcHgOiKkrAdD7maTQO2GVnV702E2iT3H8vwWiseuIr/rZHDw1pOw3gCBTRJXri5HVGpbNyofCwwpr8ugjHXBIWGbxB4QIc6GmuVqlCpvbBvFoRBt67hTaQghoZ0ENAUY+3masAD443P4W65gVVGoXgb0MlC/h48vn8Wl7aHXUrKMq8tVE4jsoVqm4DXPYL193s10iU2Lff856La/7tWQuJ6Kq5R2NsTOhDWYes8PXtUeWfYDX7l9VnJNKAsNcOXhkJyFpwPXrMgANEWFGAVg0C+xtFxgI+uBy/aL1PIOmOPw3h7eaAriZjk5ffyuxyUAyPrAxDZsDcFgSg+rWin6Wm3D4zH0w09AbwZhEm6u5TKestJ8rDpLCBI5JjqzwedkyIS1JLhk8MgKDCV8kknWQFk4UYGCWSTqjat/WoiR97kSGILOi3ltOSjn9IqOyF6DjYmJF5lnhgmg3lDEGZFbVfZg+67eAWGDE8Y5epcTJO5N2/GDxmjWsVMjDdPWSeyPG+cSXrNwHa41+qQUsDTAj3z8PjtVpTWqgji3WXFP026lORw3lqWEqdS6WQbUbNqgRz9tvuW6wJAoq3JzwKd34HXDxKOtbcfEffb7DV/tLB5oS00L1VzujNnad2p+9sZFZGOmjI5pZejKLAEhPq67buK9cH3iZuid57aqwDMJMNWSDkRF42WssZRrLGVmJog4+XySuE1s6eeYgXFjLJg+IPn/TW6uqj1zXg05GMd7wAtPakxY4cIO+Wky24jLupdBw+Q4wh7214YNy66+B6FWRABl7I1nFzIZ4sMsUdXhJoeD66wF9SSAlWNjUK6A0QSsNfT6DvSVHTNlZVjXu3Y3i8IsWy5qrR107Yb7awWGYkOjLAjOc7PVewEEBKOceS/q5DXsAXbRGMYDpKvQMNEoL2whu20V1MubdkmIsg2NCztoU5BTgxGx7VQTGBJ2jEtsmLJhfKe9brvE159c4xR7aczCJ8cOzqfhuRqda1AWrp2PXeiFg+g8wkulq/W9gNCSl8GHSygAvQzoKdAwNx4MmTL5xwZ9fOBxjbUdd/pGeHPtZXybjM2Q7oc07AfFUfvaLubTLsSINnY7YHBYQgNQ95R17yYF9ZiPwlJGBKoP1jS9MH2S7YzRy+2rUBKKQqEobAL9OXvD3aewjL4zunu8A/tzI2Yq2WT6fYNBCZUBxYzOPogQizUufjncbu7nekZvxR/TrhdOP9cVXjkBvrrZjMeeJTCkBIlwKkIoYGcL5WfOm5kkCl3lRfC7rs6P3OiMQq0Pu+sHoGRwqcFjjXLixAXYWMNAYAiSPLJNQOleQNNRMg1pb8DoL5WVq1Dwzvg+H1nDLnUbUi/CpIR+9irUnTclrKbAkkp5BdSs05aGcZpo0KZYx7/NS+P4YQM957FnhVUktmkVF4BqlpbwGARj3LmRhUEf/+WWl+GPPrJqRqBSoQd7SC5EQMtUtdM6w3YN+1xOU+ubHXdO/haOqvn6kJqPaa105P+r0KaWc+EPrmJnuMY3Pn46TKAx73QtiWNTTHCGvWoRIMJ1p5GqXlgjmJ4u+o2r7ZLGSyAwhK+viAvXN67v5p7xLh3pkqv4VYaxD1TQke0pxlKuMcg08hnPTVhjk2+QmnWXXeIHgMNjg5zE6HcAbRM/VvNJEHLFODPQeNlp4IlNhfM7hCtjbkyA0MZ+vAMlT8+PPQ9NIbbysHQJn9sMQm+8K8B1slNUM1WZHa2sToA8A++MoK+OUD63CR6V1Y0NO/KZsfhr+054MTSSuPlOKBuBYaswAsPEbMhc2WYNcSFAZWxDRdkPmCAVw24fRLeOFxow+xnQV0coHl9D7/mngoVxDzTMR0HdPQNiQSAUGMLZr5ytEXk/zhz48J4D0XPCqMQQ14CEjUOqY+TsqZonBacFhsh24obdFJ6DXVzL3RAcd5q4kBIWHFZgUKt9UD+vxAXjsgAM+njy6SWMJ8pcgsAlH0gLdl29E+L1Uvuq1o3f8U6H2DOLSEy5t+OH3ypPrJQ2l56ivSncNK6dbT5UxugpDSJl+tA67cnVRneRITZ6MZ+eEb6DB03eY6geg0cqWWYGqkHNcJguNLzdz5obQkRjf0Stg6QqajBSJXJJ2LpcLpdVfq24iNNA1arE4oHfectyoC5I2E42FxPoj3/azORQaKAwoQpcwnsNNMoUqGd7gUs2gsaEfYyhdt4TjTwMdfGhmngXABlXwCwzU5qqrG401DwZ3A9ssoS3jR7VzrVkTD59EYM7b0qfSMNICBoV584XqvBAumPeVVyIf592AilS3gtdxYXwvFLrpIQV9xNQP06877KlDASjsivTAFKeAfffj//04TswKSlxuhw0TrurlJjRVNFrFcnMPbSKD22kPAZincZ1jnUwAjdtH/64qr5+ypsiLTYA8bnUlgfJhdy810qZGGDjvje7sZ5mZHSjzdti2vrNjp0IC9c/eTjKHf3mnsJwucmHxrV1NAgZmRkhlnNtcy5Yj4cp7zdzYiC7MkVq6zkJMHwmdSB2KAIU2zwQPvzP7ND0gUzuGsoYZwcaJ3oaYyZcHSuc31Z4ZhtYGwNFSz98VtMeeyrMMyA1bZ/g3aUsCKkGoFumDUfVTM06Tlg3uemy6fQ58Mc/DX1528wosVMCmkEJ95Xa7sOLyrZz2wYzuNDQmwUmmww9gTf8016cAcSBwFCVvf7ZnrwrkzaNjZvdvLUdCU/PtuV6fQTeGoOGtruhdV3d1RrIcnNT8my64ZgazAg77rWBmsDWaEukHe6ztn6LwAAEAkN7MWvlc+cT23exuDDt+LPEhdgrITS/4jwM9jgcCxKOTIF6CnSsD7KhEf5+OXHsefehuHgCLg9D5cFASdvBX7qWtnyaF8G0/TmcvdNl3VmE5zJPGaZR2XZ7L19IPBjTFtIw3RO0y/7Z5rQBNNH0uimis8gQNxKpBvegmXWe7vdebjqWrFq714Ab/gsednaGt3sRmZNTr6V21daicgY/eQMxoIMSeVvFKseaqOmZEO6LTBw1EzAuR0A5QZUKuCpzu9jgv1TLgoqXwcCkwOTTF03SokKDfS4GJzAEL6LtNIQuOWGnvevbZWIMAZ7Y5I4FoSyqY7Im68FgGtOyJGjrpsJRR8/Fe2UZozfUaZdIjio515CiaRS6DpNDl4DKAd4pwUUJylE/z7bONdAUGMIGxt8H1BvDeF/h/oHmevEJzpoSKj4G+0LU3Qv9sbl5juF24d9U+RE15KnzsodPugNmZESFTJkQiTwDLy/hb330Njyx1nc7ae1kdp4aKKL1UQ47HAlPn7Zt3S2ao+5OPlL179N2Fr4jzXWrR4QbZU4JDbGyDqCW8FG5qWKVMW6zvFoeEzeU00YNplUpodEbJo403SuuNaCuvO5ZmfaKCNcvvUw3RtKNTU81/dO00EEkoP2PGcgUY5gZgcGYAsH70vJOMtqeN7Ow9pokhAcAsGm6/BHC/iFAvh9CZPanySQ+JOsA1mNgOCxxdqhx9zHCpZHCE5vAlTFqoRQu0XxmTSadqD+7jC+kQiaA2d4MNc1kCm11qVKMPNc+zliputhQy2fQ2GmzPiJVrWiqTA29PobempiBmcw+GAQ0LhWhrRJtPREGACswFIHA4OyxyouBapu7WGwvMPjDuac5aIcAa3tS9cA4W1gxKOirTys6AOj1McpL28huXqmEFtd4udCIPGuGmyYbOLfTsJMefA/DEVId+JS4gOY6rfmf3FSjYQMxy7ZNDR7F5Q+PWysbauuwe/jjsFFvO7pTCh5gLyaY7Ti1nSPPoFZ7oKUcyPNK9AlVVwLe98wEj+9oqCyDmfnN1ofWvg6vRpznKKSLuDDt8lbPdvUMpwYIOkNVHe3CCxbHHMbdLon7N6F4GJZjnmvjheuM669OxwG1azonw6xbRmTylQx6GlmuQdnsC5MauWIX2Nf1zjSSlNWO4AMF42zO9aLZaew01bwfQpGDg/J9+Nke3r4ydgUNejhh6xlfsajCra1jK70nngGPtcmJoBnscjE0RkpDQz3xU8dL59RVLkOBgbznhA6EBufVUHlUUF1l99MyAXm/xbhLVGBEqAkNDcK2xa4H1uALV0A3nTQLy9J4YdTi7xi+gXANhzc8yOtc1bruc9Sipzrl8e/xb9XwTfU9dU9cQ+0OkIxbjMuSKisS29X/cri/8PfauYfrRQ1rRsaDoZ+bmE9FwHCAtfsfwPqzJxqnH5Pu1Keek24Pb+MY7tnoSNoonlnLNcowy+ZhZ1jFe0oICU37t91rwS8LwiOAymNBZYw8Z+Q9PVU4yLLp7nhpkaO5Px9rHYRmmOvDtdOPy3vYrpDC4XHLcjWCaCdRQsmEsSYzKx4TCvt4ZID3anRPK1ubYzk3yR07P0ktK/rghsTrEEYCMZPP/eRW1QhrB/MLEZtCktm+hGnjNMiLFARgOWcMMo2blgilzRRfcySzuR0mmnBlxHh6C9gsqmtjPe93zRxjElMJoxQyO+KaZbZ0hfm+kplRH2XrHW9nMMAwiXRHbqaqiLBZZQbyngZvXEH51Kaxzyal6ayHFaktTJekjlUH0Xx2VbfeLjFZZ5ODAah5L8TigjkuNwWGVAeP7eA7UL+BvqfBIG1EB39a0+6VZqBgk/hyXIL7VlTQzl6kekfWb9fScCZzGHDQmY62m5bgMWHbcGi/RO1EPSklqm1nDtig6bkQiQ7sPse2UGBrcRzmEDwXNTupdm718/Tr1FyjYM4nz6CO90FLPTtwE3iVhGJYnuPhC0NsjlTVttpR9N0kbJy1zfTXxN+Ixvrd6w/3LM577P1m2nXZW1903vOqBmrM9zAvzSyuaZFBUb0/0sC+/4NBiZ6bRWCeHq+FCEBm3/VZnYapAkO1PyNcJMIqgqQnymkFtnGkzLhAwp4BKYYuFYbLE5weFmZY3XVYk0QXK9lRdJ8ZKAqMP/JZcFEaT4bSumE3Cm0UP+/FEAm9teNN7f2Ydbg0IRnlOEjm6GeNQCP/ghMdaoegalomk+CxcoE207UwFGysvm1LGl56wQhFeHn83+D+8FaB4uHL6Gm2Wf00UBjvBr++zS9RNTDmXM1DmoMGPajlHtDPjAHSmtgxcf9Sf2cR34tUAwzUhYN5xIVk2UO1Pj52sF2buGB/Q0agfmZEhmHfqO5KYXJsFf/kd5fx0EXypxgXoR3XYEbF4vnrjK63YFZ55h1R7yIyzN5f8zr4hjhZntT0UZXx4bbLMycwtO3HbsnV9rbE0fdm2eL9MQfiggIQNJDTzr97Hq+4fPEy4Vqkp4YAzDufQ4PBYNZYMpUTdkpGUQIjTdBM6EXPHAA/e4Tri4R9leaTbJe3PG9dXn1TPYV5GIwgoKy9k1lbwy2HDZVwHgklWzGdyIQiwLaNBGRgaGdY2oKXbLwmNAh9xVjKgGM9wiMbwOVRJTSkyjnNi6u2rOO5zwORtQcyY8gREe48uY2fePFjUDs7lbtuWJksreCRwRD/9pMrePLZJWN7KIWrRQ87E2Noq8zsM8s17r15BPzeg+BxYbbXXLcZw85aSMp1IyUwaIbeKTG5apI8VsKCM9so2fYob5+xL8esOs7fg1gkyQBoo1v46TijYtfOt2AUF7aBTEGdGkD1M+PCr6yNk2WB4MJTBAYOvD7RLho0tovsqJqdEYRrxp38xO4oFkPacPsvW8QF+53DMtRCRat9cCpMNCxnzVYKyh5eI9h1QmECqASTnoI6NgAdWzLCTyimRKMKxenTmBTHTJueV9dvNwKD2WcVvlRbOANdugGBVEhsVxYz9eV+Edui1d+2a12/EPslkswTCt9ZZMjITI3qnt8w2ZDWs/veB41pRqyxmdlpUnyStLDJXzAaxgOhq9DA8KPhvX6J55/SOMbbIC6M6p5rLC2XWF4ZI8vYhAZohaxnXuqdUR+rpxgvXh0Dm4HK685vVi/FV7K+lfKVn378OfC2TVqk2Yd5MKOR9C4UGNz51WizGqLOM+tKYKhmjTAeDc6NsSEwNJR7K3pYgcG/EGFfwD2/bHplrk/fmNLZ/ld5R1R/zXUwvxeaoZ/cRHl1AmTKJKuc2JclI5tfwsRRupg6Lly+CXOuRMDw1j56d58A9XPjeWNn8iAi9E5kyJYaPeD26+lwwy2K2muHecSF2vph8N+U9cKGzl/Y8PdgnTi+0P11h7ICA/o9YNADej1zbktD/Gn/FjypVwFQLUGV2c3s9z3p1RKWu2Od4V6l3dUx7I99sEp6dd1ndeLD9dsaaS+WWi8Gpbq56YXH0nra8dt+c8vm6fz7B7KbwGBfJQ4MvOY0m8K1hkvDR0R+1oYwJ9NyrqGzEjRhjCOjh2H7YI3kp/VawH3eRX7ndJlrYoZp3CrxwHrrE9sZfm2nFAxFgdAAVJ1J2z4qW+VW4R6VjKG9oWuWLeXA2SGwMQGcrh6TMgH26vHQOEbLNXXVsauLtCYMhyX+8b2PQm1uBw1F8BIzA6PLeL5m/PBxBo6bxdsrq/jFzZvw8edWwL0exlRgAsZgqPHVt+6APrwFnmhQRnV3Emc0KzL3oU1wcMeOmlaUDD0qUayXZg57F9bBxo5pO3dX71LCYaDtYnlvBnAQ7tHcsDKn7fE1e7tIazM4VJYAXyxBT15F73gOdaxnZi0AQP0MarUAVgagfoasD2THe6Bhr+rsxokdGahiQlC3EVIXIVwerh8YB8nkimFHHKhCPWYRbp8K63CXLBYXwmMCzYGWWDyw61THRXMdTngvhNciA2iQQy33QKtDn4zUE362NuSDxRBPFrkJM5pySdzodxvNsEj4Plo95j99EJUZoaG9czGLoy0wAPXXbpaHxm4HxObC77p749VZZLhrtcRSXn8PlW3IdgrCTgmsTag+Z/QBQ0HD2FgeJCOs6pxZF2oX5+Iq3U7GNPv1llZKfOHt5/E9L97A3aNngPFOVWEAdYsko6pj1e8DS8eAywootsGNeYbrqSXZu+Une/y1nvPkdz5Vy8VQU8zD81CYLjDUDhE1VqFSb0VqHyLhkzrCiw3GnRFJDwbANqLKCEsqYzswke44sLv89pRdOovGqKj/z373bV7lWcEM8CbAz03MMkY1y4WujADzl4wXC2e1kQgwsHGRgU9cAQDkA41yojDeViDFuPl1Kzj2eVFcY/i3dm7VMoqTFKWGjdx+2hrk1N+O6zWSxNTWD47d1qi69QhArky24zwDDXpGaLBTKj23dBz/5ZEzeORizx8qdGWdlaymrSPrlrsOb5eXu5rqq9v6sZjRrhRzVcc0xkXrb/pu6O69YNdvmb0hZreN+XTFPBYY2Ouqcf1e7af9uswzG5lf14fSicBwPUD2IWV2U0e6h8m8xwQCkcIgG/tcDQ6Gycfg7PRZEtdMT8y4bIjaJY7eflsdOKFBwYgHihgZkfWeNKKJtus5oUHZ/rCr3kycNfloPic2hFNzKnZTZBqWMiB3B90jromY952aNtjl23YrNHz5bZfQm4zMj6khw9QUzMxYWruKb87WgXtPYHzyNJ4D4cliBUx9vOKxTwGXtm2HOzgZdyFTJxTXcZHAAGYjMGwXNkSiu8DgZveZR+RtECbJ4EpE4BImAXcJb+eUhYLLD+EGg3xIa0nQFwisS2g7QxkwgdYjU3VnjOFqiZX7lzB83gqGz18xnp3uGvhrExmhsVGaGnhptXGmCAyhAjZras342GUZ7SsSNICmuOCqGUyxg3TbesH+4vCIlLgAGO+F5R5otW/CIxojLO7cg3Pu5fjE5SU8vpa32tV+c98mph/Ods+HegLDpmNL9ZvKQrtsBt5mSu7qSDJPHbjbAbGumH50+Ax1239nkeGB0xMUXngj/0yzVTDHmnBhR+GxDWUyGh8RnJ3vKlvW8ShXmmanhGp/2kgb0ukXIFT6XnH6Ir77pk/j7vMbwY/B5mGlUwAYF+Z3tQOsrdsnIDMKsasMezmQZUZ4CE8MsFPVNeMaCACzRvnQ08B2WRt59wkfo/vrdzXLSE/1t8JGKxYY7HSV3nOBK2HB/w0urXOLpqxqVKvGPT40WUPS7NfEqsKPRqXwYocVFVwuCO9toSnwrkBtKs26UVAZBn6ZrVTDCnW0qYLKljB+dgd8Tx+00m80rMmOPFC5f9ZOvfk81A7emOGh5Xv8XCb+TvVc8Ptz1yUw7vzv/vRNaMQgBw375mHr2xGRXg59/Dg+sn0rfv/SKhqqEMz7vtdOYCVQph7k9Pq1BnBmPhgOXsn2OgNo2kru2QWqgaf5qXeLpntSTDMw2J9DqL4vLtdBqnwtCWeT5QqW2Gu4l5lwjvJIiDAfdfHduRqaX9waYI2eylCo0oYGWKEbjIzMtJcKodgF+3sT1zTPEhtS/dO6B0O1zAkNRIByYRGKvZDuPNxLJis0GJtOAdbjwV4FYigm39fyg/L+GPYdh0lQrRassnWrZeeAAKU0MsWgXOMvLp1HPyVLpCrPumuVcde4chn97U3csXoMd5zogSfbmHzkU9A2VKISqgJiL4aaLlzvQPo2tNQmB8OGrgkMxnagRtNb1YEmZDQMFa2ONd3W8fuzz4yxUYKZxYpAPPD2G/lwWh18Z+/RUNlHOrKTXI6v8bbC+oUCwz+5hBOvGOHUl5yEGubVdUrliEp9j861to67rklPTfs5eiw6eTHUppHk+vE4ChMNBQi3OLa7GlNuVufTsKt0uL9AOInPDTDXcphDrQxAS7kRGID6w9BS4awPV3CJj82Vf2GaF2LbC+5znaW2im511XZPv0dJMWLhlcximdcucde63U7d/ewWbjul2LzHHQey5giXYN8iauZKZGAjXA8y4LblEiUDj210HN46ADSbsCiT2VwbFV/bVnIKzX7Y3gzk1pFA2xF+cbaGe/V6fNB6r8L91bb2d79Xdx/Ysdtnyng79PJKeECiEvEtkvnOSgFFifIPHzH5BKKEj65THO6Hol13xgkMJaokjoVpuEqbYsIlYgpFBTdFZVjRmEEnhspN3KWKM8MyXL6rlrJUjW6t310C5SSYJtM3qkHjyvUGsyYyoLputfjJwFDwgoNfrypA6JRSXh6Btwvjm+ovIWPqo9mlRgnV8tToTe174rdEA986t3O8L3e+XmBINMSKzDM87JnplDKbibqXmWd82MdD5Un8+mNnsb2d2cSBABA+I7t/f92+gFgt7iY0VPuoyhDu05XP6IRcn9ox8cy6yxW7G9bChlrtLWr9rTYr6NRTm+5mWKtWOtcLnKhzYzGz/l7Utp3pBpwOuWgIFUfY4BD2H5P+0HQOjRnFIK7MKZciUSFDT5XWa838qpSdcSF6/p0QAL+Pecs0/dmuhIXmMQn1sAjjfcqAtYG0rSPd9Jo6+GwaxEpocFKK2Su8N4P73AXmeeqExeH7WgRkmcabnwccyzq2dUDdMAjbtfEEWF8DRiPgyWehL23YPEtRARSlwyQanef68b3AsMUoJ6oWIloJDWE5XXEDgcHaZ7Oue+3ecHW9tB0A4hIoi2DAJ7R3QkEhWB5OMx6KC9rl2PImBNtymwKMtzNc+tgWqCxx6stOmwGGlJAwy/7x69RtlVb7JBQY3LXIqFtvz9nmYXiHO7f4WLVlHWyg2nrBMWsJHoPjhd4L0fOhjg9Ay33QIG92dNoGpOxg1adHy/j41rLvt6Ta6QY+X0zip8DDMG7TdWkWmNlfqh+Zm4mhu/TROiVa7UhqwGTWpAKdrlW19p7qyZp3Z+tvqe3M+m1l9dXWHNOKz5X4MSPniEdeAGIyDVZpH9D/P3t/92vbkuWJQb8Rc6219z7n3Hvz3vyozKqs6s5uqotyf7gB2eButWipbdkCWxYPCCNLvCAhS0hIIPsBCSEEfwAPiAfeEDKyhASyBQJLNKiRH8DudmN3N13drqyu6srOysrM+33u2WfvteaMwcOIETEiZsT8WGvtffY5Z41791lrzRkzvmZEjBG/GB/fv/FvDGQo1wdC0nbSsGneYxzf/g0SEfAnvrPHX/yNW+BQ3CiQ0BGVaCcZJ4NEwL34B5DwQAFs0HhVBNF84LAoutBjzgE//xR8ewAffIir29BioMKZEBYIELoY6FpbAAzKxPyg/hjqAEOpQqVM1aoFTp/ElpQkNe3uw95JffqiDlz5LhYmiXHGfBKwAKYUYckAE96niR2BHNO/zskp/P03hGGfvIrzguOvyebX/C+U30dMrS2YVQGPETqf347MNUPqi4m8cWKjeRVAM6Lkkbpz+Cf+Bf7XP/4efufzLjGtMM5OYSv6zjTDMdgwHQXBEumCFDOP/5g0DHUkVgYCyeskf506LY3gQo3xpTq30gAy7kRTY7o9auI1pSmQf+dgB8xZutpz4zznGCTPa081nrX10S5Zt15c6F0l1WZQWUc83Kkw74LQ7LBxHTwG9CH6QkfJ2eMozwJsOLcEUgINSmqS4Qhy8hS0FOLBbJDbhM0nQIKAuIlQMMHHa+GEvty/EKIWx5TZwpsibUu3YfxXrn6JZ9ybjdzMG1G+V3a098ChB27v0P/uL4C7XgdO2tRMnbTWOsoADMPtgP6Wo3anbtQ1Gez6bqLjiGZpDr6O9pCcRMX0ss1tD4ksoiah1lxV5Rpf0VqwmgrxHiVZL/rWqq/vkVcy4au/f4cX3/sU29/6dn2TuOTd1dJaWaMmd2i3dgtNJCzAMAdkRNPjou5WBip+V8EFOwY1TQtcAABHcB9dgT64Tv4uYltzuT5eswva9TU+p4/x6SsX5ACqyjE1mpcXVP5J1Y+mmDzW9LJyTOkfrkVz5rJrqD4UrUxh5uhY8Du6jGlqRf4yKco1YLLsel3X1msxyGDV+iio1KlPBr3XM2Pj4nbtjZBlcHGOOEjotBAb+anRn+q+wH+B/ziXdnXzb2kOtS1t1YBgWhHAg62eAIfTYBWeGEixpzz6f/QpeD+I/SYnLYYalaeZiwZxqGb0waAaDOZ3BjDwPMBgTSRIzSTGtUW1A5XZAugPhOHgYj0icGC0EaIviNiGtKDkoaTCPU7956NqICd+5GWB0vWaVEApX+Wdw3BAG1yw40c/lyDwBfKeGmO/c/0ZVBhg+XvIn20y1woIEQGG612Ko911sW233Q7/fv8D/M5Xz0ZNP4US0GOkaxgBGzrWuXhmBeiQl5gBY636l9dFSNS5m5ef5mKqY01bgBlRPXGJSUOr7kBuZ6kxm6PpkknbZoZBMJ6tRflsSxNifCJgw3Pa9WpuzDwlcPpCD0NpbnMBNgCq4SB7MYJDh46G6PegxBHr+S/fE62vOyIvs2XpddlLSuQIhnwitKsL30vWYttt1/QaJ1UTEQUlmnVcSMoSzgH86X6xc4x/4U/t8av8CtirILJik1qSZ+BwAG5fw3/5esTrpHCKmgyZFkMNjWkBDDF0eANgCGR9UuXaXcul8iibDTQyCY2aC6hfL81aS80FH2RK7/PqOKOxL/4FZcD6gfDZ377Hr/zaHej51XTFa1Epcids0r7M5MI0ugQYwvuaHX42HKX3kmWtDAUXyrqWZVtwYeqQxspgQ6U8SxsnAMOLK5nLJQMumWAZupIIP+Mr/G3+wCSLK02lwFizmZPzPE3tIMB7Ms5nK9RYbGq1m/MZSBNaF3Mk61WYn14dWKbhd05NCltqEys60jxVw5Gei9aZSwBIr4zDgRrBQxjyBsCBge/dAD9/fbY6LqYaAyeIM6JN57HZcjM6zpsiIsYzt8f1/l4vLAcXphqTPcNiM3LoRfWr2wNXO9msZZUBcLeH/+UrcfgYFjWLVNuZ27LJSpOqsinS9Z4RnTz6IUWQ8H0BMEyYSGj/uY0y1fpiVa2jR+bo0ffA0Hfir8eg9ozwWxeLmtYCkNKHexkwggSgoAYuhL4Aw5zMigaDutfowoaNB+SSV3VscO6LoewMDaNhmW8W5ql8WcjTxNtF2RPgwghYAHI0fpQWoI0D3WyBmzBWXZeYnnOA6/DNt36I/9v/50MR8juOa8C4yTRCymun9npdxy4zwkmUMB8BjdImdfz8cu2GklSTYd0z5keop465ZTwtjCs+Lsa11CGFcbOYljX5cN1Yu4iKk7dxm9apDNq0Una7PQqAENJagOL7XBntNBcw4u0lBaaSh/NMczPIOkwKM2ywcX04fJEcwjIfN921EkKSotR5qg1NO9LJZJRYtdTaOpqk8E/cK5ACDcEng+ZTkalsPqohwSB0wcFkrEej3WtI+/0U0kNmQPjoj+4+x/PNvp3YtneJLNY58KEHDsFBdrkmhw0rlQBDJV9mkdX8vcfw2gIMFOWGVL7dUQSNseDUsiV7AdNrWNSu7El8KAzHgQuZ48cCXPAFEOPCKWXXBZGUEDUxwMDdV8Cnf/0LfOevfZwcQS6lLESllWUmNu9AAhma0bjCP4zF4ILU4QzggpGl8vCWhQzlpR3uegP3rWvgaovM50wpH6rWQvY7/f1iv8Pfeyn9LyYM2lHTK9foEKG2NlDgm1q0uXfOTa9Dntec88qlpAeLERgJXCNpK6d+OlY+tNQ2HQVsH7uGWUOt3Vb20eem+r118FzSYpBBBe6OWEIjBWS7o8R+fWBkn1wP+Pnr00wmzqlytwnzpOt8XNDeNOlL/NEne/x3/qkvgX+M1q4FEdK318r7NcnFpk1Hs7Kh64foGBKmT/wvvoR/fQi+GBRgQHq2ILuxrxVpwQb97dW/Q2RUyB0+GjW7EmCwTVctlRQiDxNaDGPScKZDT9HvQlQJtFoTnBioBRd0MxfVAk0d0/MiFfjAiPV7el7UBhkKnFPYQ5OYfzg1SQnM+n4wp1MFIKCd0nxbhmraCzWnSuaT472sE6u/2darzKvCWDMGDYA6At1sgOsdSMcoQY85AAC37PA/+k8+xt3BCdNDfQoB48XW9n/eLYRSO8GOuVLmqON8yzaZNvxqrY4lQ57TlIhp3bI66AZbAIlldR6XGcyUHEcgzLZDmVnuBLK479rMfg3w0WKos3m6xvcLvaekG+UAKwSthijpEIOYAIh3BtloC+neg0iWOcfjNamlzVCK62TSK+lykwEYnH3k10l5MMOafqnHCYoVAmJUCpJNSn6mJ5oP6ZwhmVYodQ646RjfEEXFyLLdJS2Zbku1GWpKBA6QPoCwjZ0DtsOQbIozYaVS8IgIot1mnvMehx9/Af4mARdElKl2ZKeY2mij2h556+Dh9x7Da4kslQMMOtDKASV1sQBDS5Oz9PeErNnJNCKL6OUJbbOI9D0DF6L2QhhLHhgqLyicFWC7S7JOVvcQ0vD+C49Xf/OXePGXv5/Jq7N73FIuntJesM+EMuL8gHkmnlRAwAXrMLvIP8oYLXAhfI9NaIELCipEGcqU4829QhuDngWAYdON51BrUkVGnX7fXt/gdz/8NRy+oCKp2VeMqBhr1eeQgfpNfn/GbdtSOWGKOHud6QArHVLJWioyKZszvbqsVV1qDNWBmfFDsifKZalWn46e1zGvdefxs1k9F+6ll4MMKJkdB6BMZ7l8dgQ86zwI3UJRu17WuciCcpvtm1djiIhdoA8P3+AHP/npOGENVChHYragLXxWF/oBAjRsugg08P6A/g+/AvaDIKmcTkVL9bbsB80LAU2AYVA/DG0NhsyvgRapk8kBVETfWVIXbcIwOAwHiuVlJhp6uG81OZDqpoCDamWkvgoOLdl8RkRTCvYDMuzIEbC7Jmw2FGNZx9CgEMZLDrj7nS9x/f0NuhfbnFkCwmAU22t1QqaZEBphK1J8ZhoIZXmV8rmVn9ZPr2nRNi/LwK83wAfPkudjpaChwV2H/td+hJe/t0HXrZvXMeTXaAGtM4Aczc3vl8pAs2VmD4/zq5HVvgBqERIqTlDz0kf5xTSdZBxPJltHpVT5bmiz8REsKOthzRPsd4AjSCiMuMI0R/4c6lSuq0vSr6HkEImy33otgamnCzAXenNkT/qiByqzkVEXkBQWZALDsQOTrwqKLaBhVK6CAcW1Fo2Wek77rdoIlM2SmkqkenkCyLRKn/ZhLWQ2Ep4BKsrwmxQgiR88E42GL/fAvRdfD+oTYqrtLWosN4tJOQNBlDj/69/t8RepB+5ULQB1ecl+zz55rB5/fxD/Vww50fLIUSAXF8JxYxRg8AwePHjvMdx69HtnnCUmGaiSAQDR0CCjOabVHfetAA0Z0GXloahZmsth+TXkYIPy8qi5YP+CrFMMym4LdI6w3QGbnS/AgrTJjQdUA+Hlzze4+fJO1P61X+eWWwayxlqZJSpvWp5FGcgw6shifLD3ObigclPpY8rKaJm8hLxu5UFMKYdpGjPRuXwGAAhwz7ZwH10FgGE1w0vNBPCL/Rb/7u89DwdgnNLEptVfxFguN31ttHbzdOO8ujMAA6eSlbOoS7KArAc0mm/MDHIUzVDL9ltaoqmxSL4glaf0mbE81iar6Vse+owfXirrrNBkSBk6pPjJCZxMDLkjYOuAg09MYs0YP5WxZHlx8BvReQEZ3uBYLVFa54CPXhzMaYJhZnaBqn0CeVtqAMTc80TisyFIAfzFLXB7EP8MdhEOm+f0XLL9W0JxfcwYlDoCSuCCLxDxbKNfVDtqMXQsqvK0oD6M7HTG95DTAh6DC1ZLoQYuJFBCvgOBoUaAAcEuK7Q5AAtALpw5AjZbwu6KsLs2zEhP07Ixw7j9qsOHxvnjiCmFDhrZ3ZUvI/TH+JoySQvVAqNxV4AH3Bpnvkg7Sl+k7QDadnAfPpOjsdCebGdMBHaEf+vHN3CdOS1YQKq9oshyVi0vzKOkY5Bv9YxsSl723ASzqQEA2p659XVOI2DtAb7NTwG/+N2M15Q+/bYAQ3R2GU7hLB0DHCxhpMdQ2aYyakg5RC/0dlLNJwNAYBIFWLDoWIo2A4LmGcUNe0ktk4l62eNrrROumnaE1DTJZBEcsQAhdA8lmqe63A3I1wBPyexL912ek4aprZbuxzYOuNl4/OpzwndvCHcD4+s94esDcC8unt6oQ8idY1zdfglyr+RCi6fVPiOP4vQ7/PU//lRMTB1AFYBBtBrM5lVlK7Np5MEDBwEYDvcummzmjqTTS48bFt1UdGb9qYiI5YZBNkRyV3mOH4JMZqJoxfDhZRSJ6PgxXZO0bHxr6QY5ldxtgO2WcPXcwzkf5UgFMsoNawQZGOhvGV/+zW/wyV/ZgHcdaMrxddZgxkhWtjKIraRZxCkif5WJ6b08XwIMpSxUAw1GZZvyufxtQl/6Ih1CPvZ6mKx03YE+2IE2hlEdyZz6zQa/9+t/Av4/FT6vufgBEQhaSq0oUHYDbrdCy0M4lmPhVEZs80vjUuqZ5H0AsnfyyV+PlQdaMmVJ59CuiHmVstlCGSq9l9zUVoYxF2mW02rHj8wAMkTbcjG5v3WMHzxj/PQVBdOKdZU6p5yWQGRx/PgmqRT0v3Xj8c//+j3wx5xmVgtgKDeGlux9e81+xnT2GkPt83nwGH72Ev7VXjbHgVkoAHAKOJMAhtzzsPcpLCSKNDb6gqXIA4iz8ExL6qBNZsM4h2EBuGB9LRgNC+/zxUaZq9VcEOYZ7hdtcQ64viE8+4DRbb0IiDGWdL5Q6KfEnJaworHZa4Z1bXNvxs9Ic6E27iwSPxpfFaZqfo/iOxswCwDQEdx3XuS7uNLDs3MYnj3DFz+5Ei2Gzryj+Q6APXm2pPOzttG3i/US1Hmpmn+ZV4vZRIBrfGe6ALP5OJfjwjWMq+yHqMlgTCSIUrstGJHKsjmc1objgYA6k70AC+8mWbABACS0pY/fAS9gLgAXxCjCsDh/1QRYMppb4nNaw/KdZUd10wEtF0gOuzNtBFJnkIyNUxt8zpylWW0GTT8CzYPjvq0jXHWMZxvCywNwNwjYUDOlaBFhBUiDYmtAiFExiIDf+uCA397tgVuziylloqrM1U7Dgxd/V7o5nKMCYIh+jXqGf+1Fq9LIJPJM/l0axxFgcF1aS8fl5Y/lVTGyz4DskMfKfiPtBS78RJhoES2AodvIYcr1M8ZmO8Btcj88CpZbjVEL3CrP6W+Bwy/usP3BDdiCAUqKgmV8x25eUD8UsUnLfrTCAgftBX1vxtk6Z1oKKvDoX61sc71MZ+Wx8JxNW9VeYKk7XXUSpnLjIGApTtqEfdM7/O/+/nMTGlze17k2xvHQwfDS6erWy7UiYz2SxFSmpZxit2QhpDHlebtgwkzEBmjI696SA88dgGBK3lz+niiadGdXT5RxVoWw1AIVGFQmlawVZSHYOuBXbnocfIev9oS7gVaJhim39aSMJYBs8W935cMcfnMSYckIPtjs8V/qP5UfLRBhCmBoAQnNa/V82TP41R78+iCOZAZhhGnTPW6H/ZyiuCE3dn7K3OI1TpMu4iw+b3o2yEljQC/c7KiaE8MwT2T1iBEjMAMucDqljoBCyBds/C0EpgsegwtEguZfP2PcfMDY7HysH7PILOSKjbCOHU/guwHwEw6QakzSimAN3wtV5lf7PQUuWGHLAAhN5N6Or50T+0HXCC/gEgz+Bz/4ETb/yMd3xlYQM1RfzHmcxlyaU807VXWPwz/MtCovz1RWfZaOdeZo6VzOkSSzNGdd1H5ITLsUNnKy6U6h9X3SOoEBwhp5ARveCRIwIS1i1gGk/a3pHDkAGxAIngl+dM6Pym+hNUDDKDcWzYl0SFZusaeeVhOIJGnp3oxIvkvbkAEMMRwmDIZsck7ylvhrABhbItwEKXPjBAC5H4D9QqXSNVNqqj8JwMevX+OT/pURMri6sasCDErJgzMwePCXt+CvjJfzUouhM4sVYdxxnsG9+GHo9+qXqqLBUGmUi4csS2SgnIVL0wpQYWjLRaX2QglIDIPV4ExldBvCZgNsdsD2ymOz80F2S06CvQ/nluZEGMgPlJSGO8ar37/HR5/sQDtZlLMxqPzeyhXKA0tZto7am9upLpqeGdPaCxPgguZ5NLgApMOnUVsg423n4F7sZNzVTlKmSB1dEukyASbCp8++Bb4t5YC876b2VTPdDGDdwcXcprd2X82U1+aZsKWwbpp0cj6r7wur1EGPdyawnpbt04SvuYbWRc1MdCmtiC6R7Rmg9npqFiXAKkMd1+06xg+fD/hg5/BHrxxe9csHe/SXs4ABl6y15AmOxHTj2dWATecl7i+HRdJmxABjvRA/S4rMReE5CC8O2LoD8MXXiHo1Yy6Q1W90vZV+9DmTZvDwX9/D34rDx7hZbpwMEyFqEBiAN96zRSUUHCOAYRzmSDf1YyePsWz1U1BxOFlSJidUwIVRTGct04ASFlywJhIAor0hMI4YUYsmSgA2G8L2mnH9nHHzwkdHTTomXZR/GOnEyPQtAYef3WL33Z1IbXONtxkcCy4oI4xZF/mwSY/xM1XthZK57jq4D6+B3S6lJUQfDGVb/h9/xNhufQFOjQdE6VyxRbWx9lCAJMd/Hq6MSSIFhufSPCxFkGEivGVWpZM38hzXrHMCAkvNxi70dhDFrbIsUjmw4M1vCrK9HQADuGCcXN2SC7lyaS7rYg518jxr83MaaNC8COmULh4UkZ76c+yBjgTIYATtr4nSJU/pExfSEAiexE3B1uVPcWhzS+MCWOxXrPpMBPYDEPPRzuE3bjbAa/NuWnJRDWAoBZLA4P2Xr+G/uKtXhMx3YKTdp3b7fPAY7jg4eqSRY+nylarsE2WhifUs42tGxk2ySg4WqMnC2DwipB+sloPKLSkkpZbnHKHbALtrxmbL6LbBQXAINW7r23UqIzAAF/qlGF8uHLx4xvDK4/DpHtvvX+VrbwEIyiUabcirka7C81HeKIe4ykmeE8hQakTYSWzGTh4pAuN05kUlEALZdfDEPSKgI9FgUIABJl3e+OnddPwt13rX4d/b/MaCw5D2fZXNdM/YIhtdQg+PzqJ9yTjJ4X8ONCTKDzs4S/smaQmoU3lqRhbV8ZmuLG3rCk0GYbbR9pBzlhYRcGVikJjEH2w9vn1N2N8SDo1NayuShKNpJqTljvbaeg/CKHcOwBbYdowhbADjRlGfYQprh920TJe9hDTUS6b2BUbngF+58sL0psCAkloVK6+XaEsjDTOD73v4b/bgvY9RJeTZsamELgQR0SLzDooic3AhFG/U8vwEwGDJbrJjuLvqZiEtUra5pQOjzKmkYZYR3CiAjhq4ACA4ObJ+F4Sh2L18rDfENOL6OeP6hWgvOHMKEavdIfaH7U3bzrs/2uPmtz1cpyBEuNkyHakBSyVTy+5rmtStVc0Fcz9dT7+b4EJWBgO7DdyHVxJiyZwCRVQldkKq48u9OHKN4ALnp26Wxv4RllHe5AfiHtpljwA2kNlkPwWqOYQ8PdN2Po9n2nCmtlzojZKCCCr7WA0G4XkVoIEBpgRQ6DNyaxpsaMoxlJY+K9usJX1eD3CEw5jIGZS0QFO9RPj0wUxCZbJS9iOEA+MQZ17/1WW8c4SOBRe/6mSftTeHvlO0tK02H2veywB+0PX487vXgCodzMlQNYBBhYDAu/j1AfyVARjKfZqJhJSXASTH2mIC0B+cbPgLGWREKn91nGlXqYw5BZZnjqw5yWAjR441ucgeDLH6YEhanBZXcw7Y7hi7G6Db+kzrNNa5XPM9wW0AkIfvnZhbMBX8Stre33rc/eQOmw860IttuF5vvIzr1I9ce8fRpnr8DuNz+t4jyFDkpSo0NXDBVsumQ/pcBC5oXlau0r686uCeb3PNmVEjOGeA9neFOTIRhh/+KfzuTwjbXWPzNkNp6syDDBkt1PBctEdjzK4xc7Sk1gR+IwcOI75xtCy5oJcsDzo3yBCWKag7JGVKrZfsQtxlT8DzDeO6YxwK1WVqfG+lOXagJDvBeFAPdKUmUgAfkNS+qtNq5YDNou1QOkW47oB/4de2wH8W9aiKciaYnaUlcZxHC2vyaAxm+FcH8DcHoBeukkJIVhpElkmM7+UoQ868wDlTs6EfEZiZb3QHgBRNYuSNFnGgWKZbQ+pRIvcW2DD1BZCZT1jTCAUUfKYiyNWuB4TpfvARcPOBx2YXwkypqYcBaTobJ5jGm2PdjPV7J4DQlXGMmBJUaqAdksbBCFyofi8YqVLJPGvgQjYOKkxS89w6uA8CwBDDapgGl20In7/1rQH/6BsXN+gWBMofIfBp0XRHi/YixnZiGVlZnI/xPEGqTzONoacY/eCs+/7VQAJna0bM5okAMRd6s6ShHqt3CqCBNJwlE1gkCRGsgWw9bIENdukDRsvq6L6tSy2/VotsebXNqR4YeebYTruPs2YTEmJQNnJMyQeCR4pMAUjajuRv40S7oQ/tmT1IwrL5WOW/CoDsX4O+/twktg/lvLEqc2Vq8EFuenmH4Zevct6ma3DtZZX5eoZ/PRR+GNAGGGCj8dg+CXAR29+Jd2TN4/KApaLVGWWncD1z0C0lRO0FJDmRCFF7YXvF6DZBvjHAAhToNo6CmQNoEn5LhuJPwL5HxPtA/2WPw+d77G42IBcGnva/PhaQu6p9fjm5kIMRedKQNqjejMCF0Cz7fSQDjbQc0s1R6EkLVNTAB5uvI7jrDXCzWa76o3XQDXGc4OFFGbOJ/1P/K9h298vyrVDS6LWT5DzUksPCyKpefUhSSPqxZAcZUg2h8AnRiugSiSEtaQOZTxeYSw3kifujRj6Zw8llRY/q4UhU5uJBr+Zp0lR5gvneITj6YGA/APd+vi5lflaDbssev/XVT+oP2p12yezQ+F2mn0hj7cL4tof//A7+dS9ML2oxVCYxpY2u/a3MrKyKr6DjeXjKZD5R8vlYgE5cYyZR02KIGwUDEPisrPQ9qgQWTFdBjhSKMr0OtuAL534YBvu6ONVNEH3Ci48YNx/4aIcY55BpR2xLsBFipnQomw1U0aDgux78rAN1Eyua3X0quNQCFIDMoLUJLmRCUllcO7+sg5QcgZ5fAVebnME3gRJAj/T+yv6P8f/c/FDsoDmYp7hx8vMAAu1MWuDGhR6GHlLjY2oITse2pmaaC71dVAcVZMHOzSTyHY1GmZCtkmg7MMQDPYNlMw4C6wZqtCMajx2rwWBJl5u4R4j1kYRTTvfb7a7UwFwMMTWM5gab6xTaq3Uj2QgiLdnanigLOmAzZiFZHVbP9ApIo7Ldzqpd1+SoJQCDjSzhPbDv5WCmpopbmvnZemnYQ8843CKG9dZIDhZgKAHkqA0wEqbNM4U8kkXFCrJFdFyt942phD0Uyh11J9t2CzBo3bY74PrGo7vKo32V62rug6cAuQii0QAfDlnqfdjvgcPnB2y/3YNuNoihXewAUpWbcvO9whFK8oHAOcBQ5pFpN+jDlQFuhIUpcCGW3ZLVABkP1xvgqlsQWa14mAJSSCwvgsNLsOP25gr/8ad32J5gagActx6dSoz2od8pNAV4PiS2UJNl2fz7lGm940fB58Mvy0J0PqeYyx0YA6mnYUGw51DrZrmG4a3JQlUDXahgbb4QEsremTlGlBwrdbrIQ9aF217++gVgg1IJMnSffpbPwBGQULne2vxZqqbxKcswYnk/wH91D/9yL94G7SazctgSNQgMAynTxOKtqUSpjscWgEB2P5Q0yjjGfm3zbgA5Cq9hMrUO0g1J5c8yXtUayKNGIAIKmq81lQCCY+lKdxOAFx8Czz7y2Gxz1cYsXdEe5wDupJyogav2aaTLCmH47A7dh1sxaCyJZZElCy4AlbEWvk+BCzVGWUziEbhQ5JnKywYT6PkOdDOj4qfPmrZh8PjWV1/iv/r9H+A/+OMNmOpmjunKwxHjzTDR94lyxP4pdvZTrNOFjqUSaKibTQRJqJDuNdiWaDI4qEYDqw0iwTBXZKVgdDVIUgXfsGHqazQ1GlV+Mzh3BDzStZRG5CUJd6nmEB3pupfmJDNFx5EEsx6zARmQDnpUo2EwvGXgvBda7dMe57KtlX0UAHzrCvhL3wHw80KAnNICnQEY/O0B/R9+iXRqNbMGhLyj2j2A/pVEkxBnjyqPtHYwnPlfqPWNNYeQ5lGUbxRs0GaPDn44BxhyEELlNc66TMuRwxTg6rlHt23JaZb35zJ2PLwMzXeOgS73y2Xt9ZUOnx0wfH+Au9rIpKh2CqAh2mtVsXVqnjKGaGvRVMYy/ExmnpCbjgUXKvdjla83oKsOtJnY3payoM0T0BAJIUOzWXIOf/2DPw33Rfh5wg76TclHc8V6ngYNWvSQYEKLlnThWPZ9WFo6JtaZS5hxav0lqNqcDwi2fUbV5J5vGfeecNdPd8SIcdg6NICG1hIf1fqoPjBifOfA8DYuzq94X58rD4yvO+CjHfDVXjQbbP11UqnaYY2uGUDfI7gozTdhLeBhDmCYABey9MGjMd/1GF7eC1ISkVgjA5WdXFB1TQ8vUNDyCYChiL1cngraMtSL8pQTmOQTIWeOpRogOGe+mSbDYu0FYDBhK8v6dp2YRzz7lvpdsG2rcYtc1BSVSDIn8+Z+uLb/43tsf+NFynrSFK8UnsL3gplxJkEUjLCWfgm4UGOuBNCzjTDJUsqoN8DUnWS+9D3+2i9/F/9R99vRgZh19vWY9NiL+/tGojD0tDs4KDVd6K0nWaOXAg3VxxGOWJjFP4N+IvjrIQBBy6HMw0Z8yPmFqUv2CBXfObICn101G7oAdjim6F8LxpG3yEx6mMSp9aEoBf0IjEFNIohBYYM8tanQWyoXNh1yTVBsf7nvbKz71/09vvfFL8IzKmP5/HMOYLDXmYF9D749pEK00x1JyGVLEWDgKDTz3YDDawUX5C/KJKaN9lAnRZMosi82/taRtcpiWo3o+wqFbBQPS3L/CzZM90jWccKKr24Y22tOofIygCG9pOYJsAIMQQxgiHPIqFXLKU+bBw+A3w+QcN40DTTM2uSYqAoI+YT3ngAGIxMtBRcKmZyHIm0mf5WyU7vOdC3yU6Ym1CIO/FP3MyXAoJ8UNkKdA/7iX8bf+t3X2Bptk2PdDVx445unY4GeGpCgeZ0dZAASs3IkzEWv6Q1CCl0EAKrx0BHjxYaxH+RK72k056OgXjCOkv/UgIYSpHNAZJ5uYoFTrYWNE9Bg69r7k9KcQgMI3nSi0fAqaDUspWttANE0o6t912fRSGuuc5mvAgyHQUIh7n1YRI2HYGU8WfuDqr/jTIvBquZpvZI6HnKAIdgAWoChjCSRwsFxBjBEB4kjhDxvemZjOGjXWvOMvI5R8wGpzoCNFhHAhSFtdIbQT+XEdQ7Y7QgvPgKuXwyZQDCHlkZGi+AsNDDsoYfYHFrZE8DhjvKA4yWnRgEClN/1QCXr/PCPz9PFfs4626RR8sW18FnGdKZdB3ezFWZGps5FuvjFOVMgAO9BBHzQ3+F/uPnP8L9yv4mB6Y0h5pbeNYbaAmhr9JBtf8r9GnnXmzjiuNAD0BzQkMsh+eKe1jwBUB1AHoAD2MNRJ7wHBCbO8o8x7Ue5pTqVV/O6JR4JqCPGsIqazWvaR0l5EiApOGaD+mMg8RGkOVNiBw4U/TVomiUaqppE9+Fxc+yOnNuN+aYmGUNgKzfEcIc+8cAWwFCTo3wQGNRbOEPkpy9fBTVG0zhTnwSEmLCHwUyCGejvgOGQfCJYgGFMY9nHyltA6ktfHNykv/rBi3YDZ/VI9705TLEACBGw2QBXNx7dDinaV/ZOuAAcyv7hyjWKzeo6Pdghkyb5o2APDD9/Df7WFrjZhPCVnANXWqcZASxuvnWCMCdAwAIMVnbKfFPxYvnHylB5ZC/kcppNq892JOYRVx3gkMxMQ93Ze/FtNXAID8PmnVAEGwCALMCgdM/Axx/h//7Ll+h9h42jzHXDufwaHqvRfqG3j1b7ZFCkG1CAQRaCLkzkgTnt8GGj7Hk83xIOYfPaIR9oekgJJK0kIAzqfK8zaTqhc66LFRyTAg8diRbDrhOPxx2le7VJYMN4EpIA/nzLuDmIVkPc0NaLjnl//xrA50ZNyXgtjh2iny2AoZbWfGYxeXVBG1gAhr0Xh48HnyJKwDB+qVhRc7NAUVFcrKZVw8sdC6WNu6LpOSMcqwom+8OSgWWINitzpdiG6KzI5K0mDlZtEDD1YuvYkUP0CIS0geE1hAHngKsr4KPvMDY7HtXV7P3H35GnhYI5XgEGSaQeevVV8L4Hhk2+8quwY8eI/e4tYysFquI7TFrN21KTebJJkk9e2hDoaiMhl4CkaqT2gVYfVCfaMBgDTsBG5PjucIv/3ouf4n97+GEzes1j0gpzzydNur6tocdq+1MAk6boIkC9vaRaChZoAJA28FbLoDI/AudN2g6UtPRINzABpRd81cf8My/4gaHWtCVKnxG5e0VAHUtSAHDVVFTrF9h0tv8QDxJSP7WblxNwDuZo0l5iASWitkRjrNf4pK4puuR3xWawYBWzpNBzuU5tg7wweGDbAd/fAnhVnIa0AIbsxMJc8z7Zdrw+4PAHX40bVzn94JiXylkMHDwO904wh+CDIfWVZd6Amkk4VwgKUa7RKo7BhSjbqKxVHiKpyOnJAA+I5hM17QX2AHXAZgdcP/cS2tBMCQCwvmmm9vZjUAIgaOQWcSTqe46KvmViZsL9Vx7be8bmBgnYyzQJzJcSoRm9r/QAe9TBhQwUmJZ/RgCC3ovJKnKXnTPlhCBIqO+gwRBrHzDM9FjoB7U/su8nA3JYgAYgOZyDBz77Et/65A43m+egIZ9fx8gFNbrwyPeHFoMMqh2gc8FF+3BlHLo4EjTOpd5zJBoOG2JsnKDejIQ2Ky4ReGIWf3QIG40u5G5NEXSslyZXtbrb9aQLz3ZONBGuuvF608yPkp8Gzbsj4JMrafvrPtSb033VvNI8ew/8y78G4Cc8BhiyTR1XrhX3bFrzOQIYhsDkwifuRd2P+0GYISMBDVYiAZKZgtn/ZcBCYHIAoqpdDHc00mZIHRmZmkaXiGWoFgMyJ0d1DYaGc0ntOuP4UZm5t2BE7OKK9oJqNQxp7KVuDkInEboOuLoGPvwE4lFZGxI60YIKVohgT0jeubNbos3gCI5DjHIWBozQL8NAYKvJUFKFGWaqelZ6KMCFkWp6yfjsc+W1CYABBHHyeNWltHFyFOXYsnWORLMixMFKDPz6cI//1m88x7/7T77B3k8sBI9MTx1wUJB0bv08lh5LkFi7OXlo2s4nudCTJcrWag4MrmoaYSgHIUi27IVZhS5lkq4DswdRJ8I+A2xGMZldxBhU0OseKTxmMG5gzq5JNjx6tpzzHoBjhjcqdQyR2wbOD5f4iHXNPq+OuOVHOHBSQHkFqQwXJU9Ksp0jYHDAiw3jv/zJALxCDhrYhpRy1shsMAAM3ots8Pr1GF1RgbRzGVgktnxBgwEABsb+NcEHPwxjgCFvoJokZOEqs+rljq3twYmkzTUUwAlISHJQbjZhTUXLJroNsL0SDQY1BbXNjeN7yZF38axeE2eSQTNiQ/B92Fdkoy78ZhIn2MMG1NFIdonAnrRUPopNOextVtlZNyYQ+Tg7EYUZO6jL3iY/ey/TXtC6lABDCV4obR3cVRf6rTJZVJ7itGJQAA5FvuL0PdyN4KaaS8AB93v8M7//O/jxd/48foYreFC19+e0c6doa6pzoXebVmkyBOwAHcI8zdZSNkJlQCFZ4yRLQg0l6TnX9M5QbMrX764rhHUutJQw9pdgso3lRhVCsk4eGTs3NqFb0BupMiHPrQO+tUub0SlyYLz42U9hkVPJrvieMcQiTeMzW+ACU422gAo07Af42z5qMdjFMAsraetsmVxcjPWT4u/E8PI8lFePUHMLMGTltbwom+6JSDwygMHmG50aRY0E005ODDXylDCIFWAYjDaDptPnXScqgzfPCc8/ZHSbVLcEuSUgAcj7sKpGqKc74fSCo1dDg6pB2trcHVrhCciBgywUV55G2xjvw9xXyk4J6mN3pB6obds60K7Ln4kqeyqBmnuZbQyluivYoM5Kv/4Cf+Kn/zH+6rd/A//Rl89wO4RTuqeDNzTpsU/lTxUO1tDCUNdvnOw7KE3vSlJx1/6+0LtLFNBfZj+dDrlpRZEJwDbUpV6Pnn3D8t5V8xDwIC9fQAVn0lAGYtjQmvYpJd30MxILEaAB0YGjQzoMtb4iVJN0zdjXjf8GEnVC69CFfKaCJNXIkfrS4gCUUsynCwdgAwPPuMfzn/wkBxjin+2agj9qegMwoB+AYYC/PeSmEg2y4h0zwiEPw/cu17KMPWQeCN8JgGruZexWAYIob1GUe9KBUTp4ieBC4PvRTANtgMGWRyR+Eq6uCZudz+TBc5OdFnrYYpyZpD4Iber/+Babb+1yJEbTWPCv9r6CzTSXskw4/WQgoeVWXrKCfil7AxXthqJcCy6UeReyGJEMdvdsCzgyJhLhH9XeUCDBMDEtgfTwxcEIR9KnqY8Qxr8DXr7Cv8Z/B/+X7/05vNq9wJ57vDqIJrqlt9Eh5IUel1b4ZBCEvwvouG4VRqd14bcgkWR92oGIsSFgUBfMAOweKq77lYFbDsjaHM1rGxZGStoEypLFLIKjh+M1JGCIlCZeVxVIYVxvGFd9N3mCRwhaGT/9uTSz3MxlKHtlw1cDIlAucNKRHNVEZEPGQ/DH8LoH3/bp5cU8IluLZVY3+Ww/E7MDJy2GsZmE2fR7c1hg+4oSgj0LMCgjjUw19wNh65OeycGGLHIEC8CQfYcFGBCfZQjAsNsRnr1wuH7u4eLeOQxkI4gp0JA1NAOX5H4WGhQE6jgIfTJJRuH7SiHJMp+I2oWxYQWrCYaW3Yu/Gy8BedoWuAAEgOFmC9q4nDOppBBNJNgADcgZp07oEmwAcPXVN/hn3S/wfPcd/F7v4LfX4O01vry/w5d72Si8yxRlC+imJN17k2DL1Br9NpI9jdWp8i6060JCSRMhX/g0isQU2JD5RQiRJdLzSatBxf8yQkXdJ4NuNBIzZNazxSKNzbtSXsl3IrgQ2JVuaF2IFGHTWZnGIfhxmKOwZGuPUoBFNkhyHxeswGLpZV+oHb8jYBtkt6AzgoE5Rq5wxOiZcEXAR24AvrH8j/NJa2WqaiQJNgCDBw4D/Od3s01nIHgjDj6vBgYfGIfXhOGQ/FHVWqpajZmpqOZrwYUwRFXGyjQ0Cy0Fm97KZnJoJ78HI/9oPgDQbSR6xPaasVFNzSN5CnM+BmObFVgw046C3w4KzkW0PqQ21ADAhP3XHru9h9s6Ge1Zh+lHbo5UqVj69MWnlYkYbXChqtlpizDpqrK9qa++iKCeowBDXmfk70HLt+FnCrABAyTsuU62tGFDjBsYDn7o7h7/yqd/H8N//i/gJ4eX+Mk3O7xyDry5wte0wcueTYSZUGRRvTV04aVvFy1dAlZEl1CDhUSiTucz2z5hKsKoHCWVXALDgbBxMjC7gBJysPVr7WWy8uw+qHItr69qLfDIeVmsU7g3R8y5w6dNZPypjC5sjq83jN5GS6Dxy7juWCJL1BpYHqPVmGHxOy1eqTOiR1yNJhGcO/JdD/+6F7ChF+4TF3ZGRYtBHfpw1pBsjxm+D34MMFgAoNwk12Leq5lEknoa3WSYpP7OzCasY0djygGM0XobPYJ9HjlCnk+rH0P9Lzg8/4CwvfbZyc6cpkLrum2/lqNgiwuCFFnGACA79fII6FViVpnvBQsoxTFl02g+jQml+VS+c5HniDoSb8ibhmdVW09rU0nIVRKYIYCMZhLACfVY+vkv8WfdZ/izHz4Hnn8b+OAT/P7tN/jH6DCA8E/oBq/6c7kuutAasnKavQakIVHKTG+LwFHKoW+DBs2FpmkObAB07ZwfpVbLoAU0SN4qN3D2bCzPAA627Bzc0Hr7BGKMgIa8jMyvcPhCnK6NW1jO1CTjaQ308LkzmZYOJFGTM8o8wzU90OkI2Hac9cpg5DNAzXAZWwJ+9PwK+LxSfSDJUAp0c+WaAgxeooGgH+C/2tcFztJMwgIMPaPfA/3eyEejjrXjrOgPI8ckB93Gp1WhmVA61bbOresRvzg7UNFXvNkAuytgc+UrZhHLV2hzHjB6zB4mKcCidRD5R0UCMyKNGOB7wvDpHdz18yDc60IMWIGqCTRYJ3AMkZOtHKTfa+CCXQKmwIUyXfV7IbNBAAG67tJpaFl9XS+ysRc+VWa36nlE4CH0g70f+jbrI++Bu3t0/+A/xZ+8ucaf3HTAbgvcfIif4Bo/G0QjnbZb/JG7wm2fh1P3RT/MUQ2jeco0iVlpGpyxPZW9ZLVcXpZ2LZXtOHt0CUfJaDr+y7JwOeOVWJmo3XjJX9CEII7zZWigmi2qIV05iCj56ZqlIIej/KkScJgSbmXfYxd/g8xTrhVBBLzYeNwPFKNv6DM2v092w7iQKohQ3C/Twy5g6TM6rVGVr2AqwQcG3w8Sc1NVzZVh2SJKJkDpT4uXNNJ5kVllvhiUwalWAUXG2DKTsBEsahSRe8NUa4w296xMWftGGgwVgMG+jmgiEarUdYSrG8Kz5xAHj3GMwAykyrg2r7MUHmpEwQkiBZOJxCcS8+P74jzJhlWyLzQATSNmxsWzmn0N7KrQLLgACAr/fCf2RJ0ZRCNmaZirc/k1q8Wgna1ghAJgejTWMfDyFrg/AF9/jh9tt/hR59CD8Af0HD/+bIv/4G9/gI9/k/CtP33ZDb5JKjflJQCXr+1Pny4gw7tHuclBIUdEU4rEICbNJkb5jp+pAQ32GbnDo7LHkTAcmNI6PjadGLfJwte6QGf7E4y1GvRQiRTeMLzQAgwODF/M7bKf7GGQ7H04lrlxHMwjZDUYNEIHp0Mf+5xnwhUxfnh3i4Khmwo0rsXDmWAqoSB9L2amI2cFpgHRcajKDUF7dNgDw95h6F1wQG1bmt6DHuioBoP0ZQ4WABiBBCr/IKTxRuNBAYoo76jvBp4AGBAAhmvk5hElEGSaMbfupXVeBkgtP5s2ykvhoIXDeGeUZRHuf3aH7Q9ukIX0YhQb6VTXSBZAiH1l5JrscEblvZQ2XKjKP6N0Nm1FdspkNgDYOAlTuU2b9xFIYutQtlPLztTJZYLKrKR03xmggRk0DJAY6j1wF/K9vpI8vv4Sv06EX9cyaIufuBu87B14s8Ptt34Ff/jNHXodd6h2T5Ma3fnkyDrULUlfBygNsVrSJe20z83KFlxsQ85I5Xs5O8hAmacUle49UizlMn3AxXRxg9rRcVSH3nDC2ceKgnUqwULOrqeFWpnTdJtMXQNDHLXGrk0UTC/CApm0GARoIAC7HXDwYrukYfXyOhP+qY9fRJWkdkNNw+YABk3vNVxSABgUkR0YfPDgvQAM3IfrZbZWA6Nou02nAIONSCFhKVM+mX1gZG6mKcWQyUIgtRiWLlpqcjEYu0YTdikLxWTkBiAxUx9U1pK2AlIECWNCYBeLbkO4vpG/MoKE7Z/RCQTq7Yl9GZmB9gWimqDTflbmYPpt+OIe/COpMDlnGKRhnECKzV3aNo3Gz/LtHNc0Hmx+BGDbwT3fSsglIE1MVJilJVuPDNkKg0NNKPR4RD/jCcYgXr/2IWxZ57DxjP8c7vBt6vDRxy/xfAs8v/kO/r6/xhcHXOiJ0lN3nnmhd5+s40YU34BpYMDmoXxg5Fi3TFus85P5mrJrAAeFwyHrgFJS24U6yT00EoASY7KAgwvykufcJab2lAMJyKGHUJV5POIAFMxxYVhFkLG2xNgFGaH3UqYPzDke/BAHPxIEch7X5OF+/keJwWcbPMPo7Qn1MCRQXjUZQloePIZf3o492pabO0AOdoK2qD9IuMqhNwcuFVL5x1mAAUYeMYczOcCQay9YfwpZxC0jm2m6LC0ndrrdMTY7cWTtSk2D8I7Pt4mpj2+VERgAdRD+7qmafDg4+PsBbuPyiAkqWzkjmNn3pePBF/J0pskQ9ChmwIUoE9WalclaRT4xb3OhcxJFYtdJhLGpvi7LseSK/mIEUCGUSQEezGym9BEv2rPMwKZLzuuysJgADgf8Or0GmMHdBvfdHj+4P8A/+wg/2b7AL/a8io9L95x5h/wAVB5eW/JM6b5das19q8+2hHTfOUWa70McdJTvZar9llY4flRklBHtDQmgQlUtewaJRbnA1jZO1b5hIlIAy4eg5s0BJUqNHjn8RbavOYq0IxW0cME0YhP3Owlk0DnYkUyq+8FhP0j/DEzowwD4zs9+P22ySjS91hClGsAQ0kbzCCA4eRTBQpF07j34TkJXRnQ+5JmtS5kGRgttLn0bELJwlRzShIWU4xFGYopFdtLXE1oM8dkSYDD2h6l8ChvuQpZomEdEgAEFwKA2gB3BOeDmmcPVdYggMQGENAdcuCf4UpodnsW7eFRRpQQ0iNAhwhwXY/nwlce1zT42uGCWXDDQwFQjmU19FhGCIAyubMYSjlEDGDADLtRIBycZRhgxzgDUxcgT4ZonERatVBzo4+sD/rnfFoaIz77BJ9jgd7/7q/j9/c3IvvBCD0f2tTQ1GTB6fRe60BujObCh+oyebJtccqlT5YtCE8FMhPR4O20NaBhrSLTqUWdZGagABF9USUu12WakAx6Vn+IeprLPs+RMxrq3Eu1XkbOSr+OkPSoiivrGAsSMl/HJts8BBF1QSoAhAxe84SM+ajNoVC7/cl923Zj0YKdncC8Ag0STaD1n32UuW5SOqi3AYLURLMAQczW+qTgewqR0VqtTqeskesRmJ3JHjBxdkQNP3sgQopbw6IA+jGe9x0zRFwMZGdJqUtz/wSvc/PZHqZ7W15P2AwzYpuACMNZo0OdRyET6nOmzWXmoRNh8ebuQ/zsHdxMAhm6F3FSrQwmI6fG7enS1YAOR8fyK0DdhW3noZXCAkid9y7DDokCDx/XnP8cPiYDDK3y72+E1Obz8le/gH95ezUb/Ej8tcxPsadAk7kNmO15JaO8v0XybK69arj5XubaGZNmUhZaz9XlZvddrMpCvIPFpUKQ9idltkTVLUD8IsrkqGwNMdyaFjf1Vx3i2GfC673DbuxHAuJTmtB1se9QucNdJ+WpAos4fVath6xhbJ5GnmcX90zcHh9tDCD/zj362bIW2i0+5EDHyk2ob09cADDGEkmoyDHmeMVuu9xspI6hU14fNvu8p2+CzMjuGUeEzDLAopwQzSoaWmGhoYgEwRDR/0PJTWxLAYEGFwuEj5wCDJecQQlQ67K4SwDBLnACD1NC87ygs5AlUz5VaST0BOwrpco0fPxj7YPv+gdx0wgpa8R2kTmoySUYeummyvSYPQlDzczA+XvO6UJtxlqYc0Wazlt4CDTpweg8gCJiu4qJMmeOhx8fM+KdxwM8/+k0cXDdOe6EHIRlW8j7VG3wxVNKeYMGEO4WRelOXC73fZA9Tmmni5t2ABFVthnIjX5o0TP8e16nMf65+eT0s0NAsC+N5KHVIRbe2AAoIOBCGEZNP+Sh7p0JILYEHASuSbGVJo1Go0+3k4Jvjs59cfZAf1QNjgAFIAIMF6eOJBFL6+154S7Xx4R0NXtIcPLhnDL1oMERfVYYVh2+pXzSiVg1g8NIqCzBYn1TWwXV8Jsp1dYAByAEG54L/hR1nhz0PZ/6V2l5SaTYXx4qTtqjCf9ReZKB/OUj/d0Feig4Oiwx9KjszNw7arbZTovxTAgU4AlwAsk3/CFwI9YwaDGsAhqVk2h6clySwAQhRKFjuqXwID9ofjMYoxlrYEY0aErDTD3iO13juHD7+6S2+R1v8zq/8Gn55v4XWQkyBkyZVyzHohcbrpaWafztLSwGBZv4WWg/y0oNoMkwxXh1yVqRXhqLjckPq+EeY0JwLNtUQsBt9dfzz4ZbxYrvBwD3+ySvGfW/8IJxpTmZhlYIqnrZp8CGkj+MYRkltELvAKDoCHLYgcti5A242vaRVp492pxt3xQVDtOlgFkUNS4mwwdTFSwGG6PAxaTGgl9+aLnQpEJjUVJSueKrBME4W8+9ZGCRF2U0a26RR/uVgCDzEai/AmJ9YDQZUAIYcaNB+kv6L9UMAGIzcASCGsXQumEhcO1zdJIeUmcrgqCH2S+pjZmlj5HkuLbCT/R2yir4ZjAjo+5xJwCNpLthxFBkbJ82XEp0/hXRM6vR7tgVdOWTdFI/EIqIyuZ7k2dc2kKF/iYwmg8lPvYG3TJKMRHv1ySfY0vL6XOh0EsaztL/zdK3IracRpyhgyIWfCwhxoZKW+F5I0SNy2Sn/PQ1EZPlNmEaMytCyC0BksuxiT7Z0OVS5x0cGwE3BUk0agMYcplwuVG2GpBFR1jmBEGXaDz79ZRIELIggGRltBTaHNJXvANB7HH7/q7qWqXZWkMt4EHNUPyCYSBjzl2wzoO8E2fKiLA1ABjCI3FMCDBR8PBhxMTrclvKifJMBDLnMQySmvtYMtBwPVfTpXDSRbzyECQctOnBUxkmacAR+PYCeBe4SZKMYFtzw/BG4AMTxMQIPine+SJPzGIABkBDfO3c2gGHke8uWZb8wAArhQolAAxA3PGEfQX1v+tCACdI4o75k1F+IABrQDQOedwf8xZ/9Af7Gd/60KT2tYlZMvFAiovm98mMCM4rFlcBvi1aEsJwmqwYnI0UuiNOesLlDQFxGTIhj2pJRaN7pMDOYKzhC5zps0OHF5gCCF2dARfilJjUWzNrLtC9ZQAaOflI0XrOCIYwkqHoWwGFDGzB5POsO6GiXGF5WH7MYVDQYsggSuomMqn1Wc0EaF5099l78MaijR81D+yCriuV0uV2PRcUjmq5MNESVEGaWnC+mvCnLI+vbYGepSH7eJ2NAIWovBK0JAUiC8GHkCAUTYhcafwv6PfIYnzaYCj64Tpw8vvjAYbNFJgjoQXj5+ux3C7BJQ8mYIch7UtBBhYe2qUgIBxvGV0KTzRgZtO6cVgE7EVomES2yE6Hy3vIGIzGfq060GOIznOdVKhZMrZ5GFmaC9FtgYBT/1TEdGpSp74UCrT4+EOxyJA07h/+QnwMdsL1wuAtFGg34Zsq48SEAvBzhv9D7SmndqjmWtA4eR09WNSbG+eZXx5oXU9eX5cpRLmNIYKPyUMY+b2+p/yr5XpRFwWSisbfqisMpouR4uzMHUhtywOefjRmzBQ9aAIMPzMt7OcDRw5rDTOBOjeI1CMCgJhI8JLlIq6F9qJ9qIpkd5gS5IFZ7SECDBRBs00rziSTfmAOgCp/bXQObLY/kvpKs7KPmnApAWFEnHlgFOcgCGuX7K8tslR9FCq/1sA+FNK974KabzKilvVAFD6qgkqnQEio2+lMAA111mR+F+K4qTZk7+M32E0qKpEPGZcw7LAPEAFzQmNXQ6vIGxWQiFY7RS4sbNUq/ydzbeHQQH+BWi52RO+DXV/MQNByAf+fflHr/4M8Af+2/Py7o//g/Ibx+Kd9ffAz8q/9TTuP8RDr+cOQpyRQKYC+r0wpzCd01WVgwWeqldLmvERfs6VIYyHHFlOmIjR2P1Obyemj+HgMf4OCw6wbsPWGDcdzWFq05DatFp9Bn1QnR1iGh6jGhsFcGsHFb9Axs6ToHGapgQ7pXOvLTUJRQUIGD7wBlkkMQTYYQPqlnYO9FiyFwgrj2BD8EOdBQNJTki6DGFBw8pk9A1QCNt2Nj2mAdEY1VeoJQVdloJlQemamFjSCRfCzktojRNCIwW2seoVoKg7dlJf8LWh8FGLqNGYshvxTTOYlfZf+JiwAyeRqvyLr4KkrVWFUpTTGAgm8GnxyIOfFyJZvl2GHIP2uVA6Y398gZeJWRlZOeBG2jXQcFGaOQPLQeDPeMdEplyii5SF+RF8ScgQwxryH+ZRU1LWgANhv46x3+f7/xp3G7u8I2ijBn4CQXeq/Ic+JZA3O+2brQhQzVtBtavh6mwIba/dxhZMs/g0YqWi602pSO5LCoI4JjBOeO45TqNwHZs+ETiBp8pQzWEbAh0SISmbGtL0JQgIHgyMFRsBkH8Lz7ABj+cQ4eAHWAQeUpe53TPfaMw4+/HNu3Z50UZLODh+9Zwir2eviiO+AMhYEFF5TtWA2GFFUiyVvloUseFaJ+Px7QVERO54DrZxzr0Nzgcw4mkLGfsfdAuZwXR6BhrSIrMsiLc9DS0WXsnlh4uNQpLsCAlw1w6Zvh/ievcfOtXaqf3tATTM3PAgxTVAEUiEKr9F55YDMCtvLnq7LexoG2Lsg4SAdItg8KUGVKS2F0CGRlzN6+MCRky4mcL1oU4YURyebGkQBu+0NeD6MpSkDy16B/6qjOOQxdh8/+7G/Cfc0Tey82ma0gBu5fy0OuA7ZXqf2Hu7Q/+Hf+LWfmI2Prxqdtm20XABjCy0+Bf/t/APzGnwf+6n/Xx4hyp1IpIyi+lcK1jtM8FRrCOFpavxWaDHZQIiyYHNAvcQQZ3EECiGMWXUDCuNioOwMmUPG7JN3kayg/sQRg9L6HC+zzqlMeIGnP6cTNlah8wSyJDMAgMw0U/nPUhfCfwAaEbXeVMo7ggU+LQA1gyJw7KvNDAhgM6MBAAhhUk0G1GAZOphVRI0JR9iSglAClaiNkPhCCo0c/AAH0HyPser228QWyME2kY4qSnWAOJlCsbxa9wttuNGKapmULOMj9wTDm0vFRDjDIGM/uQ08k4u5XluNi0+4LBNzBaiIAyVmBhPYiL3tfauqZIgITREHrgRh8YGBb6V9dxQMIYU/zT54ZJZMMk5g2LtlBroGiddFwLh8iDJToD6v3LwBwxnlOjWvFdAGUIJIoHF2H4Xuf4A9/8EO8oivsPGMAZevXhZ4u1UJCneOU4VhSxSKQyIbnN+W40GPSY5lN1Uwc9Bug4EFyVNcKczkObSlXmX1+z4IOoWxd9SiM3YQHc8zZBUfWKlR2RCJoFuPcggdAfR5ofvaeaK8SrroON/QcLzgcxGw2eIXXuMMdBu4jLq9162gLRx06t8WVu0Hndhj8AQBh++qrAkhQrU+PHGRQGcvYEejJtvbEvgf3M1oMjAhIcIy0Rbl5AyxbzIUia5qQ+VqwwAIbGQvI5aIgb9nQ4H6UNh/bXQdc3QQ/EFF+SbtqBQ+s9oJWORt5RmZDkTaTNsI97xnUGyDCpXGjzh0zotR3hOCCCSzan3G8KahCIvt2oXY2q1hHzoEBBfusGUbM1qAMVg7UOrWAJ/t6TZLq2uJIfDBsTOjumoNECzzMLVG23HJPYa8peQJ1EmZc+i+8WxbH8QDyXWXQrI0rhspYEGCB9BSMCP76Gv1ugz/8zR/hs28I26bG7vF0uCf87/9NOd79k3/R41/8N9IL/hv/tsOnfyh1/+DbgHbMi4+4Wpf/9v+ix//hf7ZBf2C8/EwE7z/8u8D/9/9M+Ev/zYeREO16WHoGq+2L7fbCUf5bn9FPW+O1skkNx1qbx9nMJUoSRzwsphJIY7ozCLZ0XmI4sWOoBWTlzfXM8BAkdIu01/KGASubPlZusNElyrpJGxiOSMw3iOCwgXMdCA5EDhvawtEGzB5MHlfuplj4ePw97oo5MkJmBM2FADBY3wtAAhACs0uqez6ZU+jiGjUiEnOao8jkAuMTp48o0PXETEuAodRiUNOA0ebAqgB6ZGACuA4wSJ141HU5wCCfQ2S6Kb1tf7chfPihM1EVqHhFHNQBKTJmIiT7v9h8Mm1LTDixLPEjQRG4EUehHYJzSZf3DYUQlvHw3iVRFExZmKMM4dZNf1RfzAGHuYkxq8Wg5IS5REiWTf9aijvE2iQfpA9tMfb90AAckBhgCUpYsnl3pn+IgWfX+PLjj/D1dotuECGrUwHokTYY7zuVTHIVPbFNPANx3HpcQIa3mR4DYMiCl8UN1LhcCshyDWwg1ZI0DGwMRrh4f+yjoVE3XX85Hfww0v5CTYPUX1ZJduzT6DrBUfgDgciB0EH+7fCs+xDbQw85RmRgYDynLZ7tPkQPjw3tQMPe7HqVj3nZ6wwHdNovX/+iABeioDANMARQQnkoDx7+i/tci6E8jeGgaj9wPLQZesrw8bzLpQ9jExYBDMm/FTANMGiTagBDlMM3wNUzzsSBnPTN15ustxgWHEMOOGC8VDMUuJD2DH2oewd0W+lz+2rzjb2Wl4CGJC+GGeUhDjqvN1H2iTMnnkYhzzRDRTg7MJpyfN1cK0r7EZu+fMaJ9qf6YMiQnZJmcK6Jiqa6ltob2qEEARvh4qvngcPEHVKzUtiYBCCF60QQGXAYJOSlI/AHH+DVn/lN/KTrcNd7bB4AYAAA1zF+8KflxX77Bzl48F/7N/rWY0361//nB/R74N/7X25wuAe++YLw8Xf9IoDk9mvCV78gfOt7jJsPT29vDWQoN/9TWgUt0GIEHhqK8tkZZJkzgAz5YqTCFoGjnR6B43Vn0rhsNal35hx5BnbE8MZDedx+sRpz1Dc8uhb4Sk/qlY4cNm6HjjZwYaebmRqBhVEGYKFDB9dttDXo0AmzB8PDo7u7SzteSxZtNJoNbLQOkvqeNo6NRoNPC4PVbFA/DXbzF7rEOlGsEmfVCs4hg5NFo/3gff4e9bkMYKjMtRqDy1B7TsxoCcAQI1oouJDlw8klhWpxFHXaXRGePReAoT0OKUxODo6IlHEqQ8t5jN3P23YnJk2AF/VKwAUU3QvQYEwL2TADhfTJxn5GegXRtAmQ/PSYKk2F/AVVKA95md1oPiMetcuMygxmiCiEMEIar9nzBISwsOjYXs2SGBQTqqYqSH0HXO+wvb7CdQcQPAZPYX4eD0Y+Faohz5bKNbZmchrT1nCgFTSVd40pvm2bc51aesLGUGfFb7pmF3ps4tE6lVPdMSPiM6W511TaMt8k70gdSjBCN2HTPhjkVFhZCluek9es2Uapj9SXQOicQ0dbdLQJV8QZsONwnzbYuh1oYFB/Dwz32vg0sZhBh3tsQQB6QGMOBtAiprfP+QHYv05aDMpHWgBD3IAxMAyJ9w0MfnXA8Mtb4yjbLLB2IxxkLfFTpTLKjBZDub9tAQyFiajk1QYYpJyyzPR9swF2N1MAQ/5MsQeP71mbknNnm2GCs8p2eoipg3MiV7qOMQyMbsPoNrJpjJ1k8zdAg90oZe18PYC2HdhBDhc8AxVArM7gQqdoR1v99axfeHx9TngYbUAA2gYNBgaSs+4ppjxeK9rFFTJcnCfFPQARWwjmqKL1CWBIQEIMLamgQrguYA6BOxfeCwHkgN0Ww5/8U/js6gr+cI9Noe17TtpeA//a//jwIHl++QvCP/pPHP6L//w0ynP/mvAHf8/hJ/+A8Hf/Xx3+wl8d8MPfYvzoz3nsbs4sWJ5DxpgCJs4owxwFMlBgUrVu0zGnYAKgCwwZjQVnUpff7JX5FyN5WztYWYI62mLrruExYD/cwXOfHMxC7PeuBwKGPbjbAN1OJkbQdYtACW2w655hS1eItgGWmMNzutIxgA2YSLT/vRcRwQWXRV//cc4Us4UgIOLWt4JllENAia1PBkDMIXSRUi0Gz3JsP5gyjNPIGLPZC0xSBWCAuFH3AyWQwZvr9veQOyKSzzHAUIskEbvAai2WDh4NwGBDU8pnaGbhj0FPpi0gUZMHtzvC9Y2DW7hDyCJDWKGnAjSoANcCuSzQIH0pYIPbcHSMGdMHT1tkbRkNuhwb67WvKQENlN9LbeFRf0inji/WnCMRIUQu4XwiV4XUBVRDnuw9VdUbUlo2mhzxVIBIVPeiR9nweX/As6++wcdXz/DVBrgf1BFosvHU6ltR5zH2ji28z8qzJWXjamUlT2UmNRDt2LzXAMxLwKC1gPXSdzylMMn8dG0pL3Q+mossUaO6eUQCBOzsLjUcMu0H2DUqf8ae6uZ5qXkEoLKVhEQGiA1Dtpsxnds1cAOiieCoE7MFOJGbFBgAwbHDzl1jQ7vEkGO4qQGCSIeNwejQBYhH2kOfCyQtSjYFwO1dAhRKgCGpMybBYAhOHo2sxL3H8NnrEBIZ6Z7la6wmEtbZozHjDH+pedr32c/MXPVUgKE8hLHAQLcBdtd5qMw2BTmfxxv5mKIQ0zNWpIiAHUFR0BMTavbCt9lL3/meMWwYmx3DOR7JigqM6PBgYoAp68/hmz3oxTaMRdQrr/JQKe+pfKRhsVHeNmNA09caX6bTMjW9c3F4s+dRP8ZsTBWiLzBb5al3yOP6ZVHHbHkhIor4hJAXzorkQGVMTn0m3Z5kLWCsxuQ9+mEP4Aob4um6PmH63vcZ3/uX5s0kvvoS+L/+b9KW+u/8jQ5/528A/8y/1OOv/Dd6bLfNR99pWg0yREZGBOLcsRAowQcgitcoprAra5HvEZVvsfkNbXHTfYDdwPA84OCusMcBBz6ACLhyz7G5fYmr/SGp9uw2wGYrv+0KDgD9PYD7SuG1VTd4v6cQwk9iKwJuI2vM/at89deFwHvEiBE64ZmNFoMCEDDXEygRwQcbyjJ4Oo55aZqi+HEbzGKuDNBbZD4wMmWEQ52pyQrafnM5up0/OwUwZOlsWyoAQ2LcUm5izHLdOWCzFYBho04eszbkfdKihPiPgQYpeQw06DOy99fEEh2lB+C8IPyuS6qVhfIP/P0ga3t0GKQvr/VyU6dlDLMxGEYARC2NDZEZuiDN+woZ5tWs36i++iwbx49BuCABaWCAhlgH7Rtb3qHH5pef4eNhwM3H38KnN1fYQ0PgclYk2+z0e6PqSza+czQFMpwh+wtN0BzIoOvhnLhxARnebSoBgWnK4cGWeURuGmGuk270GGNQAkamEheJUbOhSJ9VpVjVaBZYF77kkEAFDAcQMzp2YiKK4JTXD4kBMwOHOwB3qDPVVh+SNr6s+MQzQHb6sT8ImFADGNKJQ5CJ7LUkY/FdL9EKdGNmX7v9HCCb455GESB4tCNMPJIImfYC0AYYykObJsAQHEPH5ilIRYRuIw7xlgEMeZ0tUAHk8leWmsZp0vu092RMMQFEwWG7rq9eDka6jco+XJV9rIyarjH8HYMPXjQEdEPsbb+n+oybW+O09nAP+eFLU25CFaQYUdA85pba4JDqXcfXaOTEe+S0u5TfWiqGKvva8RHMdJk5mJxSKlNNZMMhTjamwlzbvb7D1Yc3GLpGme8QffwJ8C/+63sAwI//Toff+7uiivw3//0N/sq/3GN7/e73QY3WRZeouEYTRDt59Y2AAqnzQ72a6z9MbEEymvOsnF0jhw3tsMNGbPsOd+gY6LoNtt0V/PYF4Dp0hx509xro96EQWfAw2FiFNaaYarVoR2F3xLp49X1iZqP0wclKVOdDrtEQFzqO2gvWJ0MGPFgtBl3IosmEKbNiuWGqE30vRHDBML0EPqjJRM5YqvmqjwsLYhQMqzwJaAEM0a9CYKY1gCGPOoGMIXcdcHXtsN1RABimNr6YRY2ZFWMqgAa9XwANtv1EgDj7kYJEzmEobB2BBkPeA8M/eQn+5Bruwx2w6cQDM3i8EypsSqO6X8GEFsWAro1dK6iy/BNTuaJvw/1VR81zcmZg0klroaGbxxzUaQ/YfvYFtnf32Ly4wS9+5dvYU7Jem1L1n5LJVcC70LtHBOSnSA26mEu8D1TbjMi1MqoDailJeQQXOVR3PjkgDx5pf4vledpQxhPIYgeSbXltWINKHYjUxCE4sO4PoGEAMctp5xDgcFYzB1WPDB/VXVFFdmqt1WspCgAD0A+FeQSCfyp7GoL42wLuzAwcBviXe/B+MHJXUV7wi8Wewb3BVxRosICAbW7By6YAhhRhIslDUwADgEzTU8vrgnZAt9HK2I0o8rZRyZoNkGVkl6lXAGSBB0ZtjTsFjTzHAhAMPvidCmxaTCcQnUNWh0oxFXkA+NUBvE0xQeMGmQF2XJf1LIIc1UbCMLYawcBoPPASQMGSF0PuqEVEpmOz8VGR5fJKp/TquHK8OCwjlcuC+Wgmt1HI37l0vUt1JR1DJPOHgvDtXn6Jq+/cYLjaTspU7wJtP2T8paDx8Jt/YcCnf5QG64vnPgbaeN/oBHMJl07r41WZyGp7l24pfBn/kfSF+mCdKvcpRW5Q04cNNqD+HjR4OL8XFTvdVfYDuuGATneBB+sXwexMhwOMRz3EeMmjKjWux3tA3JGXbSkfC5ORBy9qeQoWAMm5o+5crCmEMX2ITHAIQkv0xVBoMQDxtD8uno0u1qrxEEJURlOI4IdBtRcGRLR+BDBUZDAyf1n4JWh9LMBQ98EgXSYPCWMvHTym30p+SNeIgmn+jcPuSsbhkv1uBAPMb6C45guggVO7VTBQD8ol2CAVtRyTMISVWxlsik0tfXL/WY/N7St016+BnRMnQjtxuqPeguGoaqKS6lwZlzWa4hI197baMR5J6O7MWsCcp2uRvW9PM5TRBWQ9OXfUAkgYtKoFq3ZH1BjywOvXuNnf4wff/gF+tuszGaJFc5vId52ZXuhC7zPVfBskDYWURlY9Nr/LjJYetRTEaGgfxCMeAzLUak/mu73l4nEQkQtAwgE0HID+FuQPwnCBxIgJZsErmG7zMKYiGJyDVKBhLxqpUWZqaC8AcbMXD3ZC/fjgwXeFFkO1TIB7hh8ok6nqYm0cHCOyAAPUxNSE/5bHyTQxjTcLMJROHp0jdBvG7tq3Q4WPZFLkBySRxmBDvFNt0zhNmU7rrWADOQSTVgryDqHbejENddCD9WliwL88yLsLURto60Q+chIBizsv/ew5l0lQyEOqHWwBhtHBTAUFmAUH9NlhLFA4qq8XAKrOI5UyYGnFjlYBhKwO0PCAksRpjFOtCELkMgBdqJfu+wjiA8x74OUrbHtge9OBeViMd5yDVMQ8B7UUTVr0qz9k/OoPj/XU+W7RCk0GF3H6iF4ZiZ8iU3MGvc/tBxsg/YhajNnRBjFiA7OYMQw9iA/yaVX1dKW0G31lkBaAkBtyj5asCrVVWe8l9DPtlO09Bm5f53mZNDFahPpasFoKHJw7mtPnEcAQn/cJaQ3X2OYbi6VsQxQ1oZyWb/wwoLLZLxhdyGWii2xbi0+DwivYsARgUIZnAQbPeZ28aTcFMPb6xuHqmsbC4dRKonlGZkmGmZtkBgjQzO1zAkTUGTazar+pAEsYiIE+AQ0Wq/I9MLz2oh7YAa4LjKGjEdocgQdHokq4caANJROLKVq7WuvYK0AEHji346txgtYuPozPeFKX6WYim84a7kl6ML0HEAXPxy4JyA7Y/dEf4upPfR9DZUEqq3gBGS50ofebcqDBXC80to7aPlMu++hhSjrUKd3ryb9cO8SoZV/onaujRgHGgwbAcC/OGA93wOEeIXA8EtJtK1D+npCR4v2QZGQTH/Ib69zPU3l6kgkNZqNoAIYsncH3+a4XkGGyGXKYk+STZFKKShfYblc5IOIeUWxNmgtZdK7CfNQ2TdWrUnOSTNJtGLsbXzUtmHxNE3vZKPO7lLiWdgQoTOSpuwth0aTRzCWfnoITR2kPZ2XXagb4A4NfHuDcIfhrCjLRNgAOLnx2KPw22Vw0s6LTzHAfAQwtcKF2kNMFMMlqmQZHk8f4fMmAj3JOUmEyasuLaZADC/pp5awALHD0DWbr7eVLJ+Yu5AD0PZ7/4U/x/Nd+Hf57fwJfHP4YfsqvykmU56sy97moXDcvtIwWgwwuqhLnATEsu1P0bKSit0YtOubrYrkEQucZ2L+KuvsUnQeZz9rgtSupD67vrYMgIJweH/JNy1L1p2qZ5WA3CPvLb4JTmWmUK4ZE0jYEgCFqNljQQJHWADBELYZoMjFVUgsxDV09GHW92G25mcQigIHyvPOyWgBDykvR+SgfBIChdPhoAQZRZQzpwycRsNs5bLbjRbfGBGtDl7QNgSERcZZ/K409IYhDRME5Y0bhvUQvt0CDJ2mDDakqeQWhpmc4AINnuIFVM3AkxHEAHSgwE7ragJ5tgZ16FTJttoLeEp5XY6QVjVx1MNQky3SV8SF2pchUHSJDpwAYsCbiIKB0pg1R1U8TGkmuB/DqFhvaQn5UaMUSdrHJv9CF3k6aPQEslqbpzFaWnclSwUTBkxymlGF7TD0JBLhNjnQHU4gYlq66gAcG6z3ge5DvRZuz3wOHO/AQfqusEhjX4maxn2cb59hvZIKCYfhRbtLPRmFE+WYrnHDHSF2VMIYpxCVHf+BJ7MtNRaua+dnBSWJeKmd560TbmqIaGWUcUSI1UXxNAbsbH30YZnLa0vO0BtVkmPx+HXho5eWD3OecSjziFDIYFcRdx9BL2/S8IDQnlgnk4vcQ5ERQiKZxP0j0sK0TWaiTgxc2IEOUC7ckAAQohQJfSyo7ZQBAqHwtPOaAcdSwFk0tVRWER8uN7XOVtEnkjHNTdiGpU6NDTxfAhiGsQaNYidJn7utXwOsfg378e/j28yt8/tu/eRyIAqA018+p2Js+ohw28n/zDpHwENu+4P9ncgAmWgwybNxuQSqKYSlzrL2OomVpVPAnCQQJ9sJc778E+oMwQBv8Vx0qjmDdFuX32feIiDmz5FdVqzATvrkyz5RtYekhnBQY1Dx0hix2wVlj0kxI2gusWgyZlgJM3pzWAst3y+r4/K9aXY0mARTaBIYRlg6JWmTQ+7ycBC4ok2aD5CuqL/IBx7pZkwirzTACGIph4Zyc9O+uaWwv2KAa6BC1HjRN1OZpIPoxDRfgg9k4c55HBjQoQOEBJhImZOvnIWFY+5CXua9Ok7TupEi0Fnw3gF73YmbxfCNjULUgMsThSFJUPnRMXAcm4k9HskwwymEEOAYFr9QgArMP1QzaCTa0DRDMRRxiOA4gDBYvgkUQQr/zcsCnH+3A7I9mgu82Le2Td5PZXujdJyfoZfw9Ah0ecGhnLrKZgfs70c4c7VL0n2JzAFcBGkyq7CQurH/DAB4CqPD6a2DYG7kKYwHBuXUr41pb9WPJMnpGWte1snoaXTor0E1XYHfRwaYzjL4RxjA50aYYxjuKqAtEUd1Yax7pmfRec/9UlDWzbLY2iVnDVPoK+x4P4NrzWs9mE+zwU3nIivPBkWPj6Wa+XkVy0uhd4p+KiKOGp4oOLpidxmHtkkxKRhlG7nP+fT+IONAFU1KNPaxAQ+eAvYARuN4kHyhrh3MxddO1Sh8wi1NrBUZqZAExc05JozSN55nBukYMKS1lRQZZ25yWRHnLSVQQci71RUXeitqx2s5DL3369Sts3A7eVH4c/rfcHFl6WxwbNAxezoF8xPX/iMcaJ8AWPhjTaX2+GGToaD4pxWUgwWG5A8gpEoaH/g64fwUcXou9nO8BBHX/uFlpbOqTQXz6XaPyeHsp1Zhui2o602oP6Jw4JjJEROAOoF0XVJEY2BvtBWsaoY6MoBMU9cWPjf+G6KMhlhidNZaTmpkknJDRWNB06pSoGkdnYtwT5fkDqSuth2WL7CdGLM+KEosBG8JkY05RqSzAYIeJcylM5WYzPxpbmJG9HuW5sBdnKxyw7o05rglElK3jGkLM7uXZvAsFGiQMZQAZXAIqkk8TkukT8qRo51gs3c4y2tSv2Pdwrsfwy3t0O0L3/Wegm81Ijj2ajCQ0tzRmYGQ8KeEIKAAMeAr9EBifZxESRhoSlIQHIH0qOEfhMzBM/oe/g+f/7D+HW7qdWJDfT+IYAHieliLcF7rQU6POHeWm6jRinVkU/B/cY3K3GpmQL34z0OfpyarYdSaGmg/gwu1XIm+pU0RL5e8SmV+K1K8hyxBnD47KZ036ZzfA7b0RIlSGKnbUZDZEAMjIDjWZyvoQ06paRdW0RtbXymTJQbHKJRExXAcMh5qJBGWve3wIwtjsgOtncpDVFFcjqKJ5yYW0f5kGIxDTabmVDUrz1ZWOTPOHmFVbwYItsYrxAR+ELHUUadvG6ZWKTKRmsbYkJ4cu4u/ByBgOYBrk4OU6nI4/FFaWIUYYzV8AxR6PM3BB7tOoD+29ERY5DLm6pSeJboGAxZSvnoy8pTJWdlKmcpYDtsH0pOtSPsMg9/YHYNPhQ/oIL/F1HC/jaD1TIMPbTaQHYSfQGlksL/s8adbSck0G2mH84vOBQezTRpyBOBuyHV8xmJSZ9Pfg118Br18G84fw7BIgYEo3La2aRfW5WHFmGNpahpc9F76XNoC2zkQiEGzMojAweBgSSMBFfpqXbhqJgK3EueWBxZGkajiYk32gwXzC61DNQqvpwJybMCR+3YgikeVpGHNR/hTAEPETrQ+SpkDSaEh4jgIMds1SHwzPnosPhtn6Fl07RXZoloxONcqshoJkmgMKBAA24kT4l6EmD2Lf5img8x4YmCKKDwSmGMZ5VWMoMimK/jby6yGyl5ep142cONJ8ZyylqXwCaDJqA5svsS5Ux0AIwSeF2AZmDtKswG1NQZwwTjr0uP5b/yEO//Sfw7DbmKILwb0lQD4RxqjmWedgaCnPPKOWTXoc1Y+pq3ihC52JHD8WQGbWCj8Ek4hB9btRrjnpMc7vjxiqkbNKpN2HyFY8gPevgftbiXZlTQtinbjIDxjvbgwpcn3KtFfZ8RTSNvRD2gxZk4nIaCnnA0CbN40OdIKMMXCKWGVlppkQNOlwJ1EM04gkgw19OrxhLl5FbS/qGJsrj+1VSJKdpeUHIGOAIaSp5Jt1S62LlCU32j1iBaXGZyETSZ0lgc5GbQNFOSdwGqdgg/h1EF8A6ZAltrxSNXXQ7WyIzNBe1zE2V5T7jzqGSo2aGjUOCCPN+Q8stUJthzs73lM1KEsbZCCioNxNQXuH43MZq9d5E+QsCgBDta/09C80gjwD/++/ju4v/WX4sMfjGJri3ScyERrXHMZYIIaotEt52rTc8eP+Nt+V2FXKrlqZw0POkgfdaZMpxZ0s334NfPVL4NCfR0C1WcTqTAzksHjNbqaOZYS+7K9KfXQDRQTabRDDS2oYpSwtZGO03WSnt7rA8uDFS+9eDQbNazJOFsvVlxmhXMuUdMcMqMpeDnQ02lzj4QaJV4AhdYX5rc/bYqwcFTQ8ShOJ8hVutoTra8qcPC45JLEMp0UZCF1JR+ZL6k9dKKCvJW1aM7CB4D0HTU0SXlX43rI2iXqoNDl1HOCJEHFvYz1AJA4Zd98i0LPznuZFDY7ZuWUY4ugecmbngbjWWqa3UaZnAAZ92QOn5+VB811cV1Pf48Xv/h5e/9afQb/dohzctW11fv8NMEsGrK3iQzChWp6nrtLnBEEudKGzkG/4ZDkrFRv//oAM0Z97Nn4t5KyR3GVkMnWK7Xtwfy9+F3JD/VAfrZe5ZzfXtux4P1yzGxKlc8zv8mBlnKCSnoHdFtj3+bXm46aNOeKfk4nqlZuQUnqgVUylClaV31rzkQf6gn3lJhV5Xq5jbK9EAyLzdR5kvCZQsEDknWVpC0Tm/IKRV0lSsElstUJVBlL/DHoIoyyfBzmUIJL9LKlsQADp5ryGeasJjIc4e1ZWREEWcoC7Fj8nx/iUW0xq2lBO/VPECFvdeOidSy7abwoujAQbj+Q0nJBkKo1YFjSAaBsABqtAzghm4fq2bFs96NDh+TcDbj/YxgORsTbDu0s0cl6x5Jm3V1BavpvYv0ZybW9AhYxa19FY5FlMIvp74MvPgJevEpSb0bRYX6Ulu8Ss2rW2lBCeYbqr5gOnTfrYMcEotajHM7CpOGwiAjZhsm8oOr3Lqg0A3oGuPfDqAPDYV0ANTddq+qhdoMxTea+J/9yufoUoHzIl4k2KPqc4yMZxcahvKi8i9BMAAxHQbQhXV/KnG8rpEFNFtSogyVotCDJDSPCjFLbVAg2AXUhUbVEYqwINmm+sF6WIE35AFn212aaA3hMB5BjOMdyGsflgg81zgvvkOjHVM65rs0AD0XJmnqHoiA4dyZpGWPIcmVlE1p3a4hrBLZx8uft73Pz8j3H/K7+KfrfEF42h0L5HZZfEeHtsFS0ZwfxCF3oKNOwfNv+RKt6Q1PmnQNZWPqNjbvOpGqHDIGCG9+DhEL9XDz7K76GIVOeSIRfXy99zoXhG7Qr/LOUFLVlMQRKipMUwRaXmRm1pUhkumKvKgUwu2zRxDAsUGHLWhBEYgTQWyBgfNHHwM8XYbOU7yAwne2hTE8ntIdNKuWaKRq+u9QxpfnYLZUxIjQammEub9gAiN+rBUXnIksluBOubakQqU7nwPjoCOf94LJUI6nfrLFSuIzFfbSiSrGW3NwjfVSPBHNaQARai7FRYpieNZU7aF46SuO+cHCz0PfD3/ha2f+mvYsABSWLKO+BRZagnSrWp9NAS07n7fQXIcItcbbqyolZVzxpV9oKus++Bb14CX3whg09Xh2zgT3Tr0l3fWmJ9nWw2InaFW1GuMl5AGL4FQIr6s2X6vQcOha6UhuHZuMyWMOUnX8kR6KoTR371oibqWlwqn7MMc2IYZDiPkX00E3K6YWaAxpXjiMITDvvc3MObQq0PBiCYhm0J2yuHrgN8qCep/LBW7jFVa1kO1OTDJUCDtDCkN1dIr3Cu0UA+McyooRo/A8Ot9KU9IVFku9sA3Qcdth8QNp9cjdXdVgF1Z2SSNVLwsSMJu6lAgzI+wMwz6Wj2yFUVtY8IAJyo6ZERcFQdhAH35dfY3bwAvvsD+DUGmdnLfCoGFMD6l3PZ/F/oPaThgDpqf+p8KBhlyRQjA2uU02bC5rvV2bfgQi+yVjjUGTk/XCtDqW8prVcM8cS5JkRZ77VyU6rksu4v+9ez9EFNQCnbHeofnW2PNDyj4FHHc/T1VUwGUjH5vVJOksT6KtOBTu1Mj4ix2TC6rWgvqImFjQCmB0Xt/lr/6hUUmMx0cWb5Pli+B1k7RuNS+SdpY9j+UtkHg3keyJxjZ5vqdCnKQTEPMNhJBK907QGoAJKqpKY+x0yXiWUkJoliUPii4IH9XWwvrKYSg0EKfAUVkqyq+rynVEaINkPDANrvsf3Dfwz3/Ab87e/BYxyJZhSG8z0jqq17q9hRjY/NE69dFGZoBcjwanzN1qU5Kzn/rsK374Hb18DtLfDqFrg/mF3Z4lqto6y+J+azmjErp2g/m/wJsNj73R7AvVlodBFwoa+VaVNlQWJRf6eN8QY9w1SyAxHoK+XsuwcFLSgavdoEyuR5Isg/IdeYxnWMrkO2MbN1IPU1QKbbWACG0QFM+HSdAAwSolJlHY2/HNKsPV1ZQGV9rBxTuwaEWNDWRMIs6hHj8nLDgxH2xAa9T9zEqquRIuMo8g3lEYV++tDh6vtbuJtNiHhW9MtaRGaGearmQLmILddgQELXtSP1pIqQ/KR5pPZrVwRGxxoTOzwDdbKq+atm0NDDffMNNt8aMOx2iKdbS6d9FCDFdwFHAfu8C3iNJLwQZb/XlbvMVe+FLvTOUb9fuA4eMz9qDLjyu8Wks9N4yyyHtMOUuNPmmoT35pK5x2YcATKUKni2KXGTjlyDQNe/ipwidZjaUBhhe4pX+GKdU5DBa/uRNpwRNPAJ+4jXkPLxnJ6N2g55V5YgQP3AIY8MMaXQmuQfhkZYyHqD5IDAbZPmZ3y9HhGgQPCjNUU1k9mTaNUJTnLyqMMpPp5el3R9gYuFb2PAQR+OBy4wvNikVQ0Sp1EsEDfVUV46Vk5cIydM5tOYZ3NU1tuABnCBs+uBkpF5yB5aGjkzErOYQGio0IHFpkf72D4TDoDYheRdaIv3wEDAfg/6vd/B5sUL8Befg7/7A/hvfQI1+xT5923UznxgWjUkFdxZu76vSz5Hi0EGvquADEA2K1ubhYgOD14W/fu9AAz7A3A4BK9zSKs1UGdGtbLXkM3TPro2n1rkiPnCzXOcMbUMXAjMjA9efDH4tADGRcLCv5FD6vMcv4IZtHNidjH44hlTrwwVMIsMOEbF0iKHA9AfysWn0eIRg0sAgzDQdKmofp636TYLMGSMxwGbTsAFqa8J/VRUU5wDHQc2MI9NT7IWhoW2bEtNKSDzoExSw/x+EBaIAU6ghLYXxuQlBxmMGmHYSDM4RJug4NQIuPrEtQGGtbRCWD3JxlFBI83Ch9Czallk+CRI7AfjCHcEDGKAkjFahASu05cCeAbdfoPu1UvQ1fcRPfoqg13UTofuZ78HAOi//6Pw3NoN/xoKrhgVoYrTbW15K0xXLnShd4mGhT4ZZP48YfMAAGPxSURBVEE5ooAT5r5laGxABQ3nrX4XshjVD7XWTJDlBYr057aNBnRAIfdhus5Ty1LJ68uDnaCRyioLqexRgAgc0iofj/fKpVt5PFP4q1SpaPaIR8H4+kQqzw/ih8ltJD2rcz9C0ADlkM60A2OAIXOkPdNnp1DcmNdkuFFibb884H0NaJAxxHrBiqgAEA5L6tsCIxfZvnapD+Gp0HYQ09GkWbu46WXhqSHnoiWgRau+CjB0lIATQtYxUbvTDkqPFFGCA9DmKM0Bux8BIlDBwWRHDnYYUWPUufzggln2JN98A3r9GnS4A66uwDcfxHo/JT3Qt5resCi3XJNhGKbvl2tsXLxZmMxhAO7vA7DQyx9QYT6critlXo+LneuaDmyN2Yk82APWP5nlh6tXkpKB6lej0s1h8vlXRotBF4CodlRfyRlInmaZk1+HQg2+zgRSqMp44o4QiUYBASDGMa49rwDCqF7xlXEOMIRLo1OB+IytU3vxdo7QOWB3lSqePCtzrLf2pXMAeVkQzw2WSltSJWugg2rlj9D5Qm/SuPKLzE/iR6f2UYgykUVvDaprVg1QX4z2vdsB7qqrlIv0Dq0+55sQVktiAH04ebL1JiQnjwQJx6RhqQaIXeDACVHn1B8gkkG+3STzC88AZL1yL78Effgd+O2uLkRCtQYSS1Rm6v7oHwLffAW4DTramBR8FAOdfUann32dRwtLwKnaDK36XrQkLvRkyR8QPetOETnMu31fSHNMyAIHUTNAzQD0Hqd0J9fnxPW+vfvLGX3UePAGFMDEjnghGY2DnBoyk9ZjKJ6zclOQ3UaiF1vXFu2dXpIbA6+Ir43AA6XfMKDAoM+liAlapqSvl+ejY+8kdzRExnwTjna6FtU0NksaRTotwYQSHYr1oVipiFnFsOEU2XguvuSVUGxJvqvmQ7uBGcAQXqzUddzXvCTy3TloyfvIxqQKmkY+UnnJ9q8FAhlp/AegQPz0m4ytzBUO8jAw2Eli6dpwsMPhoMtzilThTGUU2NB14P5WzCiMg+kLyPBu0HKQIcKNgZYAA4pgHw5iDtH3SYVuipGU+RQrGJfP1jQLyrAu8Vm/bNKWz61/ZExzoWxCGj548L3VYqD0wUiodi1/hP7xkD7rHOiqA+4GWTSq2gVj0k2t+k0oMYQ8NnT+nKXSTCJTR0PDdNPniL5GUiAXDqzDsIqaWQ7oOoJ1jmQ1DjQqA7Og5sMAdI7gSe0Zz7/p0fqWoINzNAIXakzaOcr6lgvtBGWWAi7k4ISNGgEoc2WIg00WM5Udpg/intpJtmWCSsZJbxQ4nAotpaBbaY847xCAQW/3fbo3eODTX4CefYTuuz8EaJNBP7N0dxsYL6OjTfTtwOxXb7MFzJihN/DKkveQ8Zq0pjpzYUFr5dhrF+DiQmch7xGYZ3vXROqa/kSKi79FiANFAEE34wZAsJ+jY3JzX36AS+YyV6d8Z3Zc26byLfKMAIPKdSq/oCKvtWQfzSNmWJaPuvp7yC/WochLDn3q+TKSv6cacfH67KGJ95RcaDCJzOPz11jLOx0u1Da9tm5UDA8u6iKKe0pWq3NOW9OSHqKUZLcKtcjRCWjgXNSoLuPmoZLs0B4BDiEJV66X5yo2sgeCUtCdj5rDseQuOFtvkRm7Z6MCh1mUXj8XsMURWGJ/Opey0AMaAuBJNB10Pg8Ad4ELDxzSIoAL4cVsHLDZGF9ylOb77R1oH/zhxPdwMZd4F2g5yGCRZ/09xBUzAQr6W00jDn36vRT5s5OqhFlLsoxhCCeEyrB8kU73+EeZO5yBphikNnPw4N4wVkX7KKHak/kGRh2ZIzNoK56B1EPvEgZSOhe0dVyyncg1PgxCoermNpMoX1AEQhgADxSHVmIUkpeTpHG9YojWgpWNMuCC2Zw4AIeeo0ZD9wAaDc4BbA66VCjwxdgrTxLMnTwNEnIPqI8JMjJquqb3U04JeFC1RL9n4OCB62axQkuljROE0aajmdbRCxKKXmrpzBJBXvhuA2w7eS86uAazYGhop/s98PlPgd0WdPOhSGbk0mdWL1OXw11aEzcADq/hts9CWrcapX+TqH65gT8VUFhX9nw5uYx6ARsudAItCWF5NodkhsFSkWfUSuC8vCyOM6fvVRCimKl2PS0dNGbllnnPNWMMHISHx9czxmSeq2ykuNQwCN851tMkXnKAo6TmgYXcpGYSYBan2xXQIWtCdK5Yb56IzAFYMA4chyGBCtJOBQRseG/NReWGog0+WfblXcyZH6r458eyh81zvG9udWbgv9l5Yw4UEFGm8VoT4WsYVmZu0VrGbT+UQEFlGGZDrMDPWuQZIE/wX93D3XfgrYuhG+mqk4OGxz6EWQs0wKRnyMFk3GPYmzCHMwU5JC0jRxISVLURwDnQoGQPowkin2070G4TL8V0IV94yOHOYQ8a+iQUv7GDrmM6+30iI+sueEXrNBmUGAIgKHCgAES8ZgAJ64m4nPFx14981bH3aqtE2d5iw5rXO5XBusgOOSPNNjmMhGLP98r5iSH1s8h+uMFT2pmWYXo9MU15OZfW59rBxkhGKAEGlLJVCVZUmLBmZPODUQ006L2i+n5IgIHmKb6sOA0pU/+BZc/Y1O5A/ozVLDjshVkOLmeSXbHgSn3kexkxtCQKC7a1LnKV+mm6mmzmnAImNVge8URGnUZG5hlui8MCmzEFO33Tnp7rfWbGW5qTEw2uPjuRpNbgNQBFBNwa/VMjCg5TNwRcbURgIIA8I84K1fxR5hZCWWEIQMPdN4BqIHijqmzV/7oNsL0RQ9r7l8FvhAw6ev21ODQhB3YdyG2SpGaPUfKKV769eXpKdZmmi6BwoZWUbejnxk8UXhrf9fcMjbQYxmBBGz2PXN2sSXKdbXoLGjRB3fK3kT+WrtHCvObT6YFBWMfjBj+uqZzyKhVHrMaDymsKEgBxLY81Lg+VVFW7dZ8BPgzV7ojfVfRVAMHnXWxNFhS38moWEV6T9Z8Qz/CMg8hSXimpZsFsX683/N2bIaX56SEzAIxbGw4xChydotO/APQHGdGyTo7vdFw3q7kZ5XvMa06oxgSb3+kdFvONAOuzwWpPjJ+pFSwHct4DGLxEsxo8SDUeH3PzewwLK+dsOb4rUcTYpIlRxjylwxYvfrCIzRzVT9uvLphndxQ0F0IUvAA+sPeioSCClMiqzkkd71+B+3uQVbGJnk3NOtdaU4nkoHKNbBiek6y5QM/8OM3cer4EgLaD9ylG0Fhz4rog6XKQ4XWIH+2D1+JenQyhzsj0M5v/BUhgmUqsdGAsuoBZvXh9DJDTNC7ydIJAxhIcL1dd0nSlyYFtD/I0SmcXZVsbZkZzUGYbxsh4JS/2LGEv90MRb5jyZwKROm8xc0/vlypv2UbIDrhGKCcepOxeomoF589sfFYlhpMOljk9X+maBJ5OLwClCQWzbLa9AUBcN0b8y7IAi/wXY5MAV+WY43TU5KwUyuCUlsx1rYueKqgpBUE0GrwFMHIAxUfNDUrMpAbv67QswcAalfN8dLsibZxCS44jlIiEyW0IdBWEhFBf9h5gF9e0qAbZmc2/olNDDxxeQ+ZOY1E5ALi/lSr292FwM9Ax8Ooz4PAc6O8FrdcwcyDRinj+CXDzUQIcqKsdL13oVJraZJ2TLq/u7aTR2OCJe5U0463pgjLNemJ3g0uoMI0YHaXXAIZyE7KonEK2q5FG7YlEab2Nl8zOjznKexqFJ25eHIF8AAqinGh6M9bHFGdkOJHfTFujXIeR/DaqN7Nok+ozIZ9so67aCFHOCaCCOlyMhyiUnlF5KXtNKVSl1q8EGKaXrHSj9PPpjfyauTUzaYbKMIvyBqeDLSLpJ82fAoAeRe9O2pBYs6lX9gxQhtVc4oBbcysPvpyJsCX3dRSpX6r2eI0Yf6P4aJoCxIMJWM3Hc9HUnMpk7blsJvYNmp3V4LGCvGr3kOwlSA9aLNDADGSOyitVJZEtxb+VSeZZ5Cyn74UgXuWRBvtXvwC+9Qlw/SJk6HQi5cBlQ8uBwcCrr4GvfwH++AeieTpHRPlepbXunlNkWJPXFB9YBAaEvl0DHJwZ+FgBMrxOAIA9Sl7CfNbSUjR8UV60TpVOqSyekRg0FxO6xgVqPy10XCOTP4B8gdHym/UtFnWrCTHw5DgbZUvFdW3eAvm87JZUP0H7B5/ABUWLU3xnznw/xc0+zw8HPwCum3/JPqL/Rp1Qtd+IJ8uxm30/hD1gwcQIEA+7ZhUWplfUjerAiKP8BELv6ymCTI0ENoipiAhhYjpBBmioNSJ8bgjoXIi0EYQ4u3A7MwimurUm7E2lOyMxAlDT0gJwDtg60JaMbmlgmrGPw4BwJCi73dgTpcE5HJLj1TlzK/ZAfx/yFdCA+6AKCICj07ZAvge++hnw9c9FC2J7DVx/AHRbwHUgNc9wNt7rhU6i7JSiPC05kkoJ+EJvIZmNbry0QsappZnbkJTMdTQO073JGtjTAKDQPuV8LT8FbFv7XAlU1+S7psnFqPCUj2qf1YSOYGkS5SY9wdc/n9KODmgAARn2HtxzDOKh2gfeA74neE/yOagmgjF7CCADgGzzb2WqdNhTER8r1+0BjE2XX5MffsjTTLGsWreX1zJtUUKKfJU/BUABB71WOVwpZKaWzFWCD3ZPrEutAhYabYMVXtApbLMgPZBJeZU4vpjsSqj2TusZOm/2dPwUGaf16KIlx875iQdc0BhibYu+Tx2HocO0mR4JaDDKBfE+hShUjpLWQgGKRA2JDJvwZg1wIkN7lqAA+zsVrE3dwgSKZhRdhdeyqCb/wT+QA/CBgO/vkGuJFu/PuUX9+2RpjbzyBjUmVvhkgLw8IF8Bm+jZyrfXyoviP+08Y9x72UBk2gzEcn+QzRRHb6cUGZtsDAPjCXlRRxHBzuvZaGNmiwCMgIja5G91neexrDo3RuxmL3CVLL6zD34MCqfYVi6xDFDnttSHYtbeU6XenOVlUXkFCLwn+AB2+CEBCN4w4My5dEXe4rJPC6oGHmnJLjXZZGbIqlygTDK3OzRougdAHMsvI3JoWj+EvMp1vZMHZV3l7BRBr6kDyWi+UQDsCZQQ55ZuG77rb2JwP4C8i/yFQ8QXutkk7GTJNJ4Ym6ucjq2hOGgqDLCjDE2X+nkwB4cUzKnTQt9SAACZeKxhQgT4XqK90LzVfwzZm9n19GAFOmz9M/JBY+IOuP0y8nzuNsDNh6Cbj4DN9WUfe26askNbPHbp8l7eGaoxB7R/l89mVDCgkcZYZJpFuelaLkdUFtvmBqVIW4IMKL6Xct1UOxkr5kb5bP25kTYDAAoq1vNZGvlnrmyfPjONB93EB3NfHjx44BglVH0p+IGivygFHRRYiMBBwT+5+F2vW7vKCjBkshFzZlEDqAzFk8N3jjS9dTRdajwwJ0fEejgeD2CyQ2fORPiSXMOnkuD7PKpHqhugeFM+pexc0zHEcd+qzsybazWne34A3MBAl8ZXPNRodeoIH+Txe136PmZeHOcvffrZYPIAwBwg6T1k7R49R8V3DYnpKB68qOYLEOawl3qRM893zsxunwAKXfv6AXj5OXj/KqkabzapYnrY0m3EpMJ1ciijjbm/FdNWADjcA/tbOajRurtCUKxoW1/o/LTO8aP9LL9Xn1mY59LrQFxRZKBWJnC6Of4+QUSywbA8jkLou8yso5P6UQAp4iaqVY5eK2wKGSjMIgoBwDKqCRo5YTMMFDrZOYVMyp6tyDc2LJKvgAw8WKjS7pxDcZ4C8xbG60O53qc6ZEMplp3aMYXWV0GRIygyjQVqby1gw5owZOh8WFczYMFoLZDLIwWQy+vBCHyAAIR9qWotWLytVnVyDLeRCBKyJgdQQWNAOynB7xnD1wOG12J2wQcP/6qHI8bVD65AH12dtGni2nrx0JQB20l45A5Az1Ho0M4jh8DkCNy5vLmeE3jBUNUbmfNLTjR80f5KlJzpPExegweGz8FDD/rge21kvprBhS50ocU0tW5l1xbvFCZ/NoUGCzCUDLCUwexa0axv5TeQNkzHrtfHPLs03ULZTfKcKEvlC9tX1sTbyEjiWyz4KxqSHBYjQIRPr9fM53zTjmOoZRARCzok/525LHyyIrBuyDHuWvU5VbJBF2QLk0UGEOR5h3oOFTksO5CRd6c+HuqsV2UbjvVWpT/nUoS0dF/ry6PymEXGJWaAHbqtl/J94MNdlx9qzNGxbLgGTsRbOi6bCdrrjGotlP04EljNn0bUUMAg2r0gamczAzQEGYsgmgQkjiEBiGkuqeaCqUMMbRm+v3opAIGedCoQkdU1+F4gJ1qfmwA2fPN1QsNevwJ/8Qtgs5M8OgdcXYM6o90wiTZdqEkrTYWWgwzAmLmdg3Sy1JionUyV8qLDIMAgYrIsZmFnJuyyqlRDKkvoNAASyWlRTJjSZc8jv6aODWP7kC0OIy2KqtCjeXL6qUwzux+++nE2kWkVKn9SB+uoKAANRnOJA3qZxWMOWiRc7KlKWelUSvnVVQiXPc8RHGiBDTq+NM8cWAAypkrp06YDFEQVpk0hXnMsMqy9+qwMAxnLU9NZUXnnkEJTbhEBBgUlLAAi44NweE3o7xnsh2B+wSBycBvG5vM7uI+u5hlkhZllQrG9VzIxS6q5s2RsTOUDRKFRAMMwkHu5Hn1QBGaZqVaqJpQKELYM78WJiH3BZZpRPdhMFK4bvy4lHXxDD/jDCuZ4xgl3oQu9DzQluE8dNdcYfnV9aM1HTvnU8puSiex6OwIgZmS2uGHgdpq1wOhjUWhfuemKKtpan7Je1u8WTJoITHP8XnZLU+uYgcBUjsJp1j0gH6mquUZDqvPyUJRKIpfQSIS18gpHDYNcblLgwe5BCQxHuWmFnRa5XAX7YLxv87T4ump5xgMVAyK4Ln0vP6v5cTJDjTJ+J1osEv17LOtkdM6IdY2yRkDZimczYIFNJLIK2BC1D5xszqPGgSWVsRwJuMDBdWjIk6yH9DmfG7rcqZx0OJgXBRShTEw7GaDXIQS5A15+k+7d7QH+IoWOIwDbLbjbht9BcHYOuL4B7W7q63VV88xcfxflqzkQ4UFBhqU0NxFsmhKiLZ+v2RXGn6aQzJj/1AYYWrz5KcAFQtjChGsKgqgzmQiCFH1lGWPsI72W0o427CUzrDBF6804Fs3IVP+YAT9QSl8BHNgns4kU61nyLV+jfM53YIy3u5CaYIky3fJ+S8tU0xKP11sLVOm18OTY6zIZhFyZY/Jyq34fdK1X5hnRYpQgRfLs7Io10TkKGgpJO4FcrrGQ941GY5DvMOYsWgfnEJzwYJ4p6MNhbGbML7701K7ms/E3ls2xmK9KHkWePmgj2XBNkPZQsEckVRFxWodGJc17waAgw+ilt+up0XZGaWtS1QTpsxzaNPSF2t8MtTZAKcHyvC50ofeJpoCF2TScNitnrRMqslKx/uq9kexk6qX3q9XT9apYp089Hl/bF2W3etNGX0lXyk6j/Cp8x5q0tcpt1W0incWLmriVP8/qW8ORLPCwKi+MX9PotRkZd1HdbLqJvKP84xLbS7IQx3Dl8UAGIveIiT7nB9Q2v7mzgAFJnuCUnj3QHQibhXh+PfPiUuFgtCXf59lw895ceVUiSgBD7KRwzwIMtt3Wt4IBK9SRY4x4FrRDGcG/1VqTBAUarHxXhvfVdLFuYROyP5jrnIITqH34/SF/TgfY1Q682QHXV8DzD0Hdw2yLp0nH3yPJY5OT4rw8a725xJJ0raRLmFQNYKiBCxnze8AX0wIZ7HW7QVZ/D+EmwWhbkHkoghAhr7T9TPlbrYgJgCGzy4tpKH7PHVZrCMkALAw58BD9KagTI59s7LyXtiWwwSDUEwPzseZNCTBYzY1WxIiaEJD2hONnlOnxkJihzWutHCXrfAIoOid2it1G8uo6Cj4ULPNlUHByKdd4tOeOMi7su0rgksqRTMKodQ8LAuhqgSda60ir1LoxgsCog06RU7UcPchnBjsd55RiPTOMah9S3Sx4UhsOozobYbv3iJVfA8IAqOqWToEUrugzZlHTJAf4HkdJj0vpKYZUutCFHouWAAuL0+kivIYpkFmzZvJfAjCUjo5ahznljjfKM25c/6pDSR7fazbRyEtraOqRkF+mxRD4zeQmrfVqiiaWRVkTiVX1XJF2KsrVgxHDOIMOl4p9mU07BzS4kpUiOVtUM9FcttF7FOURp36rKM8H0Ou2gjNtAyB+lynJvAhghamfBjYYDqHQzqUxdQ4VeysLTeyZos+Qc1IECZBkJNVAsNsQBkA8roPKVkHOIpXB9GWrVuiSvrJIkIa6PIasczcg9G+4Fm2PC9J14e4OwB1w64CXL8G7rfiCePYC1AW7m9k13KQpJwxz/vme0XGQTQueXYqkAfV0pwIMZuI2N0A1mmN6DmlB0MllAIMsznM20CfKnKlTZgoSLzaey67ngzjNWQURTJdyMG0wcZrHjhbNX9ReoCzfmqpe3pbjZIpTqGYakt1v1NXeWxpBcM3aUWonUOBfmw2h2xK6cEitn+QImUdkytdlWwfvw3rqJR084ENILd2IJwbPmfybnSjsZk7KBxZnXGWoVdtfpy6mU4CE+hsBjHkDJxCFKJr/RaE9e96nMJU1IkovX5H1vk/3lwwMG+s0xVgN+SPv+PL7KK67zXfAeVW1KnQBGi703lJlR3nSeXNgJktCiJ3KKGsAwwhUMBvu7PQA6RmbVutV1rNGtfxOpJHTxwiKSP2SqQTqjrrnaKqqnD7jkljIDWU4xkQPv5lYM1ym5J1RhIWwmZ7zV2W3kGXSqDDokmkoufQ7L9fIJRVQoiXzgCrTylQqyoC9yEHMIks5W2kWWUhlYWG/DBenAYWk5gBjCVXklwiADXaOTWSx1MTyWBCifL2soEb4oaYQ5TMMmNBmmblvNd/JOgT5TOWxwR/XHtVm0PWCggBMFOpXWX/1WlzzPDDcAfd38tzXX4N18G03wIcfgnZXjXaYSBU1VM6sJe8bLQcZVFBurVZzMXLsMzVGVpa1qE7FsyXAsIRmVupss79WCChBgXLC1sgCGtk1RcIMEFmrFsmCmeq/psoBiFAziawbFaSgUfrZXGuAyUOTRWtR74fVr5Mwy3hrzwBJwz1qJjiKWguuk+uSnjKGOlscQxZCFtBAwmlRandwxqTMOE7jEOqySn0xAI3wFoU+O5ZrWgujek6ABqekHViiSDCiEyLtC6oADOUpzYjUwRGQnj0UxqatfrMUQYaQ1pkGlQxPy9T1tQViMCOLTfaQdPLm6n2kMEYuIM3bSw/17pbk+5BzLgMbMAYE1EeUBSZGBzpFG+LOEPnaX9WeKH4vpTkmzZXvD6gB8NhizBKa7aKiy6fSlxaBSiKz2Hs0qdBHJHKNggtEFkjIZZyyrFr9Y719ASywyqSMQU1+g8Yuwodqe+qGVl0v2PbEfEuwpONg19+uX7XCcwCDUuPgsAkwrDnIBdJciBqfUjfuSLQWIjDJ45cwMNhE/4gaCz4gOS48U/bb0vqp34YuqOoOPkeu1lJcu4DkZFLr5NM6pe2sHWRnQreRtfZ7uf4RpYGS9deF57doBcjAGIX64vhPfWCV6iu1e0tWyPh1YoKFybs6ZN7a9GtoJtuqKhQVq47tH/N9pJkT9lPMSECDvVamWVL9KZXAJ0zlCb9cS/1qlU7K4VdjdlGDAGYNogQ6uC6/JwfhAUwoQjTpPQUYxCdNgZYbYo9UyegzAVDVv6FPsbkBxFBNquUa/RB4ZKACqddlDe8kpaWP0CkMCOhQjtMYLaPCnLIGKBNrJ4m0BIRrPWf7uVUfnuBgnXQaZZIHgm+FHjjACNUV8wel0Xw2aePgKdbRDMCg5HyDkH/3PXCwIMSx3HgpPeDa+JSppvY4l1Z+hM/3sM/eFXqDANE4Is8DzL+aTDYCFsyfN2lG/CGsYxaYrZVnNzOn1Hd0ayLvsNEb9anytWKdPschyGMNnbEzag392aZMKwDp0CNXpMv9QCl1tXdLOVjQLldlm/y3yl5K2QF0SMCQ/VyGvZkl1htzXSuHAcjaweHwQaSVJAeWCoYxY1v/uQMUK7NYmSk7VEUdHIhC6KjYOi3ZJ7XGoNZT2+MHMTUt6xMEWQLk+4BCtjJ/tWoMnBxsT5HKOV1AojyLtmjNPMvWr9UHzEFWs1pZPg1w+1fPIDVQhfmy7P4gEb5UyFZEZOXB4/tGy0GGQ7/MFrlFNWYwYqpPnKZAk+YjNQa4sLw4jgNqqBvOsMOsycKtzTOQwgCtJXsy/tTJak0QAXCJ+eu9dh8hPQcBA+RCWvsUkd9sElMu167IfGkMHKSfhvnG91i+oyTUMQANDSpaYel5LS8vik35jCURVgTw4IQlesjiH9Fw04A4+GYyXSJ4nTr/Kwf/mXqj3fvVljDnUsxnTcScHDdakC+kr1ILsKyln1NTIQK2nQy03VbewdCDHSc/SHbwPChV2kVmYsW6INXnsSTuVh+wr9yfAJkudCEA/FiaQjWa0+Bcehy9lKYEdruxs0BD3MyYutqwAlOmZKUvh6k6KC06pp/OItPgsO0a1e9xhJy5Ji95rWuqmmHccQ9Hxe903T4XRaBSjinNG2YoyVucXauZaqiMk9LmQIWSK/YjHMxEUzr7vO2wADfU8OFj2QMXYent3Jkdn2vKmbhXWT7K/Qepv7hSY9qySCvn1aiUAa25xKTZBIXDHBMSM73sADycOAcVZNCIZXaA6XhpyWNaZ0GJ8usO4khyvxefDe8z6eRnv2i6rHP8WKpR68ngOeQ278dARKnFACxH/Yrnj6URSMDFZ4uIZGNnnbuciwqNhriAN+qlZuGn0NuCA5UU+8VQqy25nJTMGvRaaV9oUfqSYZUq+bU1m5nhB4JzLAERaGxWovGv4zpsVQ4Noy9PBkwpWqVFAgED6F8N2FgfJJ00sBQ0jjYjKukB9qPMEqBS1ygFcAAgIvZONvLUGWnKM6ItX0Z2nRva0lGNjmWce4gt4CH4g9h0QHlmtcTW+yGoBBFmmzjRP2spC2k1X+yboAuU8ZaS9b3ylKjcTFtV32OpJRiYg4xmXYAkr5k8skfKU0m7hFothIm187FMLYkI7JA2iSvw26WggVVQOXWFsCx3Tf9kpgrGJ0Lt4BawFohs7lEwL1An2/bhfDOvzitrIsKIhZhhkLFq/d5wFTU6rHZlPUoa30saFHlzsjqWJ/RRo4TH4IJSeTB7rLbmKZRjNUIeyZ+VpjkD08pkRBJQgToSk5OOzGEOkqaBknOngwwqKMd8h2JwQMpwhLGMh/oABMJhmxdAtX/Hufusz7F1vnfWR5ewK4Huao9hAqWn92r+ZhLH8hrUUX0Cn2NOZyeHgTm6RnklKSjywGtL60T+rPy54uToofj/WsFCgYDquF8BTFEACCS/5IixRM3H9c3zGCHIoQpRRY8RvBz70fOSR7JhlPWQJsGE3F4yL3iVNhcDwyFlRnMn7rXKW2rNy3OSZZiWSSjCziyGmB0QYz9vCdgYDYYALrBz4uwoM5sY8o4nytXyLFX7ovDpMEd2DTwMgDvI4NlsgC0bYYByXw9vC00BMpaaffYWtvlCbwctOSK2etblzogoV9NVWjrmZ+tW7tTQPJh5VGLrhFHltdIsDOBS4E+3HgVMWEwlP3bj5gChqQywCQud3Us5zBe5ovm1cmqgQ5anyiVhQ8khVLlznIl2KuYifIxchIW02h820Le3ZYe5FPe4kX3WD2fULGOJgt6cPCZp8nKtLNRkxZRjY8xUrwuzaJYOvFytZOELPmoeNFhiNKWpZdk+maqmyw5uMnAAeR9pxAnb2RZc0LWwJmedchparoWWVHjugonGpst9idW0xEqB27Mc+Nh7S73DPyaR6WfbtqV1PbM237roEhF1Nr9rn6PHePwSp9rB8Z9EtYlcDKr4TRn9GXiWINx8tFybIk/gLPUBkLfZ5lnKH0vL42Dfpo5z1tTzAeWCMdLdWCwLKk8/IvjAyuC42kZawoRiZQCOJiic6ksINniaqa1/uk4xLFOF4VK4Hx7mPJsFdNpLWeajbEUZ5xz7LWLTS2VnMQRg2DjQxiWwASIIEZCABj+IIKwgAiEwST4d6J8TjLK5bDYujoAvQ+jKjz/MpaA3qN19oQu9c3Q4LEs3nHnilXN/yQlBeb8GQiyl2hodheyF2XLxaf1klaRh8CCgfkxR5DHna+DByGFybVU/SFy2WQGHaZG4ne9Eep54vaJQInINh+gApVhOnPYP5NJLJaCpEBefza+OExpZJrts5BrmdjnH8tY12P3RFE7kI6kJhDUfimnDZ+09lXJV62Wf22wnA6SMRmepzdAiTaciVqt6ZP6y8jkI4Onlc/LSn+enoUuBfODUfsev5cbH1NM+ywwMDui8mGtY9ZUSEG6V7T0wmLRLI4BM0doBfOx4PzfPWkjHhbBUKuHUUvMg3ivSYnrTniFvFAZdZUJWN532pWu5bNKvpKUb21k65+Fb0e1HZxMZ+Tj/WmimhNqbi5SuLT1QYa5v8ucrvDBZkbk6luXAeEfM1zbVovjM0dy0lla+ixjEPl+XdJ1KfzmYUC07lC/X0g0XkY+inrXnTyD2JD4sFoNTCxNOMd5zkTKWCNRIW6IN4LYDdS4wMUpCgr4LteGz1dY4XMQAeQEfFteH6y9nKo8pFJ4I+PRLYPCgTz58BMnqQuvInHg9pRPZC62j/gmgdhEwXbFTLZMsPAACEABWLbK2gSxO9FaMby4Zrj5qrFJG66r9qc/4PFzlU5pjNS2Gdc9jhWyTPuWPI8Dgh3QtT1PNKX5r+dqzoEHz8KWB69c2/1MmpplmwhGsLc/P+FsIMlrrmVVlxY2ruVRqZGPlPuMxh3FYVjKgIbvPca4zwrua03ZQjQXVVCCqR+8Kc1aj1kW/UiKVx3Ii4FjuMUz9M2oplMZNSVlfDv4FHehqhyzqRqudsQ9Y+ENFVlxNI3XjE/N7wrTeXGLpfTbX7S1VoWLUVWNKBkLUnoe1OpWq2Xbic37tXaTVDKuWngBRsqqPfOl2WlXWHPljOfQCGjNmey0BHoSA9BOD+pyBEHFjDZKLVjWv9KisaKs6dTxqf1gAFtMIfhtuniqbFjiHTGmVGRwhAD8EEZLDIueCHaD8ZV6wldFBGW2DYRAl4ZeQnbzNkqpQWl8Pp1KoK/38C/AwgD7+6Dz5Xuh0IoxPQi70dtLhCftkaN5bkL52AqCf9uR0Ckgo1YjXEHPun0od+7WyiV7wbX31WgAcrMbguWlpvkEGSgcwFH+PDjROqGutu5lZFHb1ryLntJ61VI+yACQemfJo4eYlIDECGIo0TZqUa/I6VGUgGqdp1Xt0vVLn7HcpA6z1t1CKZSp/LBkXrbkyo/VQPfC11VanG7U0ZVr9roBC5xJQ0xKPWMYm2bWm3NMxi0YpkYTMrDVp1Hdo95vuIatrmJw48vAa8Ax6dlUfHLUB71m0AU7VNCnlhXecTtNkqNAk47CeVktPw77yHQaRGhWENiO05ZXP5JV9Opuks9LcAE4TuaaxMCVDnItpjjNul7foFXEds0rhwDn7zXGzmVchL0s2oC4DG/IhJuADRUZ9NIig+R3/aIbkjzKdYbap8AcY7I8xfxRRJ0rxrkvGF1X2gpBbq1h812bgqVYE0ez7YQDDL27BtwdsfvAc2HWLnpulIJCw88AvvgRtNsDN1am5XuhUonf7FOK9o6fq+HGKWrJQlqYiC5XAQ3G4Az3da2pYhXwi+KC/Mb3mc/Fp5UL2+eaG0+UsU4bYwzPXT05PpSXyjYIKsWo5wJAo1W8K+6lRKdMwc9LSn6njYkXDU+SVmY05gJGJxPQBSZmORnk0I3Ytage360lFOv2tDu+nxpnZazT9ICz147aGCNNa0rXiWGQf0VSYy58i4EeUDm6gBzcmAlvM3oc5qWuMWq63Nucs99hRpuF0NM1F9OgIOHjA34n57PWuDTRY0HXwASTxx/H8mkby20ZW/WfhArMcZFiDXreYTPVaDiokplJZjX3x29RnlH4pHYEOvgtUAxcAzAsImmhCPinJn7k/a5Gw0m9jixhOOrzVnllLE8/kYMOy7GrPjDSnjuT6kwh/lnB93iksKKXfT/DENus7D3DoC1bmfoxTG0qRPPIWU+HAlsG9R/+LOwzf9OC9x+aHL0Adgc/hIMiRDP6Dh//pZ3C/8d33ChF/EvQUHT1d6Hx0eALmEqeSbtTXUMvh2ghA0N9jeTD5nwr31K+BzWOobAB0MxLV/Dn3h2AOruJmhfUzbE60jAhcrGz/GagWLWGcCACoSJuDEPJnG5znr/fPxX6PkV3kQl1WGQMP0wWcAyNqayzwSN5ZyzL5sMRJ1bo8z0YPUS4ZucYhaXHGwxsTaUwBQQ9wB1DQjBBfVz6ZYbWqzwhrAotmxAnslXVNmKMDRCYbPPxuA7fbpoM4bY+2T9fFOaGaMEbTtFKkCd4iWjIpF+7rTtNksIwmfF8VcmhCg6GaRn8rMyoRq8wUYw4into9Yv1K1ABhnoLH5JH8MFWlBdVdxEwNrcGnTg21qWTVByfLM3vlVlSbKSpNI2brxZYhpoeWnAjUy2+knVoPacx4R/UML4KsF3U8wHg+EoCyGlP2JIuZ81jQgwEYABGMbPpJ1JtjhAp24xCetTrxbQ9/78EDMHy1R/edAdi6gH6fidEQ5HTlq2+AF8/Ok+eFhObW/nMtUBd6mvQ2ajIsoTXrbA1ALmU9TWd3dyNtBo2IxPl9oLnmJjMIAyxU5DwefGaWRgX4x+DU5pZsacIOSrnH8qKp38vMSssut9+TvwUeK+eeyI5bmOli84Yz0JQcNP1c7WrpjyEdvj5Yc5ZqJ4xMuadkj4Xpjnn/+m7tuzc+FUCyCdfrWejvUB8GRM5ycbKLrAWIGYKjXO7SMorK88DgvQjrtOtORpxGZbaoC8DJ7R54cRXCgxeUTY4F+TrfXkMeQtvqIUjbbHxmxDaVm42zh7CsUYvxAAtPxCEDvdy0NtVqKsxPKQIMlZW69shglKWn8tVOt95l7aTP7AeKutbKrm3WHhiIKDf5s5qVJ8rRY/mkDjytOQyvIfdWQyF6Vp545elVJVOJuT3FKcjqQ1MbvS+vj3tDGC+HdYPH7RxEeOOa2gjR+NoUVeb45IuaoXJ9YACkwKJTRhcWID09C/UghoTrKvzDVutr0CdyqJtXWPIMft1HRucHwO+HxMvPyGj44IGff43uene2PN9qsiesF+2OCx1LT8Hx42PT0s11sjVM1woNh0ybgTjXZpgzv6gQW60FvRbkLx7kpJQ8SwSw2vPpIeERCigAY760QJ2d7DMEkBOGsqRJzTSFCKn1836sBfoEzqyatNQEwlLGEqku0+SmEvNlzslFORngq8LfM1l44OT3yVJNvhlltAxgOOchTjOvqJVASQBWMEGbZixLk2kFZ3xWgDkDNKi2KxZu9jmADKo9ZjUojqQIUk4QAeLkHAD2PfjQj0BKAGOt17m6TZX7Js4mjpGFJjV9RxuwRVkeBzJYNLqCahMUvdY04bOcjCXDWQpMzFHF/AJACqmk9bUbHZ30ZpDKpsLXFwRCUiOyAISdXBZJr9XHN66fmVby9ZPeQw1zmVs4qwBEQxOhZMS2vGPKngMYjkXZz0UxwEGD6Y5UErKf820nqidVe9dR1l1lMM0wlOrdQnBcRWWdfdj+O/ke61mul0N6JJZswQSdxwPyNjWcPurSFvNjgO96+IOAWF0HYD/k4MTcwj+1cLtc2vKve3SHPq/I+0oXM4YLnYOequPHx6DZ9bhyv3QaWeG3WRjvU0n5koYoD2s/8QSvV2dzdrEOcl5m9tcIR0dTm8hGkVNiRymnlFoL8nlc3mvJK586E41AgMqyvFxLcyx7LTHPWEv15wsheOBwOEECLFnKzmEaQmupjd2iNS/3mIEQ9y1Fo1U7QR1ZFQCCaAZJnxCAGLINGMlZ9Vql/oz3VcYaWEwsDsPRawTb/JakJRJtjVsCv9oXfl0M+GLpqeLPb+pgZeH4Ww4yWOG3xows0gULNBQgRAcZDCOAwSBhwabnrFQe5WcdZECEDARAgjIrXo4TIl684K74HZ5dPQweEHg4lqZUA+eUOpp56l/Zr9BXJe+HOf3OGXMjwyVlF/ezzfwEQzt2Tk/thUpQw9ZB15E58wwq18qJekqc78BaCsbJtwfgqktjVoW0YxwXPYaPk4HjidmietoBp3SYSG5RfiV1VhTy44Ex3PkwRsXutmrPWaoqLu5TkxcB2BCwP1xO74neCz86F3oEeh81GdaQYbajGacMmjAtIwaNgZPkvEyWTDJCXNONvMZFvTMKwIKkKWTCSpHxuwP4qKFCmWyivrGm/EwtJefyfMrftfQtTYAlphJLOM4ak4s1OPEo7QoWGNnlKP34dI298Bb2LGEXlxzYtRzONw4bJw/CWvdmTuwn8/QQ8IB0LIa54wqHlQ6gAUnzIQANR8mBjqIvlSgTOaTNvWdgMEDgWjpiHeHQfr7vwdddGkDOvfkzG+0vlTtt+1y+137qtBhk4BXMl8ofYQDnqHe4TgEt88gGYkpn7pcmGWuoiURzvghUNrqlZkS8pwb9ZdfUNiuOBAO0nbN2Nz5FZwJHNa/SMST7sGky10dYTaWs8WLXrkwEGbJwTPJOvE/ggi137H15qmGVMs0zo0g+nMcTLq0E3pI5PklRA86bUwdm8H6Q06ESMAOWNfycxy21DX674GXJjhFuywGg65p+9160GEK8cj8AvA/qE3G9W19sszp7LyrDJVhLZ4hm8TbRqvFxoQu1id9VnwwPRa11tDRvmCI1qVhJBESmHcEK6+PLymqDz5g920OjSp0XsREFk5tpycgxmr747YtrU4WtpGPdx7QOV0YHGDPq43NRJVwDzGiBHK4ob4ytt8KM56T9TPGfaqqQKHz2A5htnPL5ckY0GV1hCjGw33l8DY05VntOBZGopYC0DxvVwSXdAzs/j2k/A3wY4O8Y/n5AtwHc8w14Uxkk8QVV9nsVDYw1dUj5mMt3PfhmY/xQ+Dd7vuvc0zO9OIGWazLUvC7XBhqNY6COyAGkiJjuchRI0GxaKPexQMMYpDR1LD7L72V5yqxKgEK/tlA+3ZTYOpX3M0ZXKbN2q8KklIml6ytXhfhs6Qm5Xr1mNrZO3nRbZLoqDGj69DJUZVBtE1vKKOUCu2ZoqDlCte4eoIYq4VK0/20i9pQQ60NYaIcT2kiq0XRCFmfcQMZq2Hl7SmY2xjQzuGdwjwDEEYgYfD8kRBp89Jgpq0sEEZz3lU2RozfLJB+ayHx5KnOQplnMhd4OWuRJ/kJ1aq2rDEw6ejJrPAHiX8HLZqPqQG5RXcx3NYMIJ9L6Xe5xSj6S+SrXmmVNrENGlooamX4OoBjTUlzcHMg2964tLYbFFGS3KX42JT8tJTq1ngto6sCICGJWs/dAZze7DyD/VQZD9Yyuqd0wkW+M8iCNHUVwsRoMhKBtNN7zHSOPMQDuGYevPe6+6QDeYHvjcbPzSSNC81xt2z1RKBW/7QTqUjv49gA82ybAQ3GPN2QfTZV+f5tphSZDxUheB9zad1GG74lhQpBOBr0BGiogBICcAZWaElo/owp0lKr3FPBQaVMWeqhkumX9FIUvEccpMERvW3wj07QoNBAaG/NZegSJOWo46nfmCET4oQAkClJ1OZ2P5zw4j2XUTvFPpEfVgAj9WytPtVpbz/kBIPjTYpBTQ3hbQWk4nwg2NE4Ajs3KCr6qCSJ/qZLeE/jgQWbh4iX9Wes0+9ORMMmNS06TUNx/InvvB6NoNvREGmrH+tviSfpCY9q/WwLekyBfcZlrj5PjhoDSJY2ApPdUvjNaDwoPkEX4zGcWlaL8DJoMeVjIIk15/UgqD3zk782uEeVhcX5vGRj+bmhzkhGGKpt8Txh6oHM+RK0y9+zPh+iIcxyGAMY3HNqbeFsWUdLgKJMd0Uz2wP4bwu1XG5GPQhWunvVJ24XmNWMmy4j/IG0oCGiaUg6UtocHL3JU6JdYi8fi48U4esNLw9lpuSZD3wjPoS/yWAoq8HXVnkDl6f7Eqk9EAQlfUYepDfUxm+1ssOv3NIlj9U10hBxomMi6vFcwrfgqsg06xWtr2nA6c12WgQIM6tjUD+NIEkQUNLoeGPmw9QqheM6a5wmMmSO6VLzrWv4cxC8Np2CYBjOBUEcZvBc8bzhIRJ4l4S4fh/j4g+szjOUsOx9AGFMXOZ3KtX7YA74PFQg0chpl860IuTUix+GIB3LCAmTCNZ26Jj9FajSHn0o7bZ8/kSpdaD3F+fSu0jGHQi0q5ba5xLW0JfjLHLUbdUMUD5PCGieHT+HZnoGNeMZn5sph0pQwpev2GAjQTrJmDktltKxpMV3R6doWUlNQkYGsludD0lo5ZAqUeEiaBkPKexQAklpG4z12yx9FSUMvPqtIx2CNaJmpxio6ZgyUp/f6lSsXz1rwRG4euL/tMBycGXcE3yOCDEQQYPHoQloyXitPiahG3oPuejEPDpqpyXveI+w1dD2mRyzzkWm5JsP9IyP8DVOEdPtMKJ/Vdjjx9J7LOte+I2/OiLnpd19fscqusEwQKDY+QNyQxv2pN+nf0HhOjF2YaooDPU47ZR3ztoesL0EMBRFGpwhcCAVhrMo1Hbh5WqBgmCGZ90ELZIYhDn3aMD8VoKHurKlClbFyTpDBDwTvCa7oFwUaLA19Hr612oZMuJ0mYYxA17EAv3ryahjVO8WmdH48gfHXJKInHer2QsuJ32VNBlKh/oyTqaXivQZ8qF0q5TvVdMhOnItnC/mNounuBLBrNNBqG3xmGslOQHutrvqIqslbU/k80gJ+buDgXPllcpARb7zn6JdhBJIYEMHmQ1SmpZC3edA8U/p5EK1aQr9PvJsqm+HSX8WToxUyxtmLZsLQkxwicnAuCYfDvUO34Wafri1jzbwhxyHCLosmw34w5q8P/ALtGKP4zztLy0GGmlpuSY7SOu/TihPRwzUjvLr48vR9hI3+sZtPVROqAQ4WjLDXRkfu5rm1oAXLQ1UVvnB/5JDRdklw3lczf4zPsUXO19kEztEcIKBaCOxZ/DB5wHMykYj5GDOYKcWVubqfGnO42L6fnUb5VwpUxjp18qDAgX4yU1q0K8w3UkDfywWevTCEOI7UkeCJjOAcdCTGcFZSjQXf0HKJQANRFTzLgAY/U1/WIy9EqzHasKggq5of8A6p6Bcb9kcFEidUZVo25UEqffMz40LnoEVyzlMjwmKzoUdRxWXzcYrmYSb78HihHFjMeDcugK+qzYDo02FRMUwiEwWgoTwVzWWntJkZHRbZqmagQnmQ8DTW6sfQSiiBAVuuD5s8HbuZ8+kZsgCCzVPKDAcpKvio6rw8mRKSeba17IeDA2+emeu3o7u1kAtWkYZZ1T1H2Y86ro+t2wnEnuB7insPgEADo99LpRcfHjULWLePIWKQJ7gNx/VQ9hv8sDKuSw6532qTiJULx3KQYUqNUEPvDRXbO9Su6R0c5ychZrBi1jQ9uxpQwtjwRfU7IE1gpZpmi2VykkG+As4g/rVJEjUJKyhdDi6kRbqUhfNNeuHEMTLU9SO+pkVQAgL63XsxgfADMNg40FoHNLuoyP98C8CS8h5LNbCqhaJMtJqWg3fgGsAwjaoTBRTXjVF7Je8Bh2LjtIC5vi/U0jICSuESon5XzhUy4292zJPYHDsOTodDKC0gCNmaQfTs9KRIPTarlte8+REfGRquQsYpZ8g6XAfqfXXM+nKBF94pesrmEhO+VtpS1ltGC+TBCF54ARkInKT2eDLIqw559MDFDxRBhpqMpd9HoieXMlEBWJzwevzC9VDLX7fhGl9raVRWtQeq2pgmXZGmBjSo66vKGcuI5rUZzAGLXoxgglxXTU6roTmlrZn5WrrIQavJDyRAg51DAHiAgA8n9OkSDQY7TsghOjdnT+LM07OYXZ2iGr3Ef1t1f/yGqLaHXfIMrVhUAy33yeC5zgACavxg3XckCMFTR+Ap0TxiWO5GCSBYMAHjPp95D1mWWdrqcXO12novaj34PK02P7+fAwq2i2zaYyjPi8PJgGgqiJoU9J/qa7F94hdoMSyhDCiq1HeOSu/IU9oE5yL2AFzDZGIC6bdaJGUVxYY/TxezZQDRd4Ms/AwCV8tZ33hyspjbz7edpB0I3sorfWKZpl8iOuUPJw2SgLqHNceD4ewizwCCN3wd50/GEaKCCwW4e5Sn+DVkAYRWWQNwAQcuVKMnp8lASGDZE8Y/zk4zshkzgw8s69+BwRsk9XpgHBaTzKcV5wpgHwAGAzSUphOxahrSu9DCjN8rMl2Uj454j4tM6YwMxSfumc5JUxywhTlboENkogWyl+GXFkSwJ+Wl2xybZ002IhaTx7ycEsi4UIumQADvCdDgWBN9OnWo0y4X+Tx38qdBOcX3gpGj+uE4/07dW7A2u3BQNUHsfTV4AjlXyJbrx/06c4kWyPCYR74rXuZkd7TyKlfzTJuB0i4uM/YKy6jjQpvBVKTyfsan6flyLPfza1WAwQIFFYanyLuqKmXO6cxktBoR3o+7okXKEErNhZLhZlgN1eu6lM6o1HAUtdD+WroyifdBiwBhExZPX+raDBa9z8ouxkXSTBhJOqJqX/HFkGnLhPYQ4XynyRoCs/xcSA/BzKc0d6hwkGmv2efI1/PR/jc9f3S9iBgc7P07Z+1IZfBwP4DvemF0aor0VMwmBnp8xYo4AS4C4IWOoycZwjKeIL3nVIQMVmfk3A8gdHHVHc1+I2yMDh6yblWHkkGjARXtBOMkMob3DuYWStOaoTVZrtHclaYma2Ui1X5sWohVgOsou5jXgCJdXTMzl5WibMPGZIKNqcMCGtebk6ZmBBr0k/Nn7LO2LeYigeM4qJRu6vHu8RsxuQXW8FJGG1BAIUP5YQzejEGe48CFch6oFTyFvZQD0mFXiGbIe7/O5FTX5Ic+NJmicu9d7mmDBj45zAMoAwOazrapCx3Kx8uW6xw/1ha9Ny3UmkXu5Hwq7avm7M0AHYWlhCDoceOo98P3gBlQUe3EB8PqWxY8JTtzPrkyxJwTwKC25JE52iysKhPXJ2ubOPhcwNHaEMcwyPk8H28BsGCCMlRrwhDuZIw4Mxe0jM4D1LFZ6BPpb9cl4ECZvv45l5DbUR0RhpHZJKd0T1OQPbfjyRYyLoyndm9t4cdVlj1lpy9JACy5ZqgnA/7gQecChCLyJdlP2lNrWg0x/KaHzhsO2xmL7qiIP36ht4m4FUXrMegNylJaMhfXnuz2iQVklc/wzgKzZWZQpzGuc0FhViYw9t1JW4FymSp+joGI0kSidQBkr8nf6T29xAS0JO+BrhtfrwEFrXvRFxiP5Rv5oRXE+B5ykwnblvjdACH2M7tm5R4LMGD8OWoLI/KwvPvWz8e3XlOTl27uuapVW5f/pWPVrDfOr57yg5zSPOcYDYaifDUNdk4P5fLjH0Lw63Lw86/7geWcTIawNHBdvpmTMzTU71J5pJLuHIeNKzQZKsxXIaKnTOcAIYaZHW2pYqJoue4eCTJQwiCleHqdqij2ymHBpvEGc74JOYoMLn5DvScnxgmUDLKc5PpZXC/AjGMAhhrauOY1dR0wzEyAs2BPBeMEGszKmFZYJroEaLD1bQKOlMCAUntiionavNMPinwVdDww9HjUalid0U3RNOOa5iAPJUAkgVVsV4kAOIYHobMOPDX0qFZjYADD8doLo+fWtI8b3x+eZFml3LnVm9oYAgCZ2NZv8mTjQicTHxqHKWeg6iy1c/ANglNcbsqJZF3xq04bHo46F+aW1IUZ4HuG7xkdPHhDAHyQnwDelDt6JJ9blfZQpkUaZKXGwcyk1kIGLAQ+b7wGTnXlqd187POTcgcSAFJqIsi18HsOaJigzkQQSocm6QAkHqyYuVICDKliCTPIDnIoATouyNmM5GT4fCNc+Xg7xRoHl49NArrMyEEBEFgsN+q+I1tK5BCO7Dgqp1JhAj5ZRAkukD00kk/2DFQAtQhYVijKGg+N/zqXur3Gf96kfHMirXD8OB1j8K3R5rMvy+xsF79CZVZL0tn+ss4jw7jPNpjhnwg0OGvvbRlgo7hiEzm6H7QaxLY/qfdlWg0FimmvewNa6F+MFjFC6Nkw2nR9qttmDxhMno9JpV+G0X2um0xYxmxVAHONhwKxJ10cAYDiM27lJlK1IYDkCwEu72MCP/zC+eD0eA0gAugIZH2KRNCx6qFBQCn9gFiAIdQFnsGDl42JC0cwi9VMFfg8U0Memdg56ZSnUv+noMlxofPQA4bqroKBbwqUKk/GRt4FzfzymGfQFvQ7R5vK0zxbPwbQcwQBfM+gnjEMhH5PcI6xfcHxfCfPB4DKEqw8OIC7qpVVkbdyE9K2zGTrmGsx0Oh6TGousMGQW6ROdFP+p/V3aaJQO9xqaQFkaYvNcwtksFoIZ6HwumJdw/shYmOpnApMh1PGifJZ6fTGnb2PZmiJ5jIh7E+OaF/ulFQOYfPz6bSPJFrnTyQzYw1yLRHEf5XnyX5kBnjgYEpRue90PXgg0nVzqXfXN0zHgC7LHT/uJ4z03dsawouLr+XvlaSb7rnnufFdyfC5AqTNzCzycJWWMabvUxoCmVaDggdGjT4tPGzCT6Yq23xbqOabPCVvtZ2IAMcnOWCyCLm91wIQWvWTLznAoH4R1tQpR+zDAFJgCcjVniqocSQvfWOTLqrDwnSn8k2GjFcCxAfKifkdQ2tOIZphZfUkxonQEx08quA7lWfIF/ug4ufm3Prk5R7l4OipEAHASvvJlfmX8dkBjBxYjtJf6J0g7nmdE6+ptXSc+/oKnUIE8epvfRkonR3cKPLznOSgljzTqgIBUHvtEgvxwHBP6PfAsHcYeoLrpKC7lxvs7xxcx/jun7xD17V7PB6WFGKtrMNjgCGZS1D8zHxbhZNxLXBq05YdvJiDmvjMgleTHyqN76u53dLNmqZbs7kdmXK6tBZmfhV0HBb5u3LtzPps3t9VSXJazjLUSd4H0UR3uncYG555j0fL5SbPpa+nPqaP2zNmz0RwwWiO0HKnnAIwAGzTU/G5sJZVEM5m1ZRX3qKdc9TYXFfnxSCDnwrtZF7SOyNvNbnTsi6eYwLVdJp/QPtmSgif6uk+/NJFdeXYVYbrNTxTQPstI2Zw/B01MRoId9k2rxojFpjgcUwSbmhHtMo4Js0arYi58awgTLkxUZW/CB4oYy2YrVUNbJch9mQ14KLaDg4Cz2S2UzdDHVXbgWeSIwFTM6lGmhtrKYJgRlB5bEmhNe4BFILSdB6kiCHBeM8WtGhKpZI9YTCnMOIgck3d3w7Gxgx884XD4d7ho+8O6LZW8DxTG8rxHYTTamVq86CV/kJvJfl+oQBu1vknR6ZuMZrLG9KYsAC41snem3hS0sPsJTzw8vMt7l46HPYOQ59u2gMS54B+T8BWC61mHZ5JZqTVWlRUtpMJRXjOrPmyWU+yU2znAsDBym1ckZeU9JAEoQ7kCDxwBBWWmJ7WTCxr1/T9WRmlBCIsuFA+P0ViNSwVdS0TC112CaiZrtp0oXRRwFlQh8fWFngXSBV99J2UVM73c5KdpyoqOePLoem7q8KzJUONcGH2r0sPkEqArVZukBcYeOvMKKvtOVJuXAwyDIdl6Z6qrdFZaeFGIn+GFm/+awBFjrab3ySaNhmTPGIs6ATOzCGCeSr7PHKE/p5715a5MlAw3lxlMXum8r2a9wORnWCuEv/WMqdoO0hA7qCIMs/NllGfg7mVYM2bZ5brX0irzlOgi4ybHNSMm/NHomb1AvbROsXKxkJwRgRuIe+lF/Q0kfwAMd0gLEbtJ4GRJ0qvvnT46Y83uL8l/PqfIXz8/cN6vxhzba6CaK0dQSu/t0uAuFCbhkPdbt5qqNnfj0Wt8kdD8i0BEFcTA4c7h8//aIuhD5v5ovEU1kPnCH4gCWTUwNqtZkLSIMhPV+dljJSzlZuYrWkF59oSvHC10HQcDmiy/LnJY5aQdRYNoKo5Wdso1sZ8DWCYU0+3eVk5dpwuvWBm9W2V8/5TqJThQsmr+3W9/NEu47FlmadPuek1BafXOoa9IwEamry5kWsAGAgM1ggiK+Tyt93B52w7zzgEl2sylLFia0R4IBunp0VVsGAxeLBCTchPZKuAgJdQMBw2K1FdvgAn5KB92UyMzAwJUBCmKQwz+oLK3vW8+QEXP0pGaVWSRxoPZ5zTGu+1mSclxltD94Gxn4TSKWMLYAAqaL0hiQkt76kUJBX40QXuzQMLD0NTG+c2c14vGZy9+zKQIVf3VAAqarIck7/OGeMbYlXUpScGAM9p5Xz9WYe7V4S+Z7z8bIsXnwzoNtOLzGhOvKNz5EIPQ8NAucDa2AQ9FsViiXNTSWXxI17y9AZ8vZ4rng+b//vbDfpetE3yzMNXShtRP9D8ehfkJ8CEA/f5QcASrlJqbiYNUKMNasCBuI5P1Esqkecr38dyVtOUC3WwoOaDwY6rlsxzNBX8UPvDlhXDdVKoX5SZUuW9gkZnHuJZ2Or4z5rn5aG5w5H8dysz2SG8SyZ4LZ8hU77MioTyAZWj6pHX5iiuBypDhXw07yWmb/H2gmAUT44qQOJj0HJNhgUgw1MTYt8ULbN3qvdnS7WOq/kKc+z3DuwJm10a+flcpUUTqEXKMJUP2Ly9UQOaQtfZj00j5Mb4Us2OsGSWD6nJYMuoo92YZHbnUsOzkSkGi+C/B0Dew5Pabp7hRelpkyE143AuAE6gTLiaJUqxvkHCFBnGGSwJsx1mJnW2Jj/SuFnepaXgZWxoGegPwDBw/O7D0SRRWAsfQOC80PtN/vCIA2rB+OU3jXI8EeKoCWpPt9lsVilu3r2XtUI3Dio7jEw1rByjvhaMDLNU+6sqi1QOoayWQyy3UKP2tl6tsnny53zdZtKvWlOLc6tSBowOr70dy4Eqmx6Vs3zMPNxrOKZ8WsSjw6fjD8gKeeINhrc9lZqy/wya48IEdp2kc139YGhqzCb5nUNdAB/UF60Gz2RNzDr9VNfjR9l7U97qpeN6eXQJE5GgRe4CMmR0rEpbLcxeC7gYPOHumw7DgXD9AtHJnO8Jmx2DNmwmmqD95DjAwkfUrQIwjDQSCrQ9Y5yj/NYtwjbObvm8ana0njuFfPBSG9F4D4nYEPJ1RUSAMqJEiXRrfZzTBEZN1wdVerw1TmffWlLP4muoPDmKQqlx2smU2wv6IR8j8/XSzNOGetBTNhduzDA659rz4SFpuSphZZ0Ln95TMAPTTQPLXGDKTyTOUN8LXUhpyWHKKZQLg0tG79MSZt8UlY4Ws3sFfyUKmrdxXZ44No75p89xuL3TSeUU5fs12amUm46WH1c+4w0/c25ezh/RII73aiN1Sn4Rflg+VW6utYx2Psp3S8DmTVCpUXmuMbRGdnhsmtKkAdb1QSlDqZxzTPutjwZmRKfa4u9qmUZ5krXilfUVOQPNmiE9hpxHhdbPwj3kKk2GyYbQIzX0LaflfZSQ9Rr5EBWivyd88yWj74GvvpSF9rBndB3w4SfAR9+mGC1gSdlEba/ENdW7kkYAQ/Y913ooy/Azi9V0uSgY9Al5BZOFWYeM2YmEMALvBc1mlqgBajah6pJAjkp7jwDbj+t7Ae0eixJS3iKvMdYL0mfihr4xbpYjzeMY26LFEPIhZeoTYzMwpOGx1uOSAZ6hXBH000ah71l4kHui80IOR9Lnhd5K8sPDCZFEpar7BUBYSqrWrLxV+7Emk3RdWD/UCeMiYZjMZj+Vt8bMNMvNIY/oFHKIWhVuLK9EoCNqPOQNG1YeOEwd4Mjpb94mVfs/imbW/NJkFEhOMudo2Xr/5hbdavSoc9OBJ+WTc5LIsED1ECBOkvMBKOKjKmgVFObCS85/ahpHzuXXmEm0IoM/h3JuVvOMJuBLKgGA5+W8WpSxJZo5jzm6221Yvw4Ca0GGBS1dqmL23pJhZMvSU3rMnuB78aD8y58Br7/xctpn/CdsNozrexPiqShPgAQGG9UhBA2HullU7oiuNJOQzzGjLBldAhcK50VhD8cVxmN/TS3mmb1qpQ1ZPOpFTDH/XeahfSKLZCpzGBDtx/S7paVy7ENspkpgZ61px5wd/Rpaq/bYKquWzzH1on7+Idtf4mBMmH/UXGkJdR6ghWg8M4KmEUftBSlckeR6x2X98MBrMI1+nBntZ4JzFOwvGf2B5XSye3iBbvV40vfCFI5LLgzwbaWTNBkswESVWfDIwyLxrPF1VTGeGtenqqXXyl2adpxAQIN4AOIQZJVcw1AKCvKq48lCR+03zrnFnGLmYG2F0E0k5m6E8YFIlmMBMCRggRPvNrLSlLxdk6VMjUYaHoS6bEK1sRxybr230eZw5Viyss+pWqgPRXFohXf04Ca8C+STkhYfblRBg/nyTlk/FMhwDlmY3UyMWbwISUXIJc1tLuaLLsrNOW3H+YSclT2iSXTtn13H6uWem07KsnnYm5urLB2NK0AGt+wk/CmeMj1hKvt0fIpZPiAnm5//gvHZLz32dyKEuy6fkMNAGA6IKsYlxbjIVvV4xRq2FL2tAQw2PFMEGixQwePnSoAgr2u+8Z8ag0vND1g5aGbyAOj0jQ6mtA9MvuqAs63i9/YedZYI8bHmHArMrKHxe2/nU0u7vF75fHFBhc+av6ivjBYPLGOAu24ZaMSgFGM+SHx5+6aF5vMwrBpgwNn3cZ+fmVNyiJoD6efdlYsnEUdlZujYzdO0vTIVnxd6G8kPcxvLROtlnfONjRb/fWfkr9phDANXzz3wyqE/hBXJmEioc91uI+YSXCy48xvy1u/xQxbAaYWOtA6mCx+Gi+SnzDTV8DNWJ9yTB1YLdjsLefcx/gDqWS9jyDZK11Mh2welPPo+U0vOKuUoPZwRTWnRUigdkJ5CVg7UfQZRWstHPj1mx9fEQc4TG5s1aobzXPZ08459X0v55KroEheQ4eGpiXAbDYHDPeHlVx77O29s/JLdll2gRf2Tw/+qtYCoDpj5MSnGlp4QOMeiDTfYUxCKTlCmTkTExk9uWkTaajCUAEOZhpkjCMKs0S1SwnyjmwqJKlinUAYepHCDscxzLzhlP56a/1Je+JDlnJL3Mbx89alJzgztElbT3LDjfd34ysOalveYOajxGcF5CnkvczgDAyTjRwKAWR8o2DKGup54upT1YU3gDqd9au+5uyJwT0eEm17vc+NC7y8tiqIV6LHMQ2vzek5OeJfIHkIQAVfPGXRLOIRDlAxg6MLGPpiTRqJla/WUT4RMxgliVckLahoiNfnIyk8t8lHmKw9Y0iFNquc4r1wusoJeSZWGNe/nVAt/eU6ZyPbd0zCTe3/AhTUaJFabKM4D5uzQrxyixzqzbGk3RA3QbE6GgwnjA+0okTKswW/D2ydi8JlQupL3HGOCu0KTwWxMJxPKR7Qivsh3ZyELCrz6hnH32o827X4Ik1rVspnAg3pTVbIqgTk6q4eVqsbEzFXbwvO1KU3apQCDRfNjHjateRbBM31VdXWGesD0RazVKB2584RCjP1QqCpNbpBmVrxMOGkIP+n7EZ1ky5oKo3UEMyHUhCd7ql5es+XVBcoYXpT0NwKiXngaDswRQXjlMGG8p3lBZ4HANQIugmkEm8TW/GaaeJHwvJTmGP+5NzEKooxOElkiS1gBfugJS31KJlXg0wGRC70/NDygT4Y5Wh2G9z0hWQOMrOKBzU6u9aHP9HRUD1qGXvnZuvWxlHHHGg4U5ZFUt7EfKyKIY2gjOzmiURSiZXVqX6vJQ76QpWx9a2OM9XkSZ8UluDJTu9GVBwUDaF3Y5nORjisxdXm68zSOwSP6qfreK3m4AljSsZ79RpI57fVjQnOWTh8pyOX2QFv9NzHyU3yVqXSzvNyvCUd5+bG1alo+8Sz1B2C7LSThKNeeXuEIDJ2c00pNBr8qIoF5uY2qPjWVqKdOXhnc0HboA6TNmQjqNBvy0E7QKZ8RI4y7BnpPUFXjYcHzkZEuSJ4x3woAMfVMSTrRKP5TfRpuYoOeHQq3qsDjEIjZzbh4FncW+JWIi60vToxHJzsPyThT3osgmcoJvrSFo5qdXiuFJiIB1kZMEIhOGX1gHApktLQVdPwsXadKJprAjkJwDTzQewKF904mNCUA0RqC3SyPKWkUraBKv9g2PqizSOLJty/rT76ZiFABcVBln2itHdNvcLN4obeXhscMYVnQ0zipfZoUgYZCTum6JGfqeisHLrKAr10fuTh8iWTWIo0+kR5qbNAaNKX5eUy6TKtT62LNK0ZgfZs8pk6KF+ZxBA851vpgjjefbY9xeHhgoak1cySp6DHVB3PllA4giQDvOMpxCmQsdZZ+LI3NVQAEmdZ7AryJomdPFYvqTFUvlyXTunHSq7DlTZyN2Xq1ZDCVh7/6csAXnzKevyB851e61O6Sf8T+OKIFU2CM1nVhtqscP64DGRJpDGP5oVfPewL3LlBts5N+WOZWNwOwmyMi8aZ62CdGPEVNbYCsfgQyCJ86vrP34Tg6JBqrCObXogqSbpaau/3QJqyb8HXNiHaaEZWb3AYNZizXVAfjnGyU9WiOjZ7Aia5bHN4wF3bYrh9hnNdyshtn+xvArPPF6jMNkEG1IMpyGzkDag5hBFQFIGrq0AKC8NGhZmv1lS9nyW59+Qgg10Sa7DQC+YmRDz5mmqGsWvPvselNl3+hk8gPyxxcH0vt8bk0CsL7TSr/VDf1Ye3gQc0a1/enjeRT5p3Kn5aXqnbqRngpT0hb4805jVCVTE7rlZ64VZOBmgcN8+vn5OHWavli2q/Euebhcj79eDSSQ+M/56dWeNF4fzQIUj+pr5GUFolBz5V7Ql9PKdaqLzQiAsU5VcxbKr4SML/nPH1wVPOvbcqPKYolytY//t0DXn4tk+2Dj55juw1ZWm0SAHooN320s4BOfHwxyNAfVEhennnVXivdXZ7RO0zNzSnqG2P1pqy2h9U8w2AbBuCwZ/jNfBnpguaaMwDdCKnJdmtDVq3PCFyglH+F+drvpfMk4tPX4mOY19wzZX/E+tIjhhJ8C6imrlZu2JWqnsOLOTEG5h4OTS+pJrwQ5WqKWk/vEbQXONOMGedh18zzMb03IVxlZRKDFmgX5GuCgA7eyxzyA0Z21qNyTqVx9nndGs+kV/bwJ14Xejgaej4b6FtqMZ1TwF16yn1ueuh1ZMnmG+CxacLo4OX4iubl1s044/dCHilDeAPtQwRyBB745Hep+bQocxLpx21wmap7vd/mwouvcbRc20es0QS50EqqOYjNZK6KnIUxGBZTs/wjB4Mp7amk2kj2t5IPmgoSvZtGWp41OWp877RKzrZxAhg5F7166XEwWjU/+8kev/rrO4AoyZ0EECVtk1maRKBOb9MKkKFEbBcADhVV5gu0kGipF1114MOQ0z1m4Nlzh8OB43sB8ry6Tn57z8HBwDKQQRnn+B5HVby8DRUGFzYJJVCgdRhpMwQ1/iXaDKnchJy/KYGrRmU9nmr4padEKmDUwAYOEH9N+Dm3wFsrvwUkxGuZl0j5CDwYpCFiCaCBEU8IioU7F5LPO5CbJ/+PREl4WfayOK4bnJ0SDj1j2IzzObejYUKInFE5rfGVmNmlsPPwYsaFHpL6w/yGqqTWxuws87kBwK7leUf5JZlZDx+CykOG7F7NcTQQ1tNxZ6xZG5qONOM/8mUR4BD+0WG0VHuydsjSilyxlMrnSu3Yklog/RTwswRgWHtAeaGHpRI8yH5PaCpYbelwBnASxys1LFVeYBZAgZhDdEuK6VzwuVCagZNJs8REYMn6kJxdF/V9QzQUe62f/+yA731/B4Azx//TPKm8eN46lrQYZIjoCU/Zj9dJgaUCYHovyTr1sADb5IAPp3lp8y8e02+eOdx+4zEMCYl0HbDZULRhGoZ1QpMtp6hCziimGGdVMyHFtK5pKHABTNjnR2n1mmOxlYwLZJ4O1gfBG6ALU11HCjaUtsktoGHO/OFYsoyPKC3aOQhBIa3l1PmJjI1THv17VIR2C8JNkfZL82Sssil4TC0aItTR8yNO95kZfS/1ZxYw1TnK8pIN/gkVxtjmFKDFod0iXfw/vDN02PMi+cZZYebM2isj3wznGF7HVvEJDe014ZKdwzrzwNA/s4cCzNUw8nHdtxv67H5ddoqANOXRJqy8lK4hOpOc1/aoy081spHJaifXtj71gjA7vqbqedFieNrkPcf9RGt81Mg6E9dnNLpWdmBjNsXih4lDuckXg4+birGcE9fimXV4tK4uWR94YbozUMZTsipwRHM2ne7tpK1DL3tzCaluF4vUF2/a188qkMGvjx9W0NNaSUQ9RyfPWM0tJTw+3Mq5ydbTOUK3SYs/uTSJJS0yTYfpfBei0RMTrsWgLcgxx8RLMGJ8HwbEEMZc046IgERxrfx+oadH54yfPM6bZ+eyDcVUIxlbHHzUcI6eNtbI6fbkz9h16bGpBlSso8qJIpmTiEU5MIYB2N/7yHP6nif9eZwWW/2yGFxI6NCvkHNofi15U1RbbyxIP7c5fVMndjX5wLZlqWbgKfxjrn+mxseS+nnP1SVn6WZ7csP/BsjKWRd696imzbn4WfNM6WNLNK3Dbz/2l+C6OrBYX59mAIYFfunm6HHWRUY3wVNqAOcwsACFDyQz09wh+AytMpc4HWQQWiIQrkGtyzjrrmufihZPFpuCVvvobG1v1mRiAI+ZT0D6AqLtnCgHdR0yxLE/VCauQ9wPMS8/6U91GPfDHGMVO/z0XHbaGy9zFQRYCw60GPAceHGht5PsKcwSGmkSzCyeVn0/gR8UvnNUoa+tD9bhUms9q6+F5bp0PL1JFDu9F8KwUgxlz+j7tCYMA+PQj/OI7TuXHT3qfTanEeIoCQBv+uTgQseTgPJLx+rDygWnjKM18tNTp7ItDJFbpvrn3O3PZZyGDLTqtL6euExXyk4PQWvUwZuHP29Ya/RCD0dZxKxyP7Fi021lKKLcmWlt/xMPcdCe64tNzmfAy4ciWz+7Jk3JrHP7qXL+9QeG6wjDTH+eRFkoXpF1lvbfYpDh/u70VTui/hNZnW7DzqA+OQGarvXSxfvpCPy2f6Itu5MXbwEGX0EGgcC0lmlnj8qzD/kFdnjjd5nAkdGdEeiQ/7bggRXoW74ZojbDzP0Lvd20FmGt25vm16IvAB2njLhii7fvoDkUxxBHDaJMnXBi8ZleC8+33jzcXsMIHg1Gn5x8HtGeAIBq/Pb+wGD2kXlqP8+3r0SV8upYZh/tL4/oNPvI8ljcF3pqdNgfoWM/Q8cKfKuHUe1IeTGvrzw4MVfWUsbzSdamaMuv5bTqau+bqg7F79Zza0/ixvJJXkCUa2b6dlqWHT/YWjdKE7zqIUroT0cBb1246S/X7lKLL9cQzU04Su1RuFTHUTmVZ8r7F3ns8ahmtnlqXksPScXHSPtld3GdSWnyuUFpL7VwkaxpQR7FpifW2Oo60yhkWkZI8qXt06j1UQEZaKg/s4xovMaGatTapFVfKues98mwmubRqHOSqEM/fDkldQsY8LmdANqNPvPYKQiQm0/U1NDXCMRNwGFhHi3QIbsyozJY3rO+GczVkDb4quhU62MsFVwY29OjNfN3zCxz1D2z3dOY6hBhLAk+FjQYh7BT8wglHe9W6F4zt8W/w/qBp2tMa846V2dKp9OYa2VM+4FOsLT+Lgigx5waz22MRn10AQjeazpezhnPv/PRfKay1h1f9zJaQy2vc8ovj65pcUJ5IpNM9+2SQ5dxntmVerpGvuW7sKaj0cFVhawKegkwKPA9spk35eWAPI+BBoi8VWrJRpv7qpbpRQ5705TJTUvWsapGKOWmyrzc2Se58VirFahr05p9i/WDN1OL6btL1tgzrWstGbi2Fh0OHl03FcnRUp5I+v1h27Q6usQcSUxf+a4vJZ6401yc0tNJ1Zgfk5YP4nPT/DFFWa8mWn5mJrnMx8MaFbugBdHI98KkLlTaDma/nVmcqR5nXUwvpgdS3T64gH/rtTP1Wj9Yyawx60wvVpck/2Z9ufzUbTJnWg4geR/aS+Jo7XBYBuSa0gAAbmWkgAu937REzmkBV8fJAGcQbC/04FR/t/X3ciyAsljzgeu22UGpLmgzlIcvRhO0Yes+RTUgYMqhpB7ulDJedORXNGBO0+FCT4dkTUrrFrNofbpuGahQlQGqspR9pp3x6oOESjlvep2dPiDK93llH88B47onl34vhd519TxGU2K5JsOeQ2UnXrYrbG9L2fuBAQalx1RXde7NqMfqaSh7noEZzkctk4Z4P3jTn3SaZHnlmoo3wYX25LOk6oRlpIrV9ZigtRo0fGxfvMO0dAFrbaiTVkKukRBNHwIzaa6tQ54vFXUqpzoRZSEMmRHXcVsXOhL4jIv6kjXmhHVI2nichsWSvFc7DgrzXZkj8/zpRWpDyOBMNHfSpuDJ2tPMCz09OlT8GJW/jwLYKhmde5y+LbR0jc9Mz06kpXmtnb8xPS+Qwzh/26sOdSpaoCOQvMxvBAqkMsuN2ZQDU2tian/nGoD1ZxVsAPKDLOdodNJtTeEu6+jD0UjD07z7sfmMTSefAmbJC7LKhdZvUSk3WRotnwSQ8u2G/KFrbs00Y+l6rPKBfH+YATbiHQvlHtsGW0/5LXVt7b/6w1i7QfnNUh5TvudzOl9faS4xV9kxOql2XLLI0+lxzdngFcULLRfDxyBXGfRnpbKhias9UIHz5O1GqrxZMIgpNcM1ZhHNdBNInGX7ZbpcrWtZWVNk86vWZaaM+ob59Ho9ZZpbxMo+iScxjsI4Gwt2yqyGBZ3Xeldr5rLr0NzcR18gjo8GAKxZx0OSmJKcZ8CV1T3Wz4GEs0L0yzBHNrTTKVTbZM6BDAO9W8723lfqD2c2t1TgCzgaaHxT9GDLztSaZjfiyk9PrYjKJHNraUh3zFvStSrPbPqZlubCovInN+JjAGJeqzQHyqkmcta+r+wsC1QQkfmdO/4melPawe82NQGGQsswf//yYh7ifWiYy7lNv26Djg7mQ+eRDyaWLblfrjFrAhgsPCQp5/IwjOf7nEluFUgnzS//PJXObi6RkXZ4xjVO4xms/1TyeVSQIbQt82d05nLTIrzstF7uHz+JSru5JRvwiK61GGb2/HIbraa2xBr0/xFluqX9dKFEx/ZZdMxYud9aWGtz8+j5SrptOM5PwOJiHODPuKbUnF7Ge+doB02fYKwlFXbH2iPhnj2BATAceTpRCi7H1l/H48VC4+2lYVhn7wvMzKtzaiucKDutLu4xC3tCFGVM/X3k61t62NJ65v/f3r0tJgoDAQCF///jXVv6IMg9hDDZrnjOQ1+UiFDJMJmEzdcuxE/jdMGx/3q9ttnGKm2S9fklx2tvykVpe+wb+tVnPzqOFl7pt5aj7/nbLe+O07Ha1qU5df2dtXvmOrxzrT0aRMzalZ1MSXZfsfhBPGcZDI30jwufHNd247vs7mebeTybpuky/2HykwyP3HfO3XU+4VbZTqQrWbczC64Mpo/be1WKLPcg0W5u6f/dO4y7f79IEcFbVJtn1Cxznl1XIjLJfQfTNc9r8TK4jBSzJkSi/Ummv2vKEyPL0dHoXZZkeF9//+SdvGGhs+ffwBOeGG3/1Jv+3zYrU75wqhNjk6ceJV7y2vQ9uf2XWOZ+jqY8l7oSE+1Nzzjcph2imcIvMx0EDxpoOPzIgwqD+ZunsVuzW2n1eHSzuHF5PPMSH33i8cx5zHxbdpKh1F0vVLW/V2rtiyNF+/Z1nBC667mEM2r+Dmr/xqrORfw+d8N+NPrR9SXHuWWEu5+zUf5e84bNtIn3lT2/t19/6IxTAfQbypp6OBlFHx+H+IygZ9sOQXOiQPlfxyPb1QH12q5FHEe0K/9TpduWxjKz62t/817jmrsV3+Tu8xCzzKZ192uaLOOLr8dQed6OFRttOsGwqlb43k+WD4M6pbmc6kkGyvxGR6DzgXur9Rsvbzd3w/MfcPZZ2lHMJb6/qJLwz7Q8EKkD46ABazUqYePtf1iyymBvna9uvf/jopjzRYtf1QkZ+1VShZo7mCLJAMDtuNkHAP430U9RWbWXqLZbLW68ESstnziznIafG19JMgAAAMCN5SQIrkzZn2q7K48jAAAAAOhFPhEaAAAA+GCSDAAAAEAISQYAAAAghCQDAAAAEEKSAQAAAAghyQAAAACEkGQAAAAAQkgyAAAAACEkGQAAAIAQPzLkLh5MW42iAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize input images and depth maps\n", + "n_images = prediction.depth.shape[0]\n", + "\n", + "fig, axes = plt.subplots(2, n_images, figsize=(12, 6))\n", + "\n", + "if n_images == 1:\n", + " axes = axes.reshape(2, 1)\n", + "\n", + "for i in range(n_images):\n", + " # Show original image\n", + " if prediction.processed_images is not None:\n", + " axes[0, i].imshow(prediction.processed_images[i])\n", + " axes[0, i].set_title(f\"Input {i+1}\")\n", + " axes[0, i].axis('off')\n", + " \n", + " # Show depth map\n", + " depth_vis = visualize_depth(prediction.depth[i], cmap=\"Spectral\")\n", + " axes[1, i].imshow(depth_vis)\n", + " axes[1, i].set_title(f\"Depth {i+1}\")\n", + " axes[1, i].axis('off')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/da3_tutorial.ipynb b/notebooks/da3_tutorial.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..b1696ac030f5f0aa3660e400b643613b0a7b002e --- /dev/null +++ b/notebooks/da3_tutorial.ipynb @@ -0,0 +1,667 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 🌊 Depth Anything 3 — From Images to 3D in Seconds\n", + "\n", + "
\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Aedelon/awesome-depth-anything-3/blob/main/notebooks/da3_tutorial.ipynb)\n", + "[![GitHub Stars](https://img.shields.io/github/stars/Aedelon/awesome-depth-anything-3?style=social)](https://github.com/Aedelon/awesome-depth-anything-3)\n", + "[![PyPI](https://img.shields.io/pypi/v/awesome-depth-anything-3)](https://pypi.org/project/awesome-depth-anything-3/)\n", + "[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n", + "\n", + "**State-of-the-art monocular depth estimation + 3D reconstruction**\n", + "\n", + "
\n", + "\n", + "---\n", + "\n", + "### What you'll get:\n", + "\n", + "| Input | Output |\n", + "|-------|--------|\n", + "| 📸 Single image | 🌊 Metric depth map |\n", + "| 🎬 Video / Multi-view | ☁️ 3D Point Cloud + Camera poses |\n", + "| 🖼️ Any scene | 📦 Downloadable GLB file |\n", + "\n", + "---\n", + "\n", + "### ⚡ Quick Start\n", + "\n", + "1. **Runtime → Change runtime type → T4 GPU** (free tier works!)\n", + "2. **Run all cells** (Ctrl+F9) or click ▶️ on each cell\n", + "3. **Upload your images** in Section 4\n", + "4. **Download your 3D model** (.glb file)\n", + "\n", + "⏱️ **Total time: ~5 minutes** (including model download)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 🚀 **1. Install** (run this first!) { display-mode: \"form\" }\n", + "#@markdown > ⏱️ Takes ~2 minutes on first run\n", + "\n", + "%%capture\n", + "!pip install awesome-depth-anything-3\n", + "\n", + "# Verify installation\n", + "import torch\n", + "from IPython.display import HTML, display\n", + "\n", + "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", + "gpu_name = torch.cuda.get_device_name(0) if device == \"cuda\" else \"None\"\n", + "vram = torch.cuda.get_device_properties(0).total_memory / 1e9 if device == \"cuda\" else 0\n", + "\n", + "if device == \"cuda\":\n", + " status = f'''\n", + "
\n", + "

✅ Ready to go!

\n", + "

GPU: {gpu_name}

\n", + "

VRAM: {vram:.1f} GB

\n", + "

PyTorch: {torch.__version__}

\n", + "
\n", + " '''\n", + "else:\n", + " status = '''\n", + "
\n", + "

⚠️ No GPU detected!

\n", + "

Go to Runtime → Change runtime type → GPU

\n", + "

Then restart the notebook.

\n", + "
\n", + " '''\n", + "\n", + "display(HTML(status))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 🧠 **2. Load Model** { display-mode: \"form\" }\n", + "#@markdown Choose model size:\n", + "model_size = \"DA3-LARGE\" #@param [\"DA3-SMALL\", \"DA3-BASE\", \"DA3-LARGE\", \"DA3-GIANT\", \"DA3NESTED-GIANT-LARGE\"]\n", + "#@markdown ---\n", + "#@markdown | Model | Speed | Quality | VRAM |\n", + "#@markdown |-------|-------|---------|------|\n", + "#@markdown | SMALL | ⚡⚡⚡ | ★★☆ | 4GB |\n", + "#@markdown | BASE | ⚡⚡ | ★★★ | 6GB |\n", + "#@markdown | LARGE | ⚡ | ★★★★ | 8GB |\n", + "#@markdown | GIANT | 🐢 | ★★★★★ | 12GB |\n", + "#@markdown | NESTED | 🐢 | ★★★★★+ | 16GB |\n", + "\n", + "from depth_anything_3.api import DepthAnything3\n", + "import time\n", + "\n", + "print(f\"📥 Loading {model_size}...\")\n", + "start = time.time()\n", + "\n", + "model = DepthAnything3.from_pretrained(f\"depth-anything/{model_size}\")\n", + "model = model.to(device).eval()\n", + "\n", + "print(f\"✅ Model loaded in {time.time()-start:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 🖼️ **3. Try with Sample Image** { display-mode: \"form\" }\n", + "#@markdown Run depth estimation on a sample image\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from PIL import Image\n", + "import urllib.request\n", + "import os\n", + "\n", + "# Download sample\n", + "os.makedirs(\"samples\", exist_ok=True)\n", + "url = \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1280\"\n", + "urllib.request.urlretrieve(url, \"samples/mountain.jpg\")\n", + "\n", + "# Run inference\n", + "result = model.inference([\"samples/mountain.jpg\"])\n", + "\n", + "# Visualize\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "axes[0].imshow(result.processed_images[0])\n", + "axes[0].set_title(\"📸 Input\", fontsize=14, fontweight='bold')\n", + "axes[0].axis(\"off\")\n", + "\n", + "depth = result.depth[0]\n", + "im = axes[1].imshow(depth, cmap='Spectral_r')\n", + "axes[1].set_title(f\"🌊 Depth (range: {depth.min():.1f}m - {depth.max():.1f}m)\", fontsize=14, fontweight='bold')\n", + "axes[1].axis(\"off\")\n", + "plt.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04, label='Depth (m)')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"\\n📊 Output shapes:\")\n", + "print(f\" Depth: {result.depth.shape}\")\n", + "print(f\" Confidence: {result.conf.shape}\")\n", + "print(f\" Camera intrinsics: {result.intrinsics.shape}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 📤 4. Use Your Own Images\n", + "\n", + "Upload your images and get a 3D point cloud!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 📁 **Upload Images** { display-mode: \"form\" }\n", + "#@markdown Upload **2-50 images** of the same scene from different angles.\n", + "#@markdown \n", + "#@markdown 💡 **Tips for best results:**\n", + "#@markdown - Move the camera, not the objects\n", + "#@markdown - 30-50% overlap between consecutive images\n", + "#@markdown - Avoid motion blur\n", + "#@markdown - Good lighting helps!\n", + "\n", + "from google.colab import files\n", + "import shutil\n", + "\n", + "# Clean up previous uploads\n", + "upload_dir = \"my_images\"\n", + "if os.path.exists(upload_dir):\n", + " shutil.rmtree(upload_dir)\n", + "os.makedirs(upload_dir, exist_ok=True)\n", + "\n", + "print(\"📤 Select your images...\")\n", + "uploaded = files.upload()\n", + "\n", + "# Save uploaded files\n", + "for filename, data in uploaded.items():\n", + " with open(f\"{upload_dir}/{filename}\", 'wb') as f:\n", + " f.write(data)\n", + "\n", + "image_files = sorted([f\"{upload_dir}/{f}\" for f in os.listdir(upload_dir) \n", + " if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp'))])\n", + "\n", + "print(f\"\\n✅ Uploaded {len(image_files)} images\")\n", + "\n", + "# Preview\n", + "n_preview = min(6, len(image_files))\n", + "fig, axes = plt.subplots(1, n_preview, figsize=(3*n_preview, 3))\n", + "if n_preview == 1:\n", + " axes = [axes]\n", + "for i, img_path in enumerate(image_files[:n_preview]):\n", + " img = Image.open(img_path)\n", + " axes[i].imshow(img)\n", + " axes[i].set_title(f\"#{i+1}\", fontsize=10)\n", + " axes[i].axis(\"off\")\n", + "if len(image_files) > n_preview:\n", + " print(f\" (showing first {n_preview} of {len(image_files)})\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title ⚡ **Run 3D Reconstruction** { display-mode: \"form\" }\n", + "#@markdown This will:\n", + "#@markdown 1. Estimate depth for each image\n", + "#@markdown 2. Compute camera poses\n", + "#@markdown 3. Generate a 3D point cloud\n", + "#@markdown 4. Export to GLB format\n", + "\n", + "from depth_anything_3.utils.export.glb import export_to_glb\n", + "import time\n", + "\n", + "print(f\"🔄 Processing {len(image_files)} images...\")\n", + "start = time.time()\n", + "\n", + "# Run inference\n", + "result = model.inference(\n", + " image_files,\n", + " process_res_method=\"upper_bound_resize\",\n", + ")\n", + "\n", + "inference_time = time.time() - start\n", + "print(f\"✅ Inference done in {inference_time:.1f}s ({len(image_files)/inference_time:.1f} img/s)\")\n", + "\n", + "# Export to GLB\n", + "output_dir = \"output_3d\"\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "print(\"📦 Generating 3D point cloud...\")\n", + "export_to_glb(\n", + " result,\n", + " export_dir=output_dir,\n", + " show_cameras=True,\n", + " conf_thresh_percentile=20, # Filter low-confidence points\n", + " num_max_points=500_000,\n", + ")\n", + "\n", + "print(f\"\\n✅ 3D model saved to {output_dir}/\")\n", + "!ls -lh {output_dir}/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 📥 **Download Your 3D Model** { display-mode: \"form\" }\n", + "#@markdown Downloads a `.glb` file you can view in:\n", + "#@markdown - [glTF Viewer](https://gltf-viewer.donmccurdy.com/)\n", + "#@markdown - Blender\n", + "#@markdown - Windows 3D Viewer\n", + "#@markdown - Any 3D software\n", + "\n", + "from google.colab import files\n", + "\n", + "glb_file = f\"{output_dir}/point_cloud.glb\"\n", + "if os.path.exists(glb_file):\n", + " files.download(glb_file)\n", + " print(\"\\n🎉 Download started!\")\n", + " print(\"\\n👉 View your model: https://gltf-viewer.donmccurdy.com/\")\n", + "else:\n", + " print(\"❌ GLB file not found. Run the previous cell first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 📊 5. Visualize Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 🌊 **View All Depth Maps** { display-mode: \"form\" }\n", + "\n", + "n_images = len(result.depth)\n", + "cols = min(4, n_images)\n", + "rows = (n_images + cols - 1) // cols\n", + "\n", + "fig, axes = plt.subplots(rows, cols, figsize=(4*cols, 4*rows))\n", + "axes = np.array(axes).flatten() if n_images > 1 else [axes]\n", + "\n", + "for i in range(n_images):\n", + " depth = result.depth[i]\n", + " axes[i].imshow(depth, cmap='Spectral_r')\n", + " axes[i].set_title(f\"Frame {i+1}\", fontsize=10)\n", + " axes[i].axis(\"off\")\n", + "\n", + "# Hide unused subplots\n", + "for i in range(n_images, len(axes)):\n", + " axes[i].axis(\"off\")\n", + "\n", + "plt.suptitle(\"🌊 Depth Maps\", fontsize=16, fontweight='bold')\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 📷 **View Camera Poses** { display-mode: \"form\" }\n", + "#@markdown Visualize estimated camera positions in 3D\n", + "\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "# Extract camera positions from extrinsics\n", + "positions = []\n", + "for ext in result.extrinsics:\n", + " # Extrinsic is world-to-camera, invert to get camera-to-world\n", + " R = ext[:3, :3]\n", + " t = ext[:3, 3]\n", + " cam_pos = -R.T @ t # Camera position in world coordinates\n", + " positions.append(cam_pos)\n", + "\n", + "positions = np.array(positions)\n", + "\n", + "fig = plt.figure(figsize=(10, 8))\n", + "ax = fig.add_subplot(111, projection='3d')\n", + "\n", + "# Plot camera positions\n", + "ax.scatter(positions[:, 0], positions[:, 1], positions[:, 2], \n", + " c=range(len(positions)), cmap='viridis', s=100, marker='o')\n", + "\n", + "# Connect cameras with lines\n", + "ax.plot(positions[:, 0], positions[:, 1], positions[:, 2], \n", + " 'b-', alpha=0.5, linewidth=1)\n", + "\n", + "# Mark first and last\n", + "ax.scatter(*positions[0], c='green', s=200, marker='^', label='First')\n", + "ax.scatter(*positions[-1], c='red', s=200, marker='v', label='Last')\n", + "\n", + "ax.set_xlabel('X')\n", + "ax.set_ylabel('Y')\n", + "ax.set_zlabel('Z')\n", + "ax.set_title('📷 Camera Trajectory', fontsize=14, fontweight='bold')\n", + "ax.legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"📍 {len(positions)} camera poses estimated\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 🎬 6. Process Video" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 🎬 **Upload Video** { display-mode: \"form\" }\n", + "#@markdown Upload a short video (< 30 seconds recommended)\n", + "\n", + "fps_extract = 2 #@param {type:\"slider\", min:1, max:10, step:1}\n", + "#@markdown ↑ Frames per second to extract (lower = faster, higher = more detail)\n", + "\n", + "from google.colab import files\n", + "import subprocess\n", + "\n", + "print(\"📤 Select a video file...\")\n", + "uploaded = files.upload()\n", + "\n", + "video_file = list(uploaded.keys())[0]\n", + "frames_dir = \"video_frames\"\n", + "\n", + "# Extract frames\n", + "if os.path.exists(frames_dir):\n", + " shutil.rmtree(frames_dir)\n", + "os.makedirs(frames_dir, exist_ok=True)\n", + "\n", + "print(f\"🎞️ Extracting frames at {fps_extract} FPS...\")\n", + "subprocess.run([\n", + " \"ffmpeg\", \"-i\", video_file, \n", + " \"-vf\", f\"fps={fps_extract}\",\n", + " f\"{frames_dir}/frame_%04d.jpg\",\n", + " \"-hide_banner\", \"-loglevel\", \"error\"\n", + "])\n", + "\n", + "video_images = sorted([f\"{frames_dir}/{f}\" for f in os.listdir(frames_dir)])\n", + "print(f\"✅ Extracted {len(video_images)} frames\")\n", + "\n", + "# Preview\n", + "n_preview = min(8, len(video_images))\n", + "fig, axes = plt.subplots(1, n_preview, figsize=(2*n_preview, 2))\n", + "step = max(1, len(video_images) // n_preview)\n", + "for i, ax in enumerate(axes):\n", + " idx = i * step\n", + " if idx < len(video_images):\n", + " ax.imshow(Image.open(video_images[idx]))\n", + " ax.axis(\"off\")\n", + "plt.suptitle(f\"🎬 Video Frames ({len(video_images)} total)\", fontsize=12)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title ⚡ **Process Video Frames** { display-mode: \"form\" }\n", + "\n", + "print(f\"🔄 Processing {len(video_images)} frames...\")\n", + "start = time.time()\n", + "\n", + "result_video = model.inference(\n", + " video_images,\n", + " process_res_method=\"upper_bound_resize\",\n", + ")\n", + "\n", + "elapsed = time.time() - start\n", + "print(f\"✅ Done in {elapsed:.1f}s ({len(video_images)/elapsed:.1f} FPS)\")\n", + "\n", + "# Export\n", + "video_output = \"video_3d\"\n", + "os.makedirs(video_output, exist_ok=True)\n", + "\n", + "export_to_glb(\n", + " result_video,\n", + " export_dir=video_output,\n", + " show_cameras=True,\n", + " conf_thresh_percentile=15,\n", + " num_max_points=1_000_000,\n", + ")\n", + "\n", + "print(f\"\\n📦 3D model saved!\")\n", + "!ls -lh {video_output}/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 📥 **Download Video 3D Model** { display-mode: \"form\" }\n", + "\n", + "glb_file = f\"{video_output}/point_cloud.glb\"\n", + "if os.path.exists(glb_file):\n", + " files.download(glb_file)\n", + " print(\"🎉 Download started!\")\n", + "else:\n", + " print(\"❌ Run the previous cell first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 🔧 7. Advanced: Python API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 💻 **API Reference** { display-mode: \"form\" }\n", + "#@markdown Quick code snippets for common tasks\n", + "\n", + "from IPython.display import Markdown\n", + "\n", + "api_docs = '''\n", + "### Basic Usage\n", + "\n", + "```python\n", + "from depth_anything_3.api import DepthAnything3\n", + "\n", + "# Load model\n", + "model = DepthAnything3.from_pretrained(\"depth-anything/DA3-LARGE\")\n", + "model = model.to(\"cuda\").eval()\n", + "\n", + "# Single image\n", + "result = model.inference([\"image.jpg\"])\n", + "depth = result.depth[0] # Shape: (H, W)\n", + "\n", + "# Multiple images\n", + "result = model.inference([\"img1.jpg\", \"img2.jpg\", \"img3.jpg\"])\n", + "depths = result.depth # Shape: (N, H, W)\n", + "```\n", + "\n", + "### Output Attributes\n", + "\n", + "| Attribute | Shape | Description |\n", + "|-----------|-------|-------------|\n", + "| `depth` | `(N, H, W)` | Metric depth in meters |\n", + "| `conf` | `(N, H, W)` | Confidence [0-1] |\n", + "| `extrinsics` | `(N, 3, 4)` | Camera poses (world-to-cam) |\n", + "| `intrinsics` | `(N, 3, 3)` | Camera K matrix |\n", + "| `processed_images` | `(N, H, W, 3)` | Resized inputs (uint8) |\n", + "\n", + "### Export to 3D\n", + "\n", + "```python\n", + "from depth_anything_3.utils.export.glb import export_to_glb\n", + "\n", + "export_to_glb(\n", + " result,\n", + " export_dir=\"output\",\n", + " show_cameras=True, # Show camera frustums\n", + " conf_thresh_percentile=20, # Filter low confidence\n", + " num_max_points=500_000, # Max points in cloud\n", + ")\n", + "```\n", + "\n", + "### CLI Usage\n", + "\n", + "```bash\n", + "# Single image\n", + "da3 infer image.jpg -o output/\n", + "\n", + "# Directory of images\n", + "da3 infer images/ -o output/ --model DA3-LARGE\n", + "\n", + "# Video\n", + "da3 infer video.mp4 -o output/ --fps 2\n", + "```\n", + "'''\n", + "\n", + "display(Markdown(api_docs))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 💾 8. Save to Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 💾 **Mount Google Drive** { display-mode: \"form\" }\n", + "\n", + "from google.colab import drive\n", + "drive.mount('/content/drive')\n", + "\n", + "drive_output = \"/content/drive/MyDrive/DepthAnything3_Results\"\n", + "os.makedirs(drive_output, exist_ok=True)\n", + "print(f\"✅ Drive mounted at: {drive_output}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#@title 💾 **Save Results to Drive** { display-mode: \"form\" }\n", + "\n", + "import shutil\n", + "from datetime import datetime\n", + "\n", + "# Create timestamped folder\n", + "timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + "save_dir = f\"{drive_output}/{timestamp}\"\n", + "os.makedirs(save_dir, exist_ok=True)\n", + "\n", + "# Copy all outputs\n", + "for folder in [\"output_3d\", \"video_3d\"]:\n", + " if os.path.exists(folder):\n", + " for f in os.listdir(folder):\n", + " shutil.copy(f\"{folder}/{f}\", save_dir)\n", + " print(f\" ✓ {f}\")\n", + "\n", + "print(f\"\\n✅ Saved to: {save_dir}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 🙏 Credits & Links\n", + "\n", + "
\n", + "\n", + "**Depth Anything 3** by ByteDance Research\n", + "\n", + "[📄 Paper](https://arxiv.org/abs/2511.10647) • [🌐 Project](https://depth-anything-3.github.io) • [🤗 Models](https://huggingface.co/collections/depth-anything/depth-anything-3)\n", + "\n", + "---\n", + "\n", + "**awesome-depth-anything-3** — Optimized fork with batching, caching & CLI\n", + "\n", + "[⭐ GitHub](https://github.com/Aedelon/awesome-depth-anything-3) • [📦 PyPI](https://pypi.org/project/awesome-depth-anything-3/)\n", + "\n", + "---\n", + "\n", + "Made with ❤️ by [Delanoe Pirard](https://github.com/Aedelon)\n", + "\n", + "
" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b4969de3c05ddd78b6175568a901d52a6f6158da --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,144 @@ +[build-system] +requires = ["hatchling>=1.25", "hatch-vcs>=0.4"] +build-backend = "hatchling.build" + +[project] +name = "awesome-depth-anything-3" +version = "0.0.0" +description = "Optimized wrapper for Depth Anything 3 - Metric depth, point clouds, camera poses and novel views from any images" +readme = "README.md" +requires-python = ">=3.10, <=3.13" +license = { text = "Apache-2.0" } +authors = [{ name = "Delanoe Pirard", email = "delanoe.pirard.pro@gmail.com" }] +keywords = [ + "depth-estimation", + "3d-reconstruction", + "computer-vision", + "pytorch", + "monocular-depth", + "multi-view", + "pose-estimation", + "point-cloud", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Image Processing", +] + +dependencies = [ + "torch>=2", + "torchvision", + "kornia>=0.7.0", + "einops", + "huggingface_hub", + "imageio", + "numpy<2", + "opencv-python", + "open3d", + "fastapi", + "uvicorn", + "requests", + "typer>=0.9.0,<0.13.0", + "pillow", + "omegaconf", + "evo", + "e3nn", + "moviepy==1.0.3", + "trimesh", + "plyfile", + "pillow_heif", + "safetensors", + "pycolmap", + "twine>=6.2.0", +] + +[project.optional-dependencies] +app = ["gradio==4.44.1", "huggingface_hub>=0.19,<1.0", "pillow>=9.0"] +dev = ["pre-commit", "pytest", "ruff"] +# CUDA acceleration packages (may require manual install steps) +xformers = ["xformers; platform_system!='Darwin'"] +gs = ["gsplat>=1.0.0; platform_system!='Darwin'"] +# Note: flash-attn package is optional. PyTorch >= 2.2 includes Flash Attention +# natively via F.scaled_dot_product_attention(). Only install flash-attn if you +# need the absolute latest optimizations: +# pip install flash-attn --no-build-isolation (requires CUDA toolkit) +# Convenience bundles +cuda = ["awesome-depth-anything-3[xformers,gs]"] +all = ["awesome-depth-anything-3[app,cuda]"] + + +[project.scripts] +da3 = "depth_anything_3.cli:app" + +[project.urls] +Homepage = "https://github.com/Aedelon/awesome-depth-anything-3" +Repository = "https://github.com/Aedelon/awesome-depth-anything-3" +Documentation = "https://github.com/Aedelon/awesome-depth-anything-3#readme" +Issues = "https://github.com/Aedelon/awesome-depth-anything-3/issues" +Changelog = "https://github.com/Aedelon/awesome-depth-anything-3/blob/main/CHANGELOG.md" +Upstream = "https://github.com/ByteDance-Seed/Depth-Anything-3" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.wheel] +packages = ["src/depth_anything_3"] + +[tool.hatch.build.targets.sdist] +include = [ + "/README.md", + "/pyproject.toml", + "/src/depth_anything_3", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.mypy] +plugins = ["jaxtyping.mypy_plugin"] + +[tool.black] +line-length = 99 +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] +include = '\.pyi?$' +exclude = ''' +/( + | \.git +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +known_third_party = ["bson","cruise","cv2","dataloader","diffusers","omegaconf","tensorflow","torch","torchvision","transformers","gsplat"] +known_first_party = ["common", "data", "models", "projects", "depth_anything_3"] +sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"] +skip_gitignore = true +line_length = 99 +no_lines_before="THIRDPARTY" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::UserWarning", +] + +[tool.ruff] +line-length = 99 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] +ignore = ["E501"] # Line too long (handled by formatter) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..420266337842d969acd8be407311f12ba9bd1f78 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,38 @@ +# Install this package from GitHub +git+https://github.com/Aedelon/awesome-depth-anything-3.git + +# Core dependencies - torch MUST be first for xformers +torch>=2 +torchvision +numpy<2 + +# ML/Vision libraries +einops +kornia>=0.7.0 +safetensors + +# Image/Video processing +pillow +pillow_heif +imageio +opencv-python +moviepy==1.0.3 + +# 3D and geometry +trimesh +plyfile +open3d +e3nn +evo +pycolmap + +# API and config +fastapi +uvicorn +requests +typer>=0.9.0,<0.13.0 +omegaconf + +# Gradio app +gradio>=5.50.0,<6.0 +huggingface_hub>=0.33.5,<2.0 \ No newline at end of file diff --git a/scripts/deploy_hf.sh b/scripts/deploy_hf.sh new file mode 100755 index 0000000000000000000000000000000000000000..4bc5b12fd9d40ce54a8b3dfd69a7d1f39433cbc3 --- /dev/null +++ b/scripts/deploy_hf.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +# +# Deploy to HuggingFace Spaces with LFS for binary files +# This script temporarily enables LFS, pushes to HF, then restores normal state + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +cd "$PROJECT_ROOT" + +# HuggingFace Spaces YAML front matter +HF_YAML='--- +title: Awesome Depth Anything 3 +emoji: 🌊 +colorFrom: blue +colorTo: purple +sdk: gradio +sdk_version: 5.50.0 +app_file: app.py +pinned: false +license: apache-2.0 +short_description: Metric 3D reconstruction from images/video +--- + +' + +echo "=== HuggingFace Deployment Script ===" + +# Save current HEAD +CURRENT_SHA=$(git rev-parse HEAD) +echo "Current commit: $CURRENT_SHA" + +# Step 1: Configure LFS for binary files +echo "" +echo "Step 1: Configuring Git LFS..." +cat > .gitattributes << 'EOF' +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text +EOF + +# Step 2: Create deployment branch +echo "" +echo "Step 2: Creating deployment branch..." +git checkout --orphan hf-deploy-temp 2>/dev/null || git checkout hf-deploy-temp + +# Reset to get clean state +git reset + +# Step 3: Add YAML to README +echo "" +echo "Step 3: Adding YAML front matter to README..." +cp README.md README.md.original +echo "$HF_YAML$(cat README.md.original)" > README.md + +# Step 4: Stage all files (LFS will handle binaries) +echo "" +echo "Step 4: Staging files with LFS..." +git add .gitattributes +git add -A + +# Step 5: Commit +echo "" +echo "Step 5: Committing..." +git commit -m "Deploy to HuggingFace Spaces" --no-verify || true + +# Step 6: Push to HuggingFace +echo "" +echo "Step 6: Pushing to HuggingFace Spaces..." +git push huggingface hf-deploy-temp:main --force + +# Step 7: Cleanup - return to main branch +echo "" +echo "Step 7: Cleaning up..." +git checkout main --force +git branch -D hf-deploy-temp 2>/dev/null || true + +# Restore original .gitattributes (no LFS) +cat > .gitattributes << 'EOF' +*.png !text !filter !merge !diff +*.jpg !text !filter !merge !diff +*.jpeg !text !filter !merge !diff +*.gif !text !filter !merge !diff +*.mp4 !text !filter !merge !diff +*.webm !text !filter !merge !diff +EOF + +echo "" +echo "=== Done! ===" +echo "HuggingFace updated with YAML metadata and LFS binaries." +echo "Local repo restored to normal state (no LFS)." diff --git a/src/depth_anything_3/api.py b/src/depth_anything_3/api.py new file mode 100644 index 0000000000000000000000000000000000000000..ae89dcc645e6b0dfb498e2b12a58e52499147e26 --- /dev/null +++ b/src/depth_anything_3/api.py @@ -0,0 +1,718 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Depth Anything 3 API module. + +This module provides the main API for Depth Anything 3, including model loading, +inference, and export capabilities. It supports both single and nested model architectures. +""" + +from __future__ import annotations + +import time +from typing import Optional, Sequence + +import numpy as np +import torch +import torch.nn as nn +from huggingface_hub import PyTorchModelHubMixin +from PIL import Image + +from depth_anything_3.cache import get_model_cache +from depth_anything_3.cfg import create_object, load_config +from depth_anything_3.registry import MODEL_REGISTRY +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.adaptive_batching import ( + AdaptiveBatchConfig, + AdaptiveBatchSizeCalculator, + adaptive_batch_iterator, + estimate_max_batch_size, +) +from depth_anything_3.utils.export import export +from depth_anything_3.utils.geometry import affine_inverse +from depth_anything_3.utils.io.gpu_input_processor import GPUInputProcessor +from depth_anything_3.utils.io.input_processor import InputProcessor +from depth_anything_3.utils.io.output_processor import OutputProcessor +from depth_anything_3.utils.logger import logger +from depth_anything_3.utils.pose_align import align_poses_umeyama + +torch.backends.cudnn.benchmark = False +# logger.info("CUDNN Benchmark Disabled") + +SAFETENSORS_NAME = "model.safetensors" +CONFIG_NAME = "config.json" + + +class DepthAnything3(nn.Module, PyTorchModelHubMixin): + """ + Depth Anything 3 main API class. + + This class provides a high-level interface for depth estimation using Depth Anything 3. + It supports both single and nested model architectures with metric scaling capabilities. + + Features: + - Hugging Face Hub integration via PyTorchModelHubMixin + - Support for multiple model presets (vitb, vitg, nested variants) + - Automatic mixed precision inference + - Export capabilities for various formats (GLB, PLY, NPZ, etc.) + - Camera pose estimation and metric depth scaling + + Usage: + # Load from Hugging Face Hub + model = DepthAnything3.from_pretrained("huggingface/model-name") + + # Or create with specific preset + model = DepthAnything3(preset="vitg") + + # Run inference + prediction = model.inference(images, export_dir="output", export_format="glb") + """ + + _commit_hash: str | None = None # Set by mixin when loading from Hub + + def __init__(self, model_name: str = "da3-large", device: str | torch.device | None = None, use_cache: bool = True, **kwargs): + """ + Initialize DepthAnything3 with specified preset. + + Args: + model_name: The name of the model preset to use. + Examples: 'da3-giant', 'da3-large', 'da3metric-large', 'da3nested-giant-large'. + device: Target device ('cuda', 'mps', 'cpu'). If None, auto-detect. + use_cache: Whether to use model caching (default: True). + Set to False to force reload model from disk. + **kwargs: Additional keyword arguments (currently unused). + """ + super().__init__() + self.model_name = model_name + self.use_cache = use_cache + + # Determine device + if device is None: + device = self._auto_detect_device() + self.device = torch.device(device) if isinstance(device, str) else device + + # Load model configuration + self.config = load_config(MODEL_REGISTRY[self.model_name]) + + # Build or retrieve model from cache + if use_cache: + cache = get_model_cache() + self.model = cache.get( + model_name=self.model_name, + device=self.device, + loader_fn=lambda: self._create_model() + ) + else: + logger.info(f"Model cache disabled, loading {self.model_name} from disk") + self.model = self._create_model() + + # Ensure model is on correct device and in eval mode + self.model = self.model.to(self.device) + self.model.eval() + + # Initialize processors + # Use GPUInputProcessor for CUDA/MPS devices to enable GPU ops + # Note: NVJPEG decoding is specific to CUDA, MPS will use optimized CPU decoding + GPU resize + if self.device.type in ("cuda", "mps"): + self.input_processor = GPUInputProcessor(device=self.device) + decoding_info = "NVJPEG support enabled" if self.device.type == "cuda" else "TorchVision decoding" + logger.info(f"Using GPUInputProcessor ({decoding_info} on {self.device})") + else: + self.input_processor = InputProcessor() + logger.info("Using standard InputProcessor (optimized CPU pipeline)") + + self.output_processor = OutputProcessor() + + def _auto_detect_device(self) -> torch.device: + """Auto-detect best available device.""" + if torch.cuda.is_available(): + return torch.device("cuda") + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return torch.device("mps") + else: + return torch.device("cpu") + + def _create_model(self) -> nn.Module: + """Create and return new model instance on correct device.""" + model = create_object(self.config) + model = model.to(self.device) # Move to device before caching + model.eval() + return model + + @torch.inference_mode() + def forward( + self, + image: torch.Tensor, + extrinsics: torch.Tensor | None = None, + intrinsics: torch.Tensor | None = None, + export_feat_layers: list[int] | None = None, + infer_gs: bool = False, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + ) -> dict[str, torch.Tensor]: + """ + Forward pass through the model. + + Args: + image: Input batch with shape ``(B, N, 3, H, W)`` on the model device. + extrinsics: Optional camera extrinsics with shape ``(B, N, 4, 4)``. + intrinsics: Optional camera intrinsics with shape ``(B, N, 3, 3)``. + export_feat_layers: Layer indices to return intermediate features for. + infer_gs: Enable Gaussian Splatting branch. + use_ray_pose: Use ray-based pose estimation instead of camera decoder. + ref_view_strategy: Strategy for selecting reference view from multiple views. + + Returns: + Dictionary containing model predictions + """ + with torch.no_grad(): + # MPS doesn't support autocast well - use float32 for stability + if image.device.type == "mps": + return self.model( + image, extrinsics, intrinsics, export_feat_layers, infer_gs, use_ray_pose, ref_view_strategy + ) + else: + # CUDA: use autocast for performance + autocast_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 + with torch.autocast(device_type=image.device.type, dtype=autocast_dtype): + return self.model( + image, extrinsics, intrinsics, export_feat_layers, infer_gs, use_ray_pose, ref_view_strategy + ) + + def inference( + self, + image: list[np.ndarray | Image.Image | str], + extrinsics: np.ndarray | None = None, + intrinsics: np.ndarray | None = None, + align_to_input_ext_scale: bool = True, + infer_gs: bool = False, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + render_exts: np.ndarray | None = None, + render_ixts: np.ndarray | None = None, + render_hw: tuple[int, int] | None = None, + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + export_dir: str | None = None, + export_format: str = "mini_npz", + export_feat_layers: Sequence[int] | None = None, + # GLB export parameters + conf_thresh_percentile: float = 40.0, + num_max_points: int = 1_000_000, + show_cameras: bool = True, + # Feat_vis export parameters + feat_vis_fps: int = 15, + # Other export parameters, e.g., gs_ply, gs_video + export_kwargs: Optional[dict] = {}, + ) -> Prediction: + """ + Run inference on input images. + + Args: + image: List of input images (numpy arrays, PIL Images, or file paths) + extrinsics: Camera extrinsics (N, 4, 4) + intrinsics: Camera intrinsics (N, 3, 3) + align_to_input_ext_scale: whether to align the input pose scale to the prediction + infer_gs: Enable the 3D Gaussian branch (needed for `gs_ply`/`gs_video` exports) + use_ray_pose: Use ray-based pose estimation instead of camera decoder (default: False) + ref_view_strategy: Strategy for selecting reference view from multiple views. + Options: "first", "middle", "saddle_balanced", "saddle_sim_range". + Default: "saddle_balanced". For single view input (S ≤ 2), no reordering is performed. + render_exts: Optional render extrinsics for Gaussian video export + render_ixts: Optional render intrinsics for Gaussian video export + render_hw: Optional render resolution for Gaussian video export + process_res: Processing resolution + process_res_method: Resize method for processing + export_dir: Directory to export results + export_format: Export format (mini_npz, npz, glb, ply, gs, gs_video) + export_feat_layers: Layer indices to export intermediate features from + conf_thresh_percentile: [GLB] Lower percentile for adaptive confidence threshold (default: 40.0) # noqa: E501 + num_max_points: [GLB] Maximum number of points in the point cloud (default: 1,000,000) + show_cameras: [GLB] Show camera wireframes in the exported scene (default: True) + feat_vis_fps: [FEAT_VIS] Frame rate for output video (default: 15) + export_kwargs: additional arguments to export functions. + + Returns: + Prediction object containing depth maps and camera parameters + """ + if "gs" in export_format: + assert infer_gs, "must set `infer_gs=True` to perform gs-related export." + + if "colmap" in export_format: + assert isinstance(image[0], str), "`image` must be image paths for COLMAP export." + + # Preprocess images + imgs_cpu, extrinsics, intrinsics = self._preprocess_inputs( + image, extrinsics, intrinsics, process_res, process_res_method + ) + + # Prepare tensors for model + imgs, ex_t, in_t = self._prepare_model_inputs(imgs_cpu, extrinsics, intrinsics) + + # Normalize extrinsics + ex_t_norm = self._normalize_extrinsics(ex_t.clone() if ex_t is not None else None) + + # Run model forward pass + export_feat_layers = list(export_feat_layers) if export_feat_layers is not None else [] + + raw_output = self._run_model_forward( + imgs, ex_t_norm, in_t, export_feat_layers, infer_gs, use_ray_pose, ref_view_strategy + ) + + # Convert raw output to prediction + prediction = self._convert_to_prediction(raw_output) + + # Align prediction to extrinsincs + prediction = self._align_to_input_extrinsics_intrinsics( + extrinsics, intrinsics, prediction, align_to_input_ext_scale + ) + + # Add processed images for visualization + prediction = self._add_processed_images(prediction, imgs_cpu) + + # Export if requested + if export_dir is not None: + + if "gs" in export_format: + if infer_gs and "gs_video" not in export_format: + export_format = f"{export_format}-gs_video" + if "gs_video" in export_format: + if "gs_video" not in export_kwargs: + export_kwargs["gs_video"] = {} + export_kwargs["gs_video"].update( + { + "extrinsics": render_exts, + "intrinsics": render_ixts, + "out_image_hw": render_hw, + } + ) + # Add GLB export parameters + if "glb" in export_format: + if "glb" not in export_kwargs: + export_kwargs["glb"] = {} + export_kwargs["glb"].update( + { + "conf_thresh_percentile": conf_thresh_percentile, + "num_max_points": num_max_points, + "show_cameras": show_cameras, + } + ) + # Add Feat_vis export parameters + if "feat_vis" in export_format: + if "feat_vis" not in export_kwargs: + export_kwargs["feat_vis"] = {} + export_kwargs["feat_vis"].update( + { + "fps": feat_vis_fps, + } + ) + # Add COLMAP export parameters + if "colmap" in export_format: + if "colmap" not in export_kwargs: + export_kwargs["colmap"] = {} + export_kwargs["colmap"].update( + { + "image_paths": image, + "conf_thresh_percentile": conf_thresh_percentile, + "process_res_method": process_res_method, + } + ) + self._export_results(prediction, export_format, export_dir, **export_kwargs) + + return prediction + + def _preprocess_inputs( + self, + image: list[np.ndarray | Image.Image | str], + extrinsics: np.ndarray | None = None, + intrinsics: np.ndarray | None = None, + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]: + """Preprocess input images using input processor.""" + start_time = time.time() + + # Determine normalization strategy: + # 1. Hybrid (CPU Proc + GPU Device): Skip CPU norm (return uint8), norm on GPU later. + # 2. GPU Proc (NVJPEG/Kornia): Perform norm on GPU immediately. + # 3. Standard CPU: Perform norm on CPU. + + perform_norm = True + if self.device.type in ("cuda", "mps") and not isinstance(self.input_processor, GPUInputProcessor): + perform_norm = False + + imgs_cpu, extrinsics, intrinsics = self.input_processor( + image, + extrinsics.copy() if extrinsics is not None else None, + intrinsics.copy() if intrinsics is not None else None, + process_res, + process_res_method, + perform_normalization=perform_norm, + ) + end_time = time.time() + logger.info( + "Processed Images Done taking", + end_time - start_time, + "seconds. Shape: ", + imgs_cpu.shape, + ) + return imgs_cpu, extrinsics, intrinsics + + def _prepare_model_inputs( + self, + imgs_cpu: torch.Tensor, + extrinsics: torch.Tensor | None, + intrinsics: torch.Tensor | None, + ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]: + """ + Prepare tensors for model input with optimized device transfer. + """ + device = self._get_model_device() + + # 1. Handle Image Tensor + # Compare device types (handles cuda:0 vs cuda comparison) + imgs_on_target_device = (imgs_cpu.device.type == device.type) + if imgs_on_target_device: + # Case A: Already on correct device (GPUInputProcessor) + # Ensure correct shape: (B, S, C, H, W) where B=1 + imgs = imgs_cpu + if imgs.dim() == 3: + # Single image (C, H, W) -> (1, 1, C, H, W) + imgs = imgs.unsqueeze(0).unsqueeze(0) + elif imgs.dim() == 4: + # Batch of images (N, C, H, W) -> (1, N, C, H, W) + imgs = imgs.unsqueeze(0) + # dim() == 5 means already correct shape + if imgs.dtype == torch.uint8: + # Should not happen with GPUInputProcessor default, but safety fallback + imgs = imgs.float() / 255.0 + imgs = InputProcessor.normalize_tensor( + imgs, + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ) + else: + # Case B & C: Needs transfer from CPU + if imgs_cpu.dtype == torch.uint8: + # Hybrid mode: uint8 -> GPU -> float -> normalize + if device.type == "cuda": + imgs_cpu = imgs_cpu.pin_memory() + + imgs = imgs_cpu.to(device, non_blocking=True).float() / 255.0 + imgs = InputProcessor.normalize_tensor( + imgs, + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ) + imgs = imgs[None] # Add batch dimension (1, N, 3, H, W) + else: + # Standard mode: float -> GPU + if device.type == "cuda": + imgs_cpu = imgs_cpu.pin_memory() + imgs = imgs_cpu.to(device, non_blocking=True)[None].float() + + # Convert camera parameters to tensors with non-blocking transfer + ex_t = ( + extrinsics.pin_memory().to(device, non_blocking=True)[None].float() + if extrinsics is not None and device.type == "cuda" and extrinsics.device.type == "cpu" + else extrinsics.to(device, non_blocking=True)[None].float() + if extrinsics is not None and extrinsics.device != device + else extrinsics[None].float() + if extrinsics is not None + else None + ) + in_t = ( + intrinsics.pin_memory().to(device, non_blocking=True)[None].float() + if intrinsics is not None and device.type == "cuda" and intrinsics.device.type == "cpu" + else intrinsics.to(device, non_blocking=True)[None].float() + if intrinsics is not None and intrinsics.device != device + else intrinsics[None].float() + if intrinsics is not None + else None + ) + + return imgs, ex_t, in_t + + def _normalize_extrinsics(self, ex_t: torch.Tensor | None) -> torch.Tensor | None: + """Normalize extrinsics""" + if ex_t is None: + return None + transform = affine_inverse(ex_t[:, :1]) + ex_t_norm = ex_t @ transform + c2ws = affine_inverse(ex_t_norm) + translations = c2ws[..., :3, 3] + dists = translations.norm(dim=-1) + median_dist = torch.median(dists) + median_dist = torch.clamp(median_dist, min=1e-1) + ex_t_norm[..., :3, 3] = ex_t_norm[..., :3, 3] / median_dist + return ex_t_norm + + def _align_to_input_extrinsics_intrinsics( + self, + extrinsics: torch.Tensor | None, + intrinsics: torch.Tensor | None, + prediction: Prediction, + align_to_input_ext_scale: bool = True, + ransac_view_thresh: int = 10, + ) -> Prediction: + """Align depth map to input extrinsics""" + if extrinsics is None: + return prediction + prediction.intrinsics = intrinsics.numpy() + _, _, scale, aligned_extrinsics = align_poses_umeyama( + prediction.extrinsics, + extrinsics.numpy(), + ransac=len(extrinsics) >= ransac_view_thresh, + return_aligned=True, + random_state=42, + ) + if align_to_input_ext_scale: + prediction.extrinsics = extrinsics[..., :3, :].numpy() + prediction.depth /= scale + else: + prediction.extrinsics = aligned_extrinsics + return prediction + + def _run_model_forward( + self, + imgs: torch.Tensor, + ex_t: torch.Tensor | None, + in_t: torch.Tensor | None, + export_feat_layers: Sequence[int] | None = None, + infer_gs: bool = False, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + ) -> dict[str, torch.Tensor]: + """Run model forward pass.""" + device = imgs.device + need_sync = device.type == "cuda" + if need_sync: + torch.cuda.synchronize(device) + start_time = time.time() + feat_layers = list(export_feat_layers) if export_feat_layers is not None else None + output = self.forward(imgs, ex_t, in_t, feat_layers, infer_gs, use_ray_pose, ref_view_strategy) + if need_sync: + torch.cuda.synchronize(device) + end_time = time.time() + logger.info(f"Model Forward Pass Done. Time: {end_time - start_time} seconds") + return output + + def _convert_to_prediction(self, raw_output: dict[str, torch.Tensor]) -> Prediction: + """Convert raw model output to Prediction object.""" + start_time = time.time() + output = self.output_processor(raw_output) + end_time = time.time() + logger.info(f"Conversion to Prediction Done. Time: {end_time - start_time} seconds") + return output + + def _add_processed_images(self, prediction: Prediction, imgs_cpu: torch.Tensor) -> Prediction: + """Add processed images to prediction for visualization.""" + # Convert from (N, 3, H, W) to (N, H, W, 3) + processed_imgs = imgs_cpu.permute(0, 2, 3, 1).cpu().numpy() # (N, H, W, 3) + + if imgs_cpu.dtype == torch.uint8: + # Already uint8, no need to denormalize + pass + else: + # Denormalize from ImageNet normalization + mean = np.array([0.485, 0.456, 0.406]) + std = np.array([0.229, 0.224, 0.225]) + processed_imgs = processed_imgs * std + mean + processed_imgs = np.clip(processed_imgs, 0, 1) + processed_imgs = (processed_imgs * 255).astype(np.uint8) + + prediction.processed_images = processed_imgs + return prediction + + def _export_results( + self, prediction: Prediction, export_format: str, export_dir: str, **kwargs + ) -> None: + """Export results to specified format and directory.""" + start_time = time.time() + export(prediction, export_format, export_dir, **kwargs) + end_time = time.time() + logger.info(f"Export Results Done. Time: {end_time - start_time} seconds") + + def _get_model_device(self) -> torch.device: + """ + Get the device where the model is located. + + Returns: + Device where the model parameters are located + + Raises: + ValueError: If no tensors are found in the model + """ + if self.device is not None: + return self.device + + # Find device from parameters + for param in self.parameters(): + self.device = param.device + return param.device + + # Find device from buffers + for buffer in self.buffers(): + self.device = buffer.device + return buffer.device + + raise ValueError("No tensor found in model") + + # ========================================================================= + # Adaptive Batching Methods + # ========================================================================= + + def batch_inference( + self, + images: list[np.ndarray | Image.Image | str], + process_res: int = 504, + batch_size: int | str = "auto", + max_batch_size: int = 64, + target_memory_utilization: float = 0.85, + progress_callback: callable | None = None, + ) -> list[Prediction]: + """ + Run inference on multiple images with adaptive batching. + + This method automatically determines optimal batch sizes based on + available GPU memory, maximizing throughput while preventing OOM errors. + + Args: + images: List of input images (numpy arrays, PIL Images, or file paths) + process_res: Processing resolution (default: 504) + batch_size: Batch size or "auto" for adaptive batching (default: "auto") + max_batch_size: Maximum batch size when using adaptive batching (default: 64) + target_memory_utilization: Target GPU memory usage 0.0-1.0 (default: 0.85) + progress_callback: Optional callback(processed, total) for progress updates + + Returns: + List of Prediction objects, one per batch + + Example: + >>> model = DepthAnything3(model_name="da3-large") + >>> images = ["img1.jpg", "img2.jpg", ..., "img100.jpg"] + >>> + >>> # Adaptive batching (recommended) + >>> results = model.batch_inference(images, process_res=518) + >>> + >>> # Fixed batch size + >>> results = model.batch_inference(images, batch_size=4) + >>> + >>> # With progress callback + >>> def on_progress(done, total): + ... print(f"Processed {done}/{total}") + >>> results = model.batch_inference(images, progress_callback=on_progress) + """ + import gc + + num_images = len(images) + if num_images == 0: + return [] + + results: list[Prediction] = [] + + # Determine batch size + if batch_size == "auto": + config = AdaptiveBatchConfig( + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + calculator = AdaptiveBatchSizeCalculator( + model_name=self.model_name, + device=self.device, + config=config, + ) + + for batch_info in adaptive_batch_iterator(images, calculator, process_res): + # Run inference on this batch + prediction = self.inference( + image=batch_info.items, + process_res=process_res, + ) + results.append(prediction) + + # Progress callback + if progress_callback: + progress_callback(batch_info.end_idx, num_images) + + # Memory cleanup between batches + if not batch_info.is_last: + gc.collect() + if self.device.type == "cuda": + torch.cuda.empty_cache() + elif self.device.type == "mps": + torch.mps.empty_cache() + + # Update profiling data for better estimates + if calculator.config.enable_profiling and self.device.type == "cuda": + memory_used = torch.cuda.max_memory_allocated(self.device) / (1024 * 1024) + calculator.update_from_profiling( + batch_size=batch_info.batch_size, + memory_used_mb=memory_used, + process_res=process_res, + ) + torch.cuda.reset_peak_memory_stats(self.device) + + else: + # Fixed batch size + fixed_batch_size = int(batch_size) + for i in range(0, num_images, fixed_batch_size): + end_idx = min(i + fixed_batch_size, num_images) + batch_images = images[i:end_idx] + + prediction = self.inference( + image=batch_images, + process_res=process_res, + ) + results.append(prediction) + + if progress_callback: + progress_callback(end_idx, num_images) + + # Memory cleanup + if end_idx < num_images: + gc.collect() + if self.device.type == "cuda": + torch.cuda.empty_cache() + elif self.device.type == "mps": + torch.mps.empty_cache() + + return results + + def get_optimal_batch_size( + self, + process_res: int = 504, + target_utilization: float = 0.85, + ) -> int: + """ + Get the optimal batch size for current GPU memory state. + + Args: + process_res: Processing resolution (default: 504) + target_utilization: Target GPU memory usage 0.0-1.0 (default: 0.85) + + Returns: + Recommended batch size + + Example: + >>> model = DepthAnything3(model_name="da3-large") + >>> batch_size = model.get_optimal_batch_size(process_res=518) + >>> print(f"Optimal batch size: {batch_size}") + """ + return estimate_max_batch_size( + model_name=self.model_name, + device=self.device, + process_res=process_res, + target_utilization=target_utilization, + ) diff --git a/src/depth_anything_3/app/css_and_html.py b/src/depth_anything_3/app/css_and_html.py new file mode 100644 index 0000000000000000000000000000000000000000..44db1c4855d9ab100cae87a255fffe766f851a29 --- /dev/null +++ b/src/depth_anything_3/app/css_and_html.py @@ -0,0 +1,623 @@ +# flake8: noqa: E501 + +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +CSS and HTML content for the Depth Anything 3 Gradio application. +This module contains all the CSS styles and HTML content blocks +used in the Gradio interface. +""" + +# CSS Styles for the Gradio interface +# Color palette: +# - Primary: #2563EB (Modern Blue) +# - Secondary: #14B8A6 (Vibrant Teal) +# - Accent: #F97316 (Electric Orange) +# - Neutrals: #F9FAFB to #111827 +GRADIO_CSS = """ +/* Add Font Awesome CDN with all styles including brands and colors */ +@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); + +/* Force light mode */ +html, body, .gradio-container { + color-scheme: light !important; +} + +/* CSS Custom Properties for theming */ +:root { + --primary: #2563EB; + --primary-light: #3B82F6; + --primary-dark: #1D4ED8; + --secondary: #14B8A6; + --secondary-light: #2DD4BF; + --secondary-dark: #0D9488; + --accent: #F97316; + --accent-light: #FB923C; + --accent-dark: #EA580C; + --neutral-50: #F9FAFB; + --neutral-100: #F3F4F6; + --neutral-200: #E5E7EB; + --neutral-300: #D1D5DB; + --neutral-400: #9CA3AF; + --neutral-500: #6B7280; + --neutral-600: #4B5563; + --neutral-700: #374151; + --neutral-800: #1F2937; + --neutral-900: #111827; +} + +/* Add custom styles for colored icons */ +.fa-color-blue { + color: var(--primary); +} + +.fa-color-purple { + color: #8B5CF6; +} + +.fa-color-cyan { + color: var(--secondary); +} + +.fa-color-green { + color: #10B981; +} + +.fa-color-yellow { + color: var(--accent); +} + +.fa-color-red { + color: #EF4444; +} + +.link-btn { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + padding: 12px 24px; + border-radius: 50px; + font-weight: 500; + transition: all 0.3s ease; +} + +/* Dark mode theme */ +@media (prefers-color-scheme: dark) { + html, body { + background: var(--neutral-800); + color: var(--neutral-50); + } + + .gradio-container { + background: var(--neutral-800); + color: var(--neutral-50); + } + + .link-btn { + background: rgba(20, 184, 166, 0.2); + color: white; + border: 1px solid rgba(20, 184, 166, 0.4); + } + + .link-btn:hover { + background: rgba(20, 184, 166, 0.35); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(20, 184, 166, 0.25); + } + + .tech-bg { + background: linear-gradient(135deg, var(--neutral-900), var(--neutral-800)); + position: relative; + overflow: hidden; + } + + .tech-bg::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 80%, rgba(37, 99, 235, 0.15) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(20, 184, 166, 0.15) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(249, 115, 22, 0.1) 0%, transparent 50%); + animation: techPulse 8s ease-in-out infinite; + } + + .gradio-container .panel, + .gradio-container .block, + .gradio-container .form { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(20, 184, 166, 0.2); + border-radius: 10px; + } + + .gradio-container * { + color: var(--neutral-50); + } + + .gradio-container label { + color: var(--neutral-200); + } + + .gradio-container .markdown { + color: var(--neutral-200); + } +} + +/* Light mode theme */ +@media (prefers-color-scheme: light) { + html, body { + background: var(--neutral-50); + color: var(--neutral-800); + } + + .gradio-container { + background: var(--neutral-50); + color: var(--neutral-800); + } + + .tech-bg { + background: linear-gradient(135deg, var(--neutral-50), var(--neutral-100)); + position: relative; + overflow: hidden; + } + + .link-btn { + background: rgba(20, 184, 166, 0.12); + color: var(--neutral-700); + border: 1px solid rgba(20, 184, 166, 0.3); + } + + .link-btn:hover { + background: rgba(20, 184, 166, 0.2); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(20, 184, 166, 0.2); + } + + .tech-bg::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 80%, rgba(37, 99, 235, 0.08) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(20, 184, 166, 0.08) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(249, 115, 22, 0.06) 0%, transparent 50%); + animation: techPulse 8s ease-in-out infinite; + } + + .gradio-container .panel, + .gradio-container .block, + .gradio-container .form { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(20, 184, 166, 0.2); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + } + + .gradio-container * { + color: var(--neutral-800); + } + + .gradio-container label { + color: var(--neutral-600); + } + + .gradio-container .markdown { + color: var(--neutral-600); + } +} + + + + +@keyframes techPulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 0.8; } +} + +/* Custom log with tech gradient */ +.custom-log * { + font-style: italic; + font-size: 22px !important; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + background-size: 400% 400%; + -webkit-background-clip: text; + background-clip: text; + font-weight: bold !important; + color: transparent !important; + text-align: center !important; + animation: techGradient 3s ease infinite; +} + +@keyframes techGradient { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +@keyframes metricPulse { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +@keyframes pointcloudPulse { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +@keyframes camerasPulse { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +@keyframes gaussiansPulse { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +/* Special colors for key terms - Global styles with gradient animations */ +.metric-text { + background: linear-gradient(135deg, #10B981, #059669); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent !important; + font-weight: 700; + animation: metricPulse 3s ease infinite; +} + +.pointcloud-text { + background: linear-gradient(135deg, #10B981, #059669); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent !important; + font-weight: 700; + animation: pointcloudPulse 3s ease infinite; +} + +.cameras-text { + background: linear-gradient(135deg, #F97316, #EA580C); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent !important; + font-weight: 700; + animation: camerasPulse 3s ease infinite; +} + +.gaussians-text { + background: linear-gradient(135deg, #2563EB, #1D4ED8); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent !important; + font-weight: 700; + animation: gaussiansPulse 3s ease infinite; +} + +.example-log * { + font-style: italic; + font-size: 16px !important; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + -webkit-background-clip: text; + background-clip: text; + color: transparent !important; +} + +#my_radio .wrap { + display: flex; + flex-wrap: nowrap; + justify-content: center; + align-items: center; +} + +#my_radio .wrap label { + display: flex; + width: 50%; + justify-content: center; + align-items: center; + margin: 0; + padding: 10px 0; + box-sizing: border-box; +} + +/* Align navigation buttons with dropdown bottom */ +.navigation-row { + display: flex !important; + align-items: flex-end !important; + gap: 8px !important; +} + +.navigation-row > div:nth-child(1), +.navigation-row > div:nth-child(3) { + align-self: flex-end !important; +} + +.navigation-row > div:nth-child(2) { + flex: 1 !important; +} + +/* Make thumbnails clickable with pointer cursor */ +.clickable-thumbnail img { + cursor: pointer !important; +} + +.clickable-thumbnail:hover img { + cursor: pointer !important; + opacity: 0.8; + transition: opacity 0.3s ease; +} + +/* Make thumbnail containers narrower horizontally */ +.clickable-thumbnail { + padding: 5px 2px !important; + margin: 0 2px !important; +} + +.clickable-thumbnail .image-container { + margin: 0 !important; + padding: 0 !important; +} + +.scene-info { + text-align: center !important; + padding: 5px 2px !important; + margin: 0 !important; +} +""" + + +def get_header_html(logo_base64=None): + """ + Generate the main header HTML with logo and title. + + Args: + logo_base64 (str, optional): Base64 encoded logo image + + Returns: + str: HTML string for the header + """ + return """ +
+
+

+ Depth Anything 3 +

+

+ Recovering the Visual Space from Any Views +

+
+ + Project Page + + + Paper + + + Awesome Optimized Fork + + + Original + +
+
+
+ + + + """ + + +def get_description_html(): + """ + Generate the main description and getting started HTML. + + Returns: + str: HTML string for the description + """ + return """ +
+

+ What This Demo Does +

+
+

+ Upload images or videosGet Metric Point Clouds, Cameras and Novel ViewsExplore in 3D +

+
+ +
+

+ Tip: Landscape-oriented images or videos are preferred for best 3D recovering. +

+
+
+ + + """ + + +def get_acknowledgements_html(): + """ + Generate the acknowledgements section HTML. + + Returns: + str: HTML string for the acknowledgements + """ + return """ +
+

+ Research Credits & Acknowledgments +

+ +
+ +
+

Original Research

+

+ + Depth Anything 3 + +

+
+ + +
+

Previous Versions

+
+

+ + Depth-Anything + +

+ +

+ + Depth-Anything-V2 + +

+
+
+
+ + +
+

+ HF demo adapted from Map Anything +

+
+
+ """ + + +def get_gradio_theme(): + """ + Get the configured Gradio theme with modern teal/blue colors. + + Color palette: + - Primary: Teal (#14B8A6) + - Secondary: Blue (#2563EB) + - Accent: Orange (#F97316) + - Neutrals: Clean grays (#F9FAFB to #111827) + + Returns: + gr.themes.Base: Configured Gradio theme + """ + import gradio as gr + + return gr.themes.Base( + # Primary hue: Teal + primary_hue=gr.themes.Color( + c50="#F0FDFA", + c100="#CCFBF1", + c200="#99F6E4", + c300="#5EEAD4", + c400="#2DD4BF", + c500="#14B8A6", + c600="#0D9488", + c700="#0F766E", + c800="#115E59", + c900="#134E4A", + c950="#042F2E", + ), + # Secondary hue: Blue + secondary_hue=gr.themes.Color( + c50="#EFF6FF", + c100="#DBEAFE", + c200="#BFDBFE", + c300="#93C5FD", + c400="#60A5FA", + c500="#3B82F6", + c600="#2563EB", + c700="#1D4ED8", + c800="#1E40AF", + c900="#1E3A8A", + c950="#172554", + ), + # Neutral hue: Clean grays + neutral_hue=gr.themes.Color( + c50="#F9FAFB", + c100="#F3F4F6", + c200="#E5E7EB", + c300="#D1D5DB", + c400="#9CA3AF", + c500="#6B7280", + c600="#4B5563", + c700="#374151", + c800="#1F2937", + c900="#111827", + c950="#030712", + ), + ) + + +# Measure tab instructions HTML +MEASURE_INSTRUCTIONS_HTML = """ +### Click points on the image to compute distance. +> Metric scale estimation is difficult on aerial/drone images. +""" diff --git a/src/depth_anything_3/app/gradio_app.py b/src/depth_anything_3/app/gradio_app.py new file mode 100644 index 0000000000000000000000000000000000000000..981a138dc625b394fd1820b48bcea0595cbad4c0 --- /dev/null +++ b/src/depth_anything_3/app/gradio_app.py @@ -0,0 +1,743 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Refactored Gradio App for Depth Anything 3. + +This is the main application file that orchestrates all components. +The original functionality has been split into modular components for better maintainability. +""" + +import argparse +import os +from typing import Any, Dict, List + +import gradio as gr + +from depth_anything_3.app.css_and_html import GRADIO_CSS, get_gradio_theme +from depth_anything_3.app.modules.event_handlers import EventHandlers +from depth_anything_3.app.modules.ui_components import UIComponents + +# Set environment variables +os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" + + +class DepthAnything3App: + """ + Main application class for Depth Anything 3 Gradio app. + """ + + def __init__(self, model_dir: str = None, workspace_dir: str = None, gallery_dir: str = None): + """ + Initialize the application. + + Args: + model_dir: Path to the model directory + workspace_dir: Path to the workspace directory + gallery_dir: Path to the gallery directory + """ + self.model_dir = model_dir + self.workspace_dir = workspace_dir + self.gallery_dir = gallery_dir + + # Set environment variables for directories + if self.model_dir: + os.environ["DA3_MODEL_DIR"] = self.model_dir + if self.workspace_dir: + os.environ["DA3_WORKSPACE_DIR"] = self.workspace_dir + if self.gallery_dir: + os.environ["DA3_GALLERY_DIR"] = self.gallery_dir + + self.event_handlers = EventHandlers() + self.ui_components = UIComponents() + + def cache_examples( + self, + show_cam: bool = True, + filter_black_bg: bool = False, + filter_white_bg: bool = False, + save_percentage: float = 20.0, + num_max_points: int = 1000, + cache_gs_tag: str = "", + gs_trj_mode: str = "smooth", + gs_video_quality: str = "low", + ) -> None: + """ + Pre-cache all example scenes at startup. + + Args: + show_cam: Whether to show camera in visualization + filter_black_bg: Whether to filter black background + filter_white_bg: Whether to filter white background + save_percentage: Filter percentage for point cloud + num_max_points: Maximum number of points + cache_gs_tag: Tag to match scene names for high-res+3DGS caching (e.g., "dl3dv") + gs_trj_mode: Trajectory mode for 3DGS + gs_video_quality: Video quality for 3DGS + """ + from depth_anything_3.app.modules.utils import get_scene_info + + examples_dir = os.path.join(self.workspace_dir, "examples") + if not os.path.exists(examples_dir): + print(f"Examples directory not found: {examples_dir}") + return + + scenes = get_scene_info(examples_dir) + if not scenes: + print("No example scenes found to cache.") + return + + print(f"\n{'='*60}") + print(f"Caching {len(scenes)} example scenes...") + print(f"{'='*60}\n") + + for i, scene in enumerate(scenes, 1): + scene_name = scene["name"] + + # Check if scene name matches the gs tag for high-res+3DGS caching + use_high_res_gs = cache_gs_tag and cache_gs_tag.lower() in scene_name.lower() + + if use_high_res_gs: + print(f"[{i}/{len(scenes)}] Caching scene: {scene_name} (HIGH-RES + 3DGS)") + print(f" - Number of images: {scene['num_images']}") + print(f" - Matched tag: '{cache_gs_tag}' - using high_res + 3DGS") + else: + print(f"[{i}/{len(scenes)}] Caching scene: {scene_name} (LOW-RES)") + print(f" - Number of images: {scene['num_images']}") + + try: + # Load example scene + _, target_dir, _, _, _, _, _, _, _ = self.event_handlers.load_example_scene( + scene_name + ) + + if target_dir and target_dir != "None": + # Run reconstruction with appropriate settings + print(" - Running reconstruction...") + result = self.event_handlers.gradio_demo( + target_dir=target_dir, + show_cam=show_cam, + filter_black_bg=filter_black_bg, + filter_white_bg=filter_white_bg, + process_res_method="high_res" if use_high_res_gs else "low_res", + save_percentage=save_percentage, + num_max_points=num_max_points, + infer_gs=use_high_res_gs, + ref_view_strategy="saddle_balanced", + gs_trj_mode=gs_trj_mode, + gs_video_quality=gs_video_quality, + ) + + # Check if successful + if result[0] is not None: # reconstruction_output + print(f" ✓ Scene '{scene_name}' cached successfully") + else: + print(f" ✗ Scene '{scene_name}' caching failed: {result[1]}") + else: + print(f" ✗ Scene '{scene_name}' loading failed") + + except Exception as e: + print(f" ✗ Error caching scene '{scene_name}': {str(e)}") + + print() + + print("=" * 60) + print("Example scene caching completed!") + print("=" * 60 + "\n") + + def create_app(self) -> gr.Blocks: + """ + Create and configure the Gradio application. + + Returns: + Configured Gradio Blocks interface + """ + # Get theme and CSS + self._theme = get_gradio_theme() + self._css = GRADIO_CSS + + with gr.Blocks(theme=self._theme, css=self._css) as demo: + # State variables for the tabbed interface + is_example = gr.Textbox(label="is_example", visible=False, value="None") + processed_data_state = gr.State(value=None) + measure_points_state = gr.State(value=[]) + selected_image_index_state = gr.State(value=0) # Track selected image index + # current_view_index = gr.State(value=0) # noqa: F841 Track current view index + + # Header and description + self.ui_components.create_header_section() + self.ui_components.create_description_section() + + target_dir_output = gr.Textbox(label="Target Dir", visible=False, value="None") + + # Main content area + with gr.Row(): + with gr.Column(scale=2): + # Upload section + ( + input_video, + s_time_interval, + input_images, + image_gallery, + ) = self.ui_components.create_upload_section() + + with gr.Column(scale=4): + with gr.Column(): + # gr.Markdown("**Metric 3D Reconstruction (Point Cloud and Camera Poses)**") + # Reconstruction control section (buttons) - moved below tabs + + log_output = gr.Markdown( + "Please upload a video or images, then click Reconstruct.", + elem_classes=["custom-log"], + ) + + # Tabbed interface + with gr.Tabs(): + with gr.Tab("Point Cloud & Cameras"): + reconstruction_output = ( + self.ui_components.create_3d_viewer_section() + ) + + with gr.Tab("Metric Depth"): + ( + prev_measure_btn, + measure_view_selector, + next_measure_btn, + measure_image, + measure_depth_image, + measure_text, + ) = self.ui_components.create_measure_section() + + with gr.Tab("3DGS Rendered Novel Views"): + gs_video, gs_info = self.ui_components.create_nvs_video() + + # Inference control section (before inference) + ( + model_selector, + process_res_method_dropdown, + infer_gs, + ref_view_strategy_dropdown, + ) = self.ui_components.create_inference_control_section() + + # Display control section - includes 3DGS options, buttons, and Visualization Options # noqa: E501 + ( + show_cam, + filter_black_bg, + filter_white_bg, + save_percentage, + num_max_points, + gs_trj_mode, + gs_video_quality, + submit_btn, + clear_btn, + ) = self.ui_components.create_display_control_section() + + # bind visibility of gs_trj_mode to infer_gs + infer_gs.change( + fn=lambda checked: ( + gr.update(visible=checked), + gr.update(visible=checked), + gr.update(visible=checked), + gr.update(visible=(not checked)), + ), + inputs=infer_gs, + outputs=[gs_trj_mode, gs_video_quality, gs_video, gs_info], + ) + + # Example scenes section + gr.Markdown("## Example Scenes") + + scenes = self.ui_components.create_example_scenes_section() + scene_components = self.ui_components.create_example_scene_grid(scenes) + + # Set up event handlers + self._setup_event_handlers( + demo, + is_example, + processed_data_state, + measure_points_state, + target_dir_output, + input_video, + input_images, + s_time_interval, + image_gallery, + reconstruction_output, + log_output, + show_cam, + filter_black_bg, + filter_white_bg, + process_res_method_dropdown, + save_percentage, + submit_btn, + clear_btn, + num_max_points, + infer_gs, + ref_view_strategy_dropdown, + selected_image_index_state, + measure_view_selector, + measure_image, + measure_depth_image, + measure_text, + prev_measure_btn, + next_measure_btn, + scenes, + scene_components, + gs_video, + gs_info, + gs_trj_mode, + gs_video_quality, + model_selector, + s_time_interval, + ) + + # Acknowledgements + self.ui_components.create_acknowledgements_section() + + return demo + + def _setup_event_handlers( + self, + demo: gr.Blocks, + is_example: gr.Textbox, + processed_data_state: gr.State, + measure_points_state: gr.State, + target_dir_output: gr.Textbox, + input_video: gr.Video, + input_images: gr.File, + s_time_interval: gr.Slider, + image_gallery: gr.Gallery, + reconstruction_output: gr.Model3D, + log_output: gr.Markdown, + show_cam: gr.Checkbox, + filter_black_bg: gr.Checkbox, + filter_white_bg: gr.Checkbox, + process_res_method_dropdown: gr.Dropdown, + save_percentage: gr.Slider, + submit_btn: gr.Button, + clear_btn: gr.ClearButton, + num_max_points: gr.Slider, + infer_gs: gr.Checkbox, + ref_view_strategy_dropdown: gr.Dropdown, + selected_image_index_state: gr.State, + measure_view_selector: gr.Dropdown, + measure_image: gr.Image, + measure_depth_image: gr.Image, + measure_text: gr.Markdown, + prev_measure_btn: gr.Button, + next_measure_btn: gr.Button, + scenes: List[Dict[str, Any]], + scene_components: List, # List of gr.Image or gr.Video + gs_video: gr.Video, + gs_info: gr.Markdown, + gs_trj_mode: gr.Dropdown, + gs_video_quality: gr.Dropdown, + model_selector: gr.Dropdown, + s_time_interval_slider: gr.Slider, + ) -> None: + """ + Set up all event handlers for the application. + + Args: + demo: Gradio Blocks interface + All other arguments: Gradio components to connect + """ + # Configure clear button + clear_btn.add( + [ + input_video, + input_images, + reconstruction_output, + log_output, + target_dir_output, + image_gallery, + gs_video, + ] + ) + + # Main reconstruction button + submit_btn.click( + fn=self.event_handlers.gradio_demo, + inputs=[ + target_dir_output, + show_cam, + filter_black_bg, + filter_white_bg, + process_res_method_dropdown, + save_percentage, + num_max_points, + infer_gs, + ref_view_strategy_dropdown, + gs_trj_mode, + gs_video_quality, + model_selector, + ], + outputs=[ + reconstruction_output, + log_output, + processed_data_state, + measure_image, + measure_depth_image, + measure_text, + measure_view_selector, + gs_video, + gs_info, + ], + ) + + # Real-time visualization updates + self._setup_visualization_handlers( + show_cam, + filter_black_bg, + filter_white_bg, + process_res_method_dropdown, + target_dir_output, + is_example, + reconstruction_output, + log_output, + ) + + # File upload handlers + input_video.change( + fn=self.event_handlers.handle_uploads, + inputs=[input_video, input_images, s_time_interval], + outputs=[reconstruction_output, target_dir_output, image_gallery, log_output], + ) + input_images.change( + fn=self.event_handlers.handle_uploads, + inputs=[input_video, input_images, s_time_interval], + outputs=[reconstruction_output, target_dir_output, image_gallery, log_output], + ) + + # Navigation handlers + self._setup_navigation_handlers( + prev_measure_btn, + next_measure_btn, + measure_view_selector, + measure_image, + measure_depth_image, + measure_points_state, + processed_data_state, + ) + + # Measurement handler + measure_image.select( + fn=self.event_handlers.measure, + inputs=[processed_data_state, measure_points_state, measure_view_selector], + outputs=[measure_image, measure_depth_image, measure_points_state, measure_text], + ) + + # Example scene handlers + self._setup_example_scene_handlers( + scenes, + scene_components, + reconstruction_output, + target_dir_output, + image_gallery, + log_output, + is_example, + processed_data_state, + measure_view_selector, + measure_image, + measure_depth_image, + gs_video, + gs_info, + s_time_interval, + ) + + def _setup_visualization_handlers( + self, + show_cam: gr.Checkbox, + filter_black_bg: gr.Checkbox, + filter_white_bg: gr.Checkbox, + process_res_method_dropdown: gr.Dropdown, + target_dir_output: gr.Textbox, + is_example: gr.Textbox, + reconstruction_output: gr.Model3D, + log_output: gr.Markdown, + ) -> None: + """Set up visualization update handlers.""" + # Common inputs for visualization updates + viz_inputs = [ + target_dir_output, + show_cam, + is_example, + filter_black_bg, + filter_white_bg, + process_res_method_dropdown, + ] + + # Set up change handlers for all visualization controls + for component in [show_cam, filter_black_bg, filter_white_bg]: + component.change( + fn=self.event_handlers.update_visualization, + inputs=viz_inputs, + outputs=[reconstruction_output, log_output], + ) + + def _setup_navigation_handlers( + self, + prev_measure_btn: gr.Button, + next_measure_btn: gr.Button, + measure_view_selector: gr.Dropdown, + measure_image: gr.Image, + measure_depth_image: gr.Image, + measure_points_state: gr.State, + processed_data_state: gr.State, + ) -> None: + """Set up navigation handlers for measure tab.""" + # Measure tab navigation + prev_measure_btn.click( + fn=lambda processed_data, current_selector: self.event_handlers.navigate_measure_view( + processed_data, current_selector, -1 + ), + inputs=[processed_data_state, measure_view_selector], + outputs=[ + measure_view_selector, + measure_image, + measure_depth_image, + measure_points_state, + ], + ) + + next_measure_btn.click( + fn=lambda processed_data, current_selector: self.event_handlers.navigate_measure_view( + processed_data, current_selector, 1 + ), + inputs=[processed_data_state, measure_view_selector], + outputs=[ + measure_view_selector, + measure_image, + measure_depth_image, + measure_points_state, + ], + ) + + measure_view_selector.change( + fn=lambda processed_data, selector_value: ( + self.event_handlers.update_measure_view( + processed_data, int(selector_value.split()[1]) - 1 + ) + if selector_value + else (None, None, []) + ), + inputs=[processed_data_state, measure_view_selector], + outputs=[measure_image, measure_depth_image, measure_points_state], + ) + + def _setup_example_scene_handlers( + self, + scenes: List[Dict[str, Any]], + scene_components: List, # List of gr.Image + reconstruction_output: gr.Model3D, + target_dir_output: gr.Textbox, + image_gallery: gr.Gallery, + log_output: gr.Markdown, + is_example: gr.Textbox, + processed_data_state: gr.State, + measure_view_selector: gr.Dropdown, + measure_image: gr.Image, + measure_depth_image: gr.Image, + gs_video: gr.Video, + gs_info: gr.Markdown, + s_time_interval: gr.Slider, + ) -> None: + """Set up example scene handlers.""" + # Use assets/examples directory + examples_dir = os.environ.get("DA3_EXAMPLES_DIR", "assets/examples") + + def load_and_update_measure(scene_name: str, fps: float): + """Load example scene and update measure view.""" + print(f"[load_and_update_measure] Called with scene_name={scene_name}, fps={fps}", flush=True) + result = self.event_handlers.load_example_scene(scene_name, examples_dir, fps) + print(f"[load_and_update_measure] target_dir from result[1]: {result[1]}", flush=True) + + # Update measure view if processed_data is available + measure_img = None + measure_depth = None + if result[4] is not None: # processed_data exists + measure_img, measure_depth, _ = ( + self.event_handlers.visualization_handler.update_measure_view(result[4], 0) + ) + + final_result = result + ("True", measure_img, measure_depth) + print(f"[load_and_update_measure] Returning {len(final_result)} values", flush=True) + return final_result + + def create_scene_handler(scene_name: str): + """Create a handler function for a specific scene.""" + def handler(fps: float): + return load_and_update_measure(scene_name, fps) + return handler + + for i, scene in enumerate(scenes): + if i < len(scene_components): + component = scene_components[i] + # Create handler with scene name bound + handler_fn = create_scene_handler(scene["name"]) + outputs = [ + reconstruction_output, + target_dir_output, + image_gallery, + log_output, + processed_data_state, + measure_view_selector, + gs_video, + gs_info, + is_example, + measure_image, + measure_depth_image, + ] + + # Use click event - s_time_interval value is passed as input + component.select(fn=handler_fn, inputs=[s_time_interval], outputs=outputs) + + def launch(self, host: str = "127.0.0.1", port: int = 7860, **kwargs) -> None: + """ + Launch the application. + + Args: + host: Host address to bind to + port: Port number to bind to + **kwargs: Additional arguments for demo.launch() + """ + demo = self.create_app() + demo.queue(max_size=20).launch( + show_error=True, + server_name=host, + server_port=port, + **kwargs, + ) + + +def main(): + """Main function to run the application.""" + parser = argparse.ArgumentParser( + description="Depth Anything 3 Gradio Application", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Basic usage + python gradio_app.py --help + python gradio_app.py --host 0.0.0.0 --port 8080 + python gradio_app.py --model-dir /path/to/model --workspace-dir /path/to/workspace + + # Cache examples at startup (all low-res) + python gradio_app.py --cache-examples + + # Cache with selective high-res+3DGS for scenes matching tag + python gradio_app.py --cache-examples --cache-gs-tag dl3dv + # This will use high-res + 3DGS for scenes containing "dl3dv" in their name, + # and low-res only for other scenes + """, + ) + + # Server configuration + parser.add_argument( + "--host", default="127.0.0.1", help="Host address to bind to (default: 127.0.0.1)" + ) + parser.add_argument( + "--port", type=int, default=7860, help="Port number to bind to (default: 7860)" + ) + + # Directory configuration + parser.add_argument( + "--model-dir", + default="depth-anything/DA3NESTED-GIANT-LARGE", + help="Path to the model directory (default: depth-anything/DA3NESTED-GIANT-LARGE)", + ) + parser.add_argument( + "--workspace-dir", + default="workspace/gradio", # noqa: E501 + help="Path to the workspace directory (default: workspace/gradio)", # noqa: E501 + ) + parser.add_argument( + "--gallery-dir", + default="workspace/gallery", + help="Path to the gallery directory (default: workspace/gallery)", # noqa: E501 + ) + + # Additional Gradio options + parser.add_argument("--share", action="store_true", help="Create a public link for the app") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + + # Example caching options + parser.add_argument( + "--cache-examples", + action="store_true", + help="Pre-cache all example scenes at startup for faster loading", + ) + parser.add_argument( + "--cache-gs-tag", + type=str, + default="", + help="Tag to match scene names for high-res+3DGS caching (e.g., 'dl3dv'). Scenes containing this tag will use high_res and infer_gs=True; others will use low_res only.", # noqa: E501 + ) + + args = parser.parse_args() + + # Create directories if they don't exist + os.makedirs(args.workspace_dir, exist_ok=True) + os.makedirs(args.gallery_dir, exist_ok=True) + + # Initialize and launch the application + app = DepthAnything3App( + model_dir=args.model_dir, workspace_dir=args.workspace_dir, gallery_dir=args.gallery_dir + ) + + # Prepare launch arguments + launch_kwargs = {"share": args.share, "debug": args.debug} + + print("Starting Depth Anything 3 Gradio App...") + print(f"Host: {args.host}") + print(f"Port: {args.port}") + print(f"Model Directory: {args.model_dir}") + print(f"Workspace Directory: {args.workspace_dir}") + print(f"Gallery Directory: {args.gallery_dir}") + print(f"Share: {args.share}") + print(f"Debug: {args.debug}") + print(f"Cache Examples: {args.cache_examples}") + if args.cache_examples: + if args.cache_gs_tag: + print( + f"Cache GS Tag: '{args.cache_gs_tag}' (scenes matching this tag will use high-res + 3DGS)" # noqa: E501 + ) # noqa: E501 + else: + print("Cache GS Tag: None (all scenes will use low-res only)") + + # Pre-cache examples if requested + if args.cache_examples: + print("\n" + "=" * 60) + print("Pre-caching mode enabled") + if args.cache_gs_tag: + print(f"Scenes containing '{args.cache_gs_tag}' will use HIGH-RES + 3DGS") + print("Other scenes will use LOW-RES only") + else: + print("All scenes will use LOW-RES only") + print("=" * 60) + app.cache_examples( + show_cam=True, + filter_black_bg=False, + filter_white_bg=False, + save_percentage=5.0, + num_max_points=1000, + cache_gs_tag=args.cache_gs_tag, + gs_trj_mode="smooth", + gs_video_quality="low", + ) + + app.launch(host=args.host, port=args.port, **launch_kwargs) + + +if __name__ == "__main__": + main() diff --git a/src/depth_anything_3/app/modules/__init__.py b/src/depth_anything_3/app/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dd0b71780214eeadbc4fc44f4ac070e0e5fa7795 --- /dev/null +++ b/src/depth_anything_3/app/modules/__init__.py @@ -0,0 +1,43 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Modules package for Depth Anything 3 Gradio app. + +This package contains all the modular components for the Gradio application. +""" + +from depth_anything_3.app.modules.event_handlers import EventHandlers +from depth_anything_3.app.modules.file_handlers import FileHandler +from depth_anything_3.app.modules.model_inference import ModelInference +from depth_anything_3.app.modules.ui_components import UIComponents +from depth_anything_3.app.modules.utils import ( + create_depth_visualization, + get_logo_base64, + get_scene_info, + save_to_gallery_func, +) +from depth_anything_3.app.modules.visualization import VisualizationHandler + +__all__ = [ + "ModelInference", + "FileHandler", + "VisualizationHandler", + "EventHandlers", + "UIComponents", + "create_depth_visualization", + "save_to_gallery_func", + "get_scene_info", + "get_logo_base64", +] diff --git a/src/depth_anything_3/app/modules/event_handlers.py b/src/depth_anything_3/app/modules/event_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..2a39b9ac94db69ee2e38d7e5c3b3c0183dbe2e5e --- /dev/null +++ b/src/depth_anything_3/app/modules/event_handlers.py @@ -0,0 +1,624 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Event handling module for Depth Anything 3 Gradio app. + +This module handles all event callbacks and user interactions. +""" + +import os +import time +from glob import glob +from typing import Any, Dict, List, Optional, Tuple + +import gradio as gr +import numpy as np +import torch + +from depth_anything_3.app.modules.file_handlers import FileHandler +from depth_anything_3.app.modules.model_inference import ModelInference +from depth_anything_3.app.modules.visualization import VisualizationHandler +from depth_anything_3.utils.memory import cleanup_cuda_memory + + +class EventHandlers: + """ + Handles all event callbacks and user interactions for the Gradio app. + """ + + def __init__(self): + """Initialize the event handlers.""" + self.model_inference = ModelInference() + self.file_handler = FileHandler() + self.visualization_handler = VisualizationHandler() + + def clear_fields(self) -> None: + """ + Clears the 3D viewer, the stored target_dir, and empties the gallery. + """ + return None + + def update_log(self) -> str: + """ + Display a quick log message while waiting. + """ + return "Loading and Reconstructing..." + + def save_current_visualization( + self, + target_dir: str, + save_percentage: float, + show_cam: bool, + filter_black_bg: bool, + filter_white_bg: bool, + processed_data: Optional[Dict], + scene_name: str = "", + ) -> str: + """ + Save current visualization results to gallery with specified save percentage. + + Args: + target_dir: Directory containing results + save_percentage: Percentage of points to save (0-100) + show_cam: Whether to show cameras + filter_black_bg: Whether to filter black background + filter_white_bg: Whether to filter white background + processed_data: Processed data from reconstruction + + Returns: + Status message + """ + if not target_dir or target_dir == "None" or not os.path.isdir(target_dir): + return "No reconstruction available. Please run 'Reconstruct' first." + + if processed_data is None: + return "No processed data available. Please run 'Reconstruct' first." + + try: + import datetime + + from .utils import save_to_gallery_func + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + if scene_name and scene_name.strip(): + gallery_name = f"{scene_name.strip()}_{timestamp}_pct{save_percentage:.0f}" + else: + gallery_name = f"save_{timestamp}_pct{save_percentage:.0f}" + + success, message = save_to_gallery_func( + target_dir=target_dir, processed_data=processed_data, gallery_name=gallery_name + ) + + if success: + return ( + "Successfully saved to gallery!\n" + f"Gallery name: {gallery_name}\n" + f"Save percentage: {save_percentage}%\n" + f"Show cameras: {show_cam}\n" + f"Filter black bg: {filter_black_bg}\n" + f"Filter white bg: {filter_white_bg}\n\n" + f"{message}" + ) + else: + return f"Failed to save to gallery: {message}" + + except Exception as e: + return f"Error saving visualization: {str(e)}" + + def gradio_demo( + self, + target_dir: str, + show_cam: bool = True, + filter_black_bg: bool = False, + filter_white_bg: bool = False, + process_res_method: str = "upper_bound_resize", + save_percentage: float = 30.0, + num_max_points: int = 1_000_000, + infer_gs: bool = False, + ref_view_strategy: str = "saddle_balanced", + gs_trj_mode: str = "extend", + gs_video_quality: str = "high", + model_name: str = None, + ): + """ + Perform reconstruction using the already-created target_dir/images. + + Args: + target_dir: Directory containing images + show_cam: Whether to show camera + filter_black_bg: Whether to filter black background + filter_white_bg: Whether to filter white background + process_res_method: Method for resizing input images + save_percentage: Filter percentage for point cloud + num_max_points: Maximum number of points + infer_gs: Whether to infer 3D Gaussian Splatting + ref_view_strategy: Reference view selection strategy + model_name: Model to use (da3-base, da3-large, da3nested-giant-large) + + Returns: + Tuple of reconstruction results + """ + from depth_anything_3.app.modules.model_inference import DEFAULT_MODEL + + if model_name is None: + model_name = DEFAULT_MODEL + + print(f"[gradio_demo] Called with target_dir={target_dir}, model={model_name}", flush=True) + + if target_dir is None or not os.path.isdir(target_dir) or target_dir == "None": + print("[gradio_demo] Invalid target_dir, returning early") + return ( + None, + "No valid target directory found. Please upload first.", + None, + None, + None, + "", + None, + gr.update(value=None, visible=False), # gs_video + gr.update(visible=True), # gs_info + ) + + start_time = time.time() + cleanup_cuda_memory() + + # Get image files for logging + target_dir_images = os.path.join(target_dir, "images") + all_files = ( + sorted(os.listdir(target_dir_images)) if os.path.isdir(target_dir_images) else [] + ) + + print(f"[gradio_demo] Running {model_name} on {len(all_files)} images...") + print(f"[gradio_demo] Reference view strategy: {ref_view_strategy}") + + try: + with torch.no_grad(): + prediction, processed_data = self.model_inference.run_inference( + target_dir, + process_res_method=process_res_method, + show_camera=show_cam, + save_percentage=save_percentage, + num_max_points=int(num_max_points * 1000), # Convert K to actual count + infer_gs=infer_gs, + ref_view_strategy=ref_view_strategy, + gs_trj_mode=gs_trj_mode, + gs_video_quality=gs_video_quality, + model_name=model_name, + ) + except Exception as e: + error_msg = f"Reconstruction failed: {str(e)}" + print(f"[ERROR] {error_msg}") + import traceback + traceback.print_exc() + return ( + None, + error_msg, + None, + None, + None, + "", + None, + gr.update(value=None, visible=False), + gr.update(visible=True), + ) + + # The GLB file is already generated by the API + glbfile = os.path.join(target_dir, "scene.glb") + + # Handle 3DGS video based on infer_gs flag + gsvideo_path = None + gs_video_visible = False + gs_info_visible = True + + if infer_gs: + try: + gsvideo_path = sorted(glob(os.path.join(target_dir, "gs_video", "*.mp4")))[-1] + gs_video_visible = True + gs_info_visible = False + except IndexError: + gsvideo_path = None + print("3DGS video not found, but infer_gs was enabled") + + # Cleanup + cleanup_cuda_memory() + + end_time = time.time() + print(f"Total time: {end_time - start_time:.2f} seconds") + log_msg = f"Reconstruction Success ({len(all_files)} frames). Waiting for visualization." + + # Populate visualization tabs with processed data + depth_vis, measure_img, measure_depth_vis, measure_pts = ( + self.visualization_handler.populate_visualization_tabs(processed_data) + ) + + # Update view selectors based on available views + depth_selector, measure_selector = self.visualization_handler.update_view_selectors( + processed_data + ) + + return ( + glbfile, + log_msg, + processed_data, + measure_img, # measure_image + measure_depth_vis, # measure_depth_image + "", # measure_text (empty initially) + measure_selector, # measure_view_selector + gr.update(value=gsvideo_path, visible=gs_video_visible), # gs_video + gr.update(visible=gs_info_visible), # gs_info visibility + ) + + def update_visualization( + self, + target_dir: str, + show_cam: bool, + is_example: str, + filter_black_bg: bool = False, + filter_white_bg: bool = False, + process_res_method: str = "upper_bound_resize", + ) -> Tuple[gr.update, str]: + """ + Reload saved predictions from npz, create (or reuse) the GLB for new parameters, + and return it for the 3D viewer. + + Args: + target_dir: Directory containing results + show_cam: Whether to show camera + is_example: Whether this is an example scene + filter_black_bg: Whether to filter black background + filter_white_bg: Whether to filter white background + process_res_method: Method for resizing input images + + Returns: + Tuple of (glb_file, log_message) + """ + if not target_dir or target_dir == "None" or not os.path.isdir(target_dir): + return ( + gr.update(), + "No reconstruction available. Please click the Reconstruct button first.", + ) + + # Check if GLB exists (could be cached example or reconstructed scene) + glbfile = os.path.join(target_dir, "scene.glb") + if os.path.exists(glbfile): + return ( + glbfile, + ( + "Visualization loaded from cache." + if is_example == "True" + else "Visualization updated." + ), + ) + + # If no GLB but it's an example that hasn't been reconstructed yet + if is_example == "True": + return ( + gr.update(), + "No reconstruction available. Please click the Reconstruct button first.", + ) + + # For non-examples, check predictions.npz + predictions_path = os.path.join(target_dir, "predictions.npz") + if not os.path.exists(predictions_path): + error_message = ( + f"No reconstruction available at {predictions_path}. " + "Please run 'Reconstruct' first." + ) + return gr.update(), error_message + + loaded = np.load(predictions_path, allow_pickle=True) + predictions = {key: loaded[key] for key in loaded.keys()} # noqa: F841 + + return ( + glbfile, + "Visualization updated.", + ) + + def handle_uploads( + self, + input_video: Optional[str], + input_images: Optional[List], + s_time_interval: float = 10.0, + ) -> Tuple[Optional[str], Optional[str], Optional[List], Optional[str]]: + """ + Handle file uploads and update gallery. + + Args: + input_video: Path to input video file + input_images: List of input image files + s_time_interval: Sampling FPS (frames per second) for frame extraction + + Returns: + Tuple of (reconstruction_output, target_dir, image_paths, log_message) + """ + return self.file_handler.update_gallery_on_upload( + input_video, input_images, s_time_interval + ) + + def load_example_scene( + self, scene_name: str, examples_dir: str = None, s_time_interval: float = None + ) -> Tuple[ + Optional[str], + Optional[str], + Optional[List], + str, + Optional[Dict], + gr.Dropdown, # measure_view_selector + dict, # gs_video update (value + visibility) + dict, # gs_info update (visibility) + ]: + """ + Load a scene from examples directory. + + Args: + scene_name: Name of the scene to load + examples_dir: Path to examples directory (if None, uses workspace_dir/examples) + s_time_interval: Sampling FPS for video frame extraction (default 1.0) + + Returns: + Tuple of (reconstruction_output, target_dir, image_paths, log_message, processed_data, measure_view_selector, gs_video, gs_video_vis, gs_info_vis) # noqa: E501 + """ + if examples_dir is None: + # Get workspace directory from environment variable + workspace_dir = os.environ.get("DA3_WORKSPACE_DIR", "gradio_workspace") + examples_dir = os.path.join(workspace_dir, "examples") + + # Default FPS for video extraction + if s_time_interval is None: + s_time_interval = 1.0 + + reconstruction_output, target_dir, image_paths, log_message = ( + self.file_handler.load_example_scene(scene_name, examples_dir, s_time_interval) + ) + + # Try to load cached processed data if available + processed_data = None + measure_view_selector = gr.Dropdown(choices=["View 1"], value="View 1") + gs_video_path = None + gs_video_visible = False + gs_info_visible = True + + if target_dir and target_dir != "None": + predictions_path = os.path.join(target_dir, "predictions.npz") + if os.path.exists(predictions_path): + try: + # Load predictions from cache + loaded = np.load(predictions_path, allow_pickle=True) + predictions = {key: loaded[key] for key in loaded.keys()} + + # Reconstruct processed_data structure + num_images = len(predictions.get("images", [])) + processed_data = {} + + for i in range(num_images): + processed_data[i] = { + "image": predictions["images"][i] if "images" in predictions else None, + "depth": predictions["depths"][i] if "depths" in predictions else None, + "depth_image": os.path.join( + target_dir, "depth_vis", f"{i:04d}.jpg" # Fixed: use .jpg not .png + ), + "intrinsics": ( + predictions["intrinsics"][i] + if "intrinsics" in predictions + and i < len(predictions["intrinsics"]) + else None + ), + "mask": None, + } + + # Update measure view selector + choices = [f"View {i + 1}" for i in range(num_images)] + measure_view_selector = gr.Dropdown(choices=choices, value=choices[0]) + + except Exception as e: + print(f"Error loading cached data: {e}") + + # Check for cached 3DGS video + gs_video_dir = os.path.join(target_dir, "gs_video") + if os.path.exists(gs_video_dir): + try: + from glob import glob + + gs_videos = sorted(glob(os.path.join(gs_video_dir, "*.mp4"))) + if gs_videos: + gs_video_path = gs_videos[-1] + gs_video_visible = True + gs_info_visible = False + print(f"Loaded cached 3DGS video: {gs_video_path}") + except Exception as e: + print(f"Error loading cached 3DGS video: {e}") + + return ( + reconstruction_output, + target_dir, + image_paths, + log_message, + processed_data, + measure_view_selector, + gr.update(value=gs_video_path, visible=gs_video_visible), # gs_video + gr.update(visible=gs_info_visible), # gs_info + ) + + def navigate_depth_view( + self, + processed_data: Optional[dict], + current_selector: str, + direction: int, + ) -> Tuple[str, Optional[str]]: + """ + Navigate depth view. + + Args: + processed_data: Processed data dictionary + current_selector: Current selector value + direction: Direction to navigate + + Returns: + Tuple of (new_selector_value, depth_vis) + """ + return self.visualization_handler.navigate_depth_view( + processed_data, current_selector, direction + ) + + def update_depth_view( + self, processed_data: Optional[dict], view_index: int + ) -> Optional[str]: + """ + Update depth view for a specific view index. + + Args: + processed_data: Processed data dictionary + view_index: Index of the view to update + + Returns: + Path to depth visualization image or None + """ + return self.visualization_handler.update_depth_view(processed_data, view_index) + + def navigate_measure_view( + self, + processed_data: Optional[dict], + current_selector: str, + direction: int, + ) -> Tuple[str, Optional[np.ndarray], Optional[np.ndarray], List]: + """ + Navigate measure view. + + Args: + processed_data: Processed data dictionary + current_selector: Current selector value + direction: Direction to navigate + + Returns: + Tuple of (new_selector_value, measure_image, depth_right_half, measure_points) + """ + return self.visualization_handler.navigate_measure_view( + processed_data, current_selector, direction + ) + + def update_measure_view( + self, processed_data: Optional[dict], view_index: int + ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], List]: + """ + Update measure view for a specific view index. + + Args: + processed_data: Processed data dictionary + view_index: Index of the view to update + + Returns: + Tuple of (measure_image, depth_right_half, measure_points) + """ + return self.visualization_handler.update_measure_view(processed_data, view_index) + + def measure( + self, + processed_data: Optional[dict], + measure_points: List, + current_view_selector: str, + event: gr.SelectData, + ) -> List: + """ + Handle measurement on images. + + Args: + processed_data: Processed data dictionary + measure_points: List of current measure points + current_view_selector: Current view selector value + event: Gradio select event + + Returns: + List of [image, depth_right_half, measure_points, text] + """ + return self.visualization_handler.measure( + processed_data, measure_points, current_view_selector, event + ) + + def select_first_frame( + self, image_gallery: List, selected_index: int = 0 + ) -> Tuple[List, str, str]: + """ + Select the first frame from the image gallery. + + Args: + image_gallery: List of images in the gallery + selected_index: Index of the selected image (default: 0) + + Returns: + Tuple of (updated_image_gallery, log_message, selected_frame_path) + """ + try: + if not image_gallery or len(image_gallery) == 0: + return image_gallery, "No images available to select as first frame.", "" + + # Handle None or invalid selected_index + if ( + selected_index is None + or selected_index < 0 + or selected_index >= len(image_gallery) + ): + selected_index = 0 + print(f"Invalid selected_index: {selected_index}, using default: 0") + + # Get the selected image based on index + selected_image = image_gallery[selected_index] + print(f"Selected image index: {selected_index}") + print(f"Total images: {len(image_gallery)}") + + # Extract the file path from the selected image + selected_frame_path = "" + print(f"Selected image type: {type(selected_image)}") + print(f"Selected image: {selected_image}") + + if isinstance(selected_image, tuple): + # Gradio Gallery returns tuple (path, None) + selected_frame_path = selected_image[0] + elif isinstance(selected_image, str): + selected_frame_path = selected_image + elif hasattr(selected_image, "name"): + selected_frame_path = selected_image.name + elif isinstance(selected_image, dict): + if "name" in selected_image: + selected_frame_path = selected_image["name"] + elif "path" in selected_image: + selected_frame_path = selected_image["path"] + elif "src" in selected_image: + selected_frame_path = selected_image["src"] + else: + # Try to convert to string + selected_frame_path = str(selected_image) + + print(f"Extracted path: {selected_frame_path}") + + # Extract filename from the path for matching + import os + + selected_filename = os.path.basename(selected_frame_path) + print(f"Selected filename: {selected_filename}") + + # Move the selected image to the front + updated_gallery = [selected_image] + [ + img for img in image_gallery if img != selected_image + ] + + log_message = ( + f"Selected frame: {selected_filename}. " + f"Moved to first position. Total frames: {len(updated_gallery)}" + ) + return updated_gallery, log_message, selected_filename + + except Exception as e: + print(f"Error selecting first frame: {e}") + return image_gallery, f"Error selecting first frame: {e}", "" diff --git a/src/depth_anything_3/app/modules/file_handlers.py b/src/depth_anything_3/app/modules/file_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..351a2d786145d1bfa09ed3db35522ba4fb0819e8 --- /dev/null +++ b/src/depth_anything_3/app/modules/file_handlers.py @@ -0,0 +1,327 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +File handling module for Depth Anything 3 Gradio app. + +This module handles file uploads, video processing, and file operations. +""" + +import os +import shutil +import time +from datetime import datetime +from typing import List, Optional, Tuple + +import cv2 +from PIL import Image +from pillow_heif import register_heif_opener + +register_heif_opener() + + +class FileHandler: + """ + Handles file uploads and processing for the Gradio app. + """ + + def __init__(self): + """Initialize the file handler.""" + + def handle_uploads( + self, + input_video: Optional[str], + input_images: Optional[List], + s_time_interval: float = 10.0, + ) -> Tuple[str, List[str]]: + """ + Create a new 'target_dir' + 'images' subfolder, and place user-uploaded + images or extracted frames from video into it. + + Args: + input_video: Path to input video file + input_images: List of input image files + s_time_interval: Sampling FPS (frames per second) for frame extraction + + Returns: + Tuple of (target_dir, image_paths) + """ + start_time = time.time() + + # Get workspace directory from environment variable or use default + workspace_dir = os.environ.get("DA3_WORKSPACE_DIR", "gradio_workspace") + if not os.path.exists(workspace_dir): + os.makedirs(workspace_dir) + + # Create input_images subdirectory + input_images_dir = os.path.join(workspace_dir, "input_images") + if not os.path.exists(input_images_dir): + os.makedirs(input_images_dir) + + # Create a unique folder name within input_images + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + target_dir = os.path.join(input_images_dir, f"session_{timestamp}") + target_dir_images = os.path.join(target_dir, "images") + + # Clean up if somehow that folder already exists + if os.path.exists(target_dir): + shutil.rmtree(target_dir) + os.makedirs(target_dir) + os.makedirs(target_dir_images) + + image_paths = [] + + # Handle images + if input_images is not None: + image_paths.extend(self._process_images(input_images, target_dir_images)) + + # Handle video + if input_video is not None: + image_paths.extend( + self._process_video(input_video, target_dir_images, s_time_interval) + ) + + # Sort final images for gallery + image_paths = sorted(image_paths) + + end_time = time.time() + print(f"Files copied to {target_dir_images}; took {end_time - start_time:.3f} seconds") + return target_dir, image_paths + + def _process_images(self, input_images: List, target_dir_images: str) -> List[str]: + """ + Process uploaded images. + + Args: + input_images: List of input image files + target_dir_images: Target directory for images + + Returns: + List of processed image paths + """ + image_paths = [] + + for file_data in input_images: + if isinstance(file_data, dict) and "name" in file_data: + file_path = file_data["name"] + else: + file_path = file_data + + # Check if the file is a HEIC image + file_ext = os.path.splitext(file_path)[1].lower() + if file_ext in [".heic", ".heif"]: + # Convert HEIC to JPEG for better gallery compatibility + try: + with Image.open(file_path) as img: + # Convert to RGB if necessary (HEIC can have different color modes) + if img.mode not in ("RGB", "L"): + img = img.convert("RGB") + + # Create JPEG filename + base_name = os.path.splitext(os.path.basename(file_path))[0] + dst_path = os.path.join(target_dir_images, f"{base_name}.jpg") + + # Save as JPEG with high quality + img.save(dst_path, "JPEG", quality=95) + image_paths.append(dst_path) + print( + f"Converted HEIC to JPEG: {os.path.basename(file_path)} -> " + f"{os.path.basename(dst_path)}" + ) + except Exception as e: + print(f"Error converting HEIC file {file_path}: {e}") + # Fall back to copying as is + dst_path = os.path.join(target_dir_images, os.path.basename(file_path)) + shutil.copy(file_path, dst_path) + image_paths.append(dst_path) + else: + # Regular image files - copy as is + dst_path = os.path.join(target_dir_images, os.path.basename(file_path)) + shutil.copy(file_path, dst_path) + image_paths.append(dst_path) + + return image_paths + + def _process_video( + self, input_video: str, target_dir_images: str, s_time_interval: float + ) -> List[str]: + """ + Process video file and extract frames. + + Args: + input_video: Path to input video file + target_dir_images: Target directory for extracted frames + s_time_interval: Sampling FPS (frames per second) for frame extraction + + Returns: + List of extracted frame paths + """ + image_paths = [] + + if isinstance(input_video, dict) and "name" in input_video: + video_path = input_video["name"] + else: + video_path = input_video + + vs = cv2.VideoCapture(video_path) + fps = vs.get(cv2.CAP_PROP_FPS) + frame_interval = max(1, int(fps / s_time_interval)) # Convert FPS to frame interval + + count = 0 + video_frame_num = 0 + while True: + gotit, frame = vs.read() + if not gotit: + break + count += 1 + if count % frame_interval == 0: + image_path = os.path.join(target_dir_images, f"{video_frame_num:06}.png") + cv2.imwrite(image_path, frame) + image_paths.append(image_path) + video_frame_num += 1 + + return image_paths + + def update_gallery_on_upload( + self, + input_video: Optional[str], + input_images: Optional[List], + s_time_interval: float = 10.0, + ) -> Tuple[Optional[str], Optional[str], Optional[List], Optional[str]]: + """ + Handle file uploads and update gallery. + + Args: + input_video: Path to input video file + input_images: List of input image files + s_time_interval: Sampling FPS (frames per second) for frame extraction + + Returns: + Tuple of (reconstruction_output, target_dir, image_paths, log_message) + """ + if not input_video and not input_images: + return None, None, None, None + + target_dir, image_paths = self.handle_uploads(input_video, input_images, s_time_interval) + return ( + None, + target_dir, + image_paths, + "Upload complete. Click 'Reconstruct' to begin 3D processing.", + ) + + def load_example_scene( + self, scene_name: str, examples_dir: str = "examples", s_time_interval: float = 1.0 + ) -> Tuple[Optional[str], Optional[str], Optional[List], str]: + """ + Load a scene from examples directory. + + Args: + scene_name: Name of the scene to load + examples_dir: Path to examples directory + s_time_interval: Sampling FPS for video frame extraction (default 1.0) + + Returns: + Tuple of (reconstruction_output, target_dir, image_paths, log_message) + """ + from depth_anything_3.app.modules.utils import get_scene_info + + scenes = get_scene_info(examples_dir) + + # Find the selected scene + selected_scene = None + for scene in scenes: + if scene["name"] == scene_name: + selected_scene = scene + break + + if selected_scene is None: + return None, None, None, "Scene not found" + + # Check if this is a video scene + is_video_scene = selected_scene.get("type") == "video" + + # Use fixed directory name for examples (not timestamp-based) + workspace_dir = os.environ.get("DA3_WORKSPACE_DIR", "gradio_workspace") + input_images_dir = os.path.join(workspace_dir, "input_images") + if not os.path.exists(input_images_dir): + os.makedirs(input_images_dir) + + # For video scenes, include FPS in folder name so different FPS = different cache + if is_video_scene: + target_dir = os.path.join( + input_images_dir, f"example_{scene_name}_fps{s_time_interval:.1f}" + ) + else: + target_dir = os.path.join(input_images_dir, f"example_{scene_name}") + target_dir_images = os.path.join(target_dir, "images") + + # Check if already cached (GLB file exists) + glb_path = os.path.join(target_dir, "scene.glb") + is_cached = os.path.exists(glb_path) + + # Create directory if it doesn't exist + if not os.path.exists(target_dir): + os.makedirs(target_dir) + os.makedirs(target_dir_images) + + # Process images or extract video frames if directory is new or empty + if not os.path.exists(target_dir_images) or len(os.listdir(target_dir_images)) == 0: + os.makedirs(target_dir_images, exist_ok=True) + image_paths = [] + + if is_video_scene: + # Extract frames from video using specified FPS + video_path = selected_scene.get("video_file") + if video_path: + image_paths = self._process_video( + video_path, target_dir_images, s_time_interval + ) + else: + # Copy images + for file_path in selected_scene["image_files"]: + dst_path = os.path.join(target_dir_images, os.path.basename(file_path)) + shutil.copy(file_path, dst_path) + image_paths.append(dst_path) + else: + # Use existing images + image_paths = sorted( + [ + os.path.join(target_dir_images, f) + for f in os.listdir(target_dir_images) + if f.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif")) + ] + ) + + num_frames = len(image_paths) + scene_type = "video" if is_video_scene else "scene" + + # Return cached GLB if available + if is_cached: + return ( + glb_path, # Return cached reconstruction + target_dir, # Set target directory + image_paths, # Set gallery + f"Loaded cached {scene_type} '{scene_name}' with {num_frames} frames.", + ) + else: + return ( + None, # No cached reconstruction + target_dir, # Set target directory + image_paths, # Set gallery + ( + f"Loaded {scene_type} '{scene_name}' with {num_frames} frames. " + "Click 'Reconstruct' to begin 3D processing." + ), + ) diff --git a/src/depth_anything_3/app/modules/model_inference.py b/src/depth_anything_3/app/modules/model_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..43b6ed7d24d39610f6570f6c7902a6ef4cc74911 --- /dev/null +++ b/src/depth_anything_3/app/modules/model_inference.py @@ -0,0 +1,454 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# Optimizations (c) Delanoe Pirard / Aedelon - Apache 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Model inference module for Depth Anything 3 Gradio app. + +This module handles all model-related operations including inference, +data processing, and result preparation. + +Optimizations based on benchmarks: +- Smart batch sizing per model/device (MPS: B=4 for small/base, B=2 for large, B=1 for giant) +- CUDA: Adaptive batching at 85% memory utilization +- CPU: Always batch=1 +- Model caching for 200x faster subsequent loads +""" + +import glob +import os +import time +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import torch + +from depth_anything_3.api import DepthAnything3 +from depth_anything_3.utils.export.glb import export_to_glb +from depth_anything_3.utils.export.gs import export_to_gs_video +from depth_anything_3.utils.memory import cleanup_cuda_memory + +# Available models for UI selection +AVAILABLE_MODELS = { + "da3-small": "Small (fastest, ~27 img/s)", + "da3-base": "Base (fast, ~10 img/s)", + "da3-large": "Large (balanced, ~4 img/s)", + "da3-giant": "Giant (high quality, ~1.6 img/s)", + "da3nested-giant-large": "Giant+Large (best quality, ~1.5 img/s)", +} + +# Mapping from UI names to HuggingFace repo IDs +MODEL_TO_HF_REPO = { + "da3-small": "depth-anything/DA3-SMALL", + "da3-base": "depth-anything/DA3-BASE", + "da3-large": "depth-anything/DA3-LARGE", + "da3-giant": "depth-anything/DA3-GIANT", + "da3nested-giant-large": "depth-anything/DA3NESTED-GIANT-LARGE", +} + +DEFAULT_MODEL = "da3nested-giant-large" + + +class ModelInference: + """ + Handles model inference and data processing for Depth Anything 3. + + Uses benchmark-optimized batch sizes: + - MPS: B=4 for small/base, B=2 for large, B=1 for giant + - CUDA: Adaptive batching (85% VRAM utilization) + - CPU: B=1 always + """ + + def __init__(self): + """Initialize the model inference handler.""" + self.model: Optional[DepthAnything3] = None + self.current_model_name: Optional[str] = None + self.device: Optional[torch.device] = None + + def _get_optimal_batch_size( + self, num_images: int, model_name: str, device_type: str + ) -> int: + """ + Get optimal batch size based on benchmarks. + + Benchmark results (MPS, 1280x720): + - da3-small: B=4 → 27.2 img/s (vs B=1 → 22.2 img/s) + - da3-base: B=4 → 11.6 img/s (vs B=1 → 10.7 img/s) + - da3-large: B=2 → 3.8 img/s (B=4 slower due to memory pressure) + - da3-giant: B=1 → 1.6 img/s (B=4 → 1.2 img/s, worse!) + + Args: + num_images: Number of images to process + model_name: Name of the model + device_type: Device type ('cuda', 'mps', 'cpu') + + Returns: + Optimal batch size + """ + if device_type == "cpu": + return 1 + + # MPS: Use benchmark-optimized fixed batch sizes + if device_type == "mps": + if "small" in model_name: + return min(4, num_images) + elif "base" in model_name: + return min(4, num_images) + elif "giant" in model_name: + return 1 + else: # large + return min(2, num_images) + + # CUDA: Conservative batch size, can be tuned + if "giant" in model_name: + return min(2, num_images) + elif "large" in model_name: + return min(4, num_images) + else: + return min(8, num_images) + + def initialize_model(self, device: torch.device, model_name: str = None) -> None: + """ + Initialize the DepthAnything3 model. + + Args: + device: Device to load the model on + model_name: Model name to load (default: da3-base) + """ + if model_name is None: + model_name = os.environ.get("DA3_MODEL_NAME", DEFAULT_MODEL) + + # Check if we need to reload the model + need_reload = ( + self.model is None + or self.current_model_name != model_name + or self.device != device + ) + + if need_reload: + # Cleanup old model if exists + if self.model is not None: + print(f"[ModelInference] Unloading {self.current_model_name}") + del self.model + self.model = None + cleanup_cuda_memory() + + # Get HuggingFace repo ID from model name + hf_repo = MODEL_TO_HF_REPO.get(model_name, model_name) + print(f"[ModelInference] Loading model: {model_name} ({hf_repo}) on {device}") + start_time = time.time() + + # Use from_pretrained to load from HuggingFace + self.model = DepthAnything3.from_pretrained(hf_repo) + self.model = self.model.to(device) + self.current_model_name = model_name + self.device = device + + load_time = time.time() - start_time + print(f"[ModelInference] Model loaded in {load_time:.2f}s") + else: + print(f"[ModelInference] Reusing cached model: {model_name}") + + self.model.eval() + + def run_inference( + self, + target_dir: str, + filter_black_bg: bool = False, + filter_white_bg: bool = False, + process_res_method: str = "upper_bound_resize", + show_camera: bool = True, + save_percentage: float = 30.0, + num_max_points: int = 1_000_000, + infer_gs: bool = False, + ref_view_strategy: str = "saddle_balanced", + gs_trj_mode: str = "extend", + gs_video_quality: str = "high", + model_name: str = None, + ) -> Tuple[Any, dict]: + """ + Run DepthAnything3 model inference on images. + + All images are processed in a single batch for optimal performance. + + Args: + target_dir: Directory containing images + filter_black_bg: Whether to filter black background + filter_white_bg: Whether to filter white background + process_res_method: Method for resizing input images + show_camera: Whether to show camera in 3D view + save_percentage: Percentage of points to save (0-100) + num_max_points: Maximum number of points in point cloud + infer_gs: Whether to infer 3D Gaussian Splatting + ref_view_strategy: Reference view selection strategy + gs_trj_mode: Trajectory mode for 3DGS + gs_video_quality: Video quality for 3DGS + model_name: Model to use (default: da3-base) + + Returns: + Tuple of (prediction, processed_data) + """ + inference_start = time.time() + print(f"[ModelInference] Processing images from {target_dir}") + + # Device check - support CUDA, MPS (Apple Silicon), and CPU + if torch.cuda.is_available(): + device = torch.device("cuda") + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + device = torch.device("mps") + else: + device = torch.device("cpu") + + # Initialize model (with caching) + if model_name is None: + model_name = DEFAULT_MODEL + self.initialize_model(device, model_name) + + # Get image paths + image_folder_path = os.path.join(target_dir, "images") + all_image_paths = sorted(glob.glob(os.path.join(image_folder_path, "*"))) + + # Filter for image files + image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif"] + image_paths = [ + path + for path in all_image_paths + if any(path.lower().endswith(ext) for ext in image_extensions) + ] + + num_images = len(image_paths) + print(f"[ModelInference] Found {num_images} images") + + if num_images == 0: + raise ValueError("No images found. Check your upload.") + + # Map UI options to actual method names + method_mapping = {"high_res": "lower_bound_resize", "low_res": "upper_bound_resize"} + actual_method = method_mapping.get(process_res_method, "upper_bound_crop") + + # Get optimal batch size based on benchmarks + batch_size = self._get_optimal_batch_size(num_images, model_name, device.type) + print( + f"[ModelInference] Batched inference: model={model_name}, " + f"device={device.type}, images={num_images}, batch_size={batch_size}" + ) + + # Run model inference with batching + with torch.no_grad(): + if num_images <= batch_size: + # Single batch - process all at once + prediction = self.model.inference( + image_paths, + export_dir=None, + process_res_method=actual_method, + infer_gs=infer_gs, + ref_view_strategy=ref_view_strategy, + ) + else: + # Multiple batches - process in chunks and merge + predictions = [] + for i in range(0, num_images, batch_size): + batch_paths = image_paths[i : i + batch_size] + print(f"[ModelInference] Processing batch {i // batch_size + 1}/{(num_images + batch_size - 1) // batch_size} ({len(batch_paths)} images)") + batch_pred = self.model.inference( + batch_paths, + export_dir=None, + process_res_method=actual_method, + infer_gs=False, # Only infer GS on final merged result + ref_view_strategy=ref_view_strategy, + ) + predictions.append(batch_pred) + + # Merge all batch predictions + prediction = self._merge_predictions(predictions) + # num_max_points: int = 1_000_000, + export_to_glb( + prediction, + filter_black_bg=filter_black_bg, + filter_white_bg=filter_white_bg, + export_dir=target_dir, + show_cameras=show_camera, + conf_thresh_percentile=save_percentage, + num_max_points=int(num_max_points), + ) + + # export to gs video if needed + if infer_gs: + mode_mapping = {"extend": "extend", "smooth": "interpolate_smooth"} + print(f"GS mode: {gs_trj_mode}; Backend mode: {mode_mapping[gs_trj_mode]}") + export_to_gs_video( + prediction, + export_dir=target_dir, + chunk_size=4, + trj_mode=mode_mapping.get(gs_trj_mode, "extend"), + enable_tqdm=True, + vis_depth="hcat", + video_quality=gs_video_quality, + ) + + # Save predictions.npz for caching metric depth data + self._save_predictions_cache(target_dir, prediction) + + # Process results + processed_data = self._process_results(target_dir, prediction, image_paths) + + # Clean up using centralized memory utilities for consistency with backend + cleanup_cuda_memory() + + inference_time = time.time() - inference_start + throughput = num_images / inference_time if inference_time > 0 else 0 + print( + f"[ModelInference] Completed in {inference_time:.2f}s " + f"({throughput:.1f} img/s)" + ) + + return prediction, processed_data + + def _merge_predictions(self, predictions: List[Any]) -> Any: + """ + Merge multiple batch predictions into a single prediction. + + Args: + predictions: List of Prediction objects from batch inference + + Returns: + Merged Prediction object + """ + if not predictions: + return None + if len(predictions) == 1: + return predictions[0] + + from depth_anything_3.specs import Prediction + + # Concatenate arrays from all predictions + merged_depth = np.concatenate([p.depth for p in predictions], axis=0) + merged_conf = ( + np.concatenate([p.conf for p in predictions], axis=0) + if predictions[0].conf is not None + else None + ) + merged_processed_images = ( + np.concatenate([p.processed_images for p in predictions], axis=0) + if predictions[0].processed_images is not None + else None + ) + merged_extrinsics = ( + np.concatenate([p.extrinsics for p in predictions], axis=0) + if predictions[0].extrinsics is not None + else None + ) + merged_intrinsics = ( + np.concatenate([p.intrinsics for p in predictions], axis=0) + if predictions[0].intrinsics is not None + else None + ) + + # Create merged prediction (use is_metric from first batch) + merged = Prediction( + depth=merged_depth, + is_metric=predictions[0].is_metric, + conf=merged_conf, + extrinsics=merged_extrinsics, + intrinsics=merged_intrinsics, + processed_images=merged_processed_images, + ) + + print(f"[ModelInference] Merged {len(predictions)} batches into single prediction") + return merged + + def _save_predictions_cache(self, target_dir: str, prediction: Any) -> None: + """ + Save predictions data to predictions.npz for caching. + + Args: + target_dir: Directory to save the cache + prediction: Model prediction object + """ + try: + output_file = os.path.join(target_dir, "predictions.npz") + + # Build save dict with prediction data + save_dict = {} + + # Save processed images if available + if prediction.processed_images is not None: + save_dict["images"] = prediction.processed_images + + # Save depth data + if prediction.depth is not None: + save_dict["depths"] = np.round(prediction.depth, 6) + + # Save confidence if available + if prediction.conf is not None: + save_dict["conf"] = np.round(prediction.conf, 2) + + # Save camera parameters + if prediction.extrinsics is not None: + save_dict["extrinsics"] = prediction.extrinsics + if prediction.intrinsics is not None: + save_dict["intrinsics"] = prediction.intrinsics + + # Save to file + np.savez_compressed(output_file, **save_dict) + print(f"Saved predictions cache to: {output_file}") + + except Exception as e: + print(f"Warning: Failed to save predictions cache: {e}") + + def _process_results( + self, target_dir: str, prediction: Any, image_paths: list + ) -> dict: + """ + Process model results into structured data. + + Args: + target_dir: Directory containing results + prediction: Model prediction object + image_paths: List of input image paths + + Returns: + Dictionary containing processed data for each view + """ + processed_data = {} + + # Read generated depth visualization files + depth_vis_dir = os.path.join(target_dir, "depth_vis") + + if os.path.exists(depth_vis_dir): + depth_files = sorted(glob.glob(os.path.join(depth_vis_dir, "*.jpg"))) + for i, depth_file in enumerate(depth_files): + # Use processed images directly from API + processed_image = None + if prediction.processed_images is not None and i < len( + prediction.processed_images + ): + processed_image = prediction.processed_images[i] + + processed_data[i] = { + "depth_image": depth_file, + "image": processed_image, + "original_image_path": image_paths[i] if i < len(image_paths) else None, + "depth": prediction.depth[i] if i < len(prediction.depth) else None, + "intrinsics": ( + prediction.intrinsics[i] + if prediction.intrinsics is not None and i < len(prediction.intrinsics) + else None + ), + "mask": None, # No mask information available + } + + return processed_data + + # cleanup() removed: call cleanup_cuda_memory() directly where needed. diff --git a/src/depth_anything_3/app/modules/ui_components.py b/src/depth_anything_3/app/modules/ui_components.py new file mode 100644 index 0000000000000000000000000000000000000000..1944121f0ad434488ac53a8596930a34ce80b54b --- /dev/null +++ b/src/depth_anything_3/app/modules/ui_components.py @@ -0,0 +1,497 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +UI components module for Depth Anything 3 Gradio app. + +This module contains UI component definitions and layout functions. +""" + +import os +from typing import Any, Dict, List, Tuple + +import gradio as gr + +from depth_anything_3.app.modules.utils import get_logo_base64, get_scene_info + + +class UIComponents: + """ + Handles UI component creation and layout for the Gradio app. + """ + + def __init__(self): + """Initialize the UI components handler.""" + + def create_upload_section(self) -> Tuple[gr.Video, gr.Slider, gr.File, gr.Gallery]: + """ + Create the upload section with video, images, and gallery components. + + Returns: + A tuple of Gradio components: (input_video, s_time_interval, input_images, image_gallery). + """ + input_video = gr.Video(label="Upload Video", interactive=True) + s_time_interval = gr.Slider( + minimum=0.1, + maximum=60, + value=10, + step=0.1, + label="Sampling FPS (Frames Per Second)", + interactive=True, + visible=True, + ) + input_images = gr.File(file_count="multiple", label="Upload Images", interactive=True) + image_gallery = gr.Gallery( + label="Preview", + columns=4, + height="300px", + object_fit="contain", + allow_preview=True, + interactive=False, + ) + + return input_video, s_time_interval, input_images, image_gallery + + def create_3d_viewer_section(self) -> gr.Model3D: + """ + Create the 3D viewer component. + + Returns: + 3D model viewer component + """ + return gr.Model3D( + height=520, + zoom_speed=0.5, + pan_speed=0.5, + clear_color=[0.0, 0.0, 0.0, 0.0], + key="persistent_3d_viewer", + elem_id="reconstruction_3d_viewer", + ) + + def create_nvs_video(self) -> Tuple[gr.Video, gr.Markdown]: + """ + Create the 3DGS rendered video display component and info message. + + Returns: + Tuple of (video component, info message component) + """ + with gr.Column(): + gs_info = gr.Markdown( + ( + "‼️ **3D Gaussian Splatting rendering is currently DISABLED.**


" + "To render novel views from 3DGS, " + "enable **Infer 3D Gaussian Splatting** below.
" + "Next, in **Visualization Options**, " + "*optionally* configure the **rendering trajectory** (default: smooth) " + "and **video quality** (default: low), " + "then click **Reconstruct**." + ), + visible=True, + height=520, + ) + gs_video = gr.Video( + height=520, + label="3DGS Rendered NVS Video (depth shown for reference only)", + interactive=False, + visible=False, + ) + return gs_video, gs_info + + def create_depth_section(self) -> Tuple[gr.Button, gr.Dropdown, gr.Button, gr.Image]: + """ + Create the depth visualization section. + + Returns: + A tuple of (prev_depth_btn, depth_view_selector, next_depth_btn, depth_map) + """ + with gr.Row(elem_classes=["navigation-row"]): + prev_depth_btn = gr.Button("◀ Previous", size="sm", scale=1) + depth_view_selector = gr.Dropdown( + choices=["View 1"], + value="View 1", + label="Select View", + scale=2, + interactive=True, + allow_custom_value=True, + ) + next_depth_btn = gr.Button("Next ▶", size="sm", scale=1) + depth_map = gr.Image( + type="numpy", + label="Colorized Depth Map", + format="png", + interactive=False, + ) + + return prev_depth_btn, depth_view_selector, next_depth_btn, depth_map + + def create_measure_section( + self, + ) -> Tuple[gr.Button, gr.Dropdown, gr.Button, gr.Image, gr.Image, gr.Markdown]: + """ + Create the measurement section. + + Returns: + A tuple of (prev_measure_btn, measure_view_selector, next_measure_btn, measure_image, + measure_depth_image, measure_text) + """ + from depth_anything_3.app.css_and_html import MEASURE_INSTRUCTIONS_HTML + + gr.Markdown(MEASURE_INSTRUCTIONS_HTML) + with gr.Row(elem_classes=["navigation-row"]): + prev_measure_btn = gr.Button("◀ Previous", size="sm", scale=1) + measure_view_selector = gr.Dropdown( + choices=["View 1"], + value="View 1", + label="Select View", + scale=2, + interactive=True, + allow_custom_value=True, + ) + next_measure_btn = gr.Button("Next ▶", size="sm", scale=1) + with gr.Row(): + measure_image = gr.Image( + type="numpy", + show_label=False, + format="webp", + interactive=False, + sources=[], + label="RGB Image", + scale=1, + height=275, + ) + measure_depth_image = gr.Image( + type="numpy", + show_label=False, + format="webp", + interactive=False, + sources=[], + label="Depth Visualization (Right Half)", + scale=1, + height=275, + ) + gr.Markdown( + "**Note:** Images have been adjusted to model processing size. " + "Click two points on the RGB image to measure distance." + ) + measure_text = gr.Markdown("") + + return ( + prev_measure_btn, + measure_view_selector, + next_measure_btn, + measure_image, + measure_depth_image, + measure_text, + ) + + def create_inference_control_section( + self, + ) -> Tuple[gr.Dropdown, gr.Dropdown, gr.Checkbox, gr.Dropdown]: + """ + Create the inference control section (before inference). + + Returns: + Tuple of (model_selector, process_res_method_dropdown, infer_gs, ref_view_strategy) + """ + from depth_anything_3.app.modules.model_inference import AVAILABLE_MODELS, DEFAULT_MODEL + + with gr.Row(): + # Model selector - most important control + model_selector = gr.Dropdown( + choices=list(AVAILABLE_MODELS.keys()), + value=DEFAULT_MODEL, + label="Model", + info="da3-base: fast | da3-large: balanced | giant: best quality", + scale=1, + ) + process_res_method_dropdown = gr.Dropdown( + choices=["high_res", "low_res"], + value="low_res", + label="Image Processing Method", + info="low_res for much more images", + scale=1, + ) + infer_gs = gr.Checkbox( + label="Infer 3D Gaussian Splatting", + value=False, + info=( + 'Enable novel view rendering from 3DGS ( requires extra processing time)' + ), + scale=1, + ) + ref_view_strategy = gr.Dropdown( + choices=["saddle_balanced", "saddle_sim_range", "first", "middle"], + value="saddle_balanced", + label="Reference View Strategy", + info="Strategy for selecting reference view from multiple inputs", + scale=1, + ) + + return (model_selector, process_res_method_dropdown, infer_gs, ref_view_strategy) + + def create_display_control_section( + self, + ) -> Tuple[ + gr.Checkbox, + gr.Checkbox, + gr.Checkbox, + gr.Slider, + gr.Slider, + gr.Dropdown, + gr.Dropdown, + gr.Button, + gr.ClearButton, + ]: + """ + Create the display control section (options for visualization). + + Returns: + Tuple of display control components including buttons + """ + with gr.Column(): + # 3DGS options at the top + with gr.Row(): + gs_trj_mode = gr.Dropdown( + choices=["smooth", "extend"], + value="smooth", + label=("Rendering trajectory for 3DGS viewpoints (requires n_views ≥ 2)"), + info=("'smooth' for view interpolation; 'extend' for longer trajectory"), + visible=False, # initially hidden + ) + gs_video_quality = gr.Dropdown( + choices=["low", "medium", "high"], + value="low", + label=("Video quality for 3DGS rendered outputs"), + info=("'low' for faster loading speed; 'high' for better visual quality"), + visible=False, # initially hidden + ) + + # Reconstruct and Clear buttons (before Visualization Options) + with gr.Row(): + submit_btn = gr.Button("Reconstruct", scale=1, variant="primary") + clear_btn = gr.ClearButton(scale=1) + + gr.Markdown("### Visualization Options: (Click Reconstruct to update)") + show_cam = gr.Checkbox(label="Show Camera", value=True) + filter_black_bg = gr.Checkbox(label="Filter Black Background", value=False) + filter_white_bg = gr.Checkbox(label="Filter White Background", value=False) + save_percentage = gr.Slider( + minimum=0, + maximum=100, + value=10, + step=1, + label="Filter Percentage", + info="Confidence Threshold (%): Higher values filter more points.", + ) + num_max_points = gr.Slider( + minimum=1000, + maximum=100000, + value=1000, + step=1000, + label="Max Points (K points)", + info="Maximum number of points to export to GLB (in thousands)", + ) + + return ( + show_cam, + filter_black_bg, + filter_white_bg, + save_percentage, + num_max_points, + gs_trj_mode, + gs_video_quality, + submit_btn, + clear_btn, + ) + + def create_control_section( + self, + ) -> Tuple[ + gr.Button, + gr.ClearButton, + gr.Dropdown, + gr.Checkbox, + gr.Checkbox, + gr.Checkbox, + gr.Checkbox, + gr.Checkbox, + gr.Dropdown, + gr.Checkbox, + gr.Textbox, + ]: + """ + Create the control section with buttons and options. + + Returns: + Tuple of control components + """ + with gr.Row(): + submit_btn = gr.Button("Reconstruct", scale=1, variant="primary") + clear_btn = gr.ClearButton( + scale=1, + ) + + with gr.Row(): + frame_filter = gr.Dropdown( + choices=["All"], value="All", label="Show Points from Frame" + ) + with gr.Column(): + gr.Markdown("### Visualization Option: (Click Reconstruct to update)") + show_cam = gr.Checkbox(label="Show Camera", value=True) + show_mesh = gr.Checkbox(label="Show Mesh", value=True) + filter_black_bg = gr.Checkbox(label="Filter Black Background", value=False) + filter_white_bg = gr.Checkbox(label="Filter White Background", value=False) + gr.Markdown("### Reconstruction Options: (updated on next run)") + apply_mask_checkbox = gr.Checkbox( + label="Apply mask for predicted ambiguous depth classes & edges", + value=True, + ) + process_res_method_dropdown = gr.Dropdown( + choices=[ + "upper_bound_resize", + "upper_bound_crop", + "lower_bound_resize", + "lower_bound_crop", + ], + value="upper_bound_resize", + label="Image Processing Method", + info="Method for resizing input images", + ) + save_to_gallery_checkbox = gr.Checkbox( + label="Save to Gallery", + value=False, + info="Save current reconstruction results to gallery directory", + ) + gallery_name_input = gr.Textbox( + label="Gallery Name", + placeholder="Enter a name for the gallery folder", + value="", + info="Leave empty for auto-generated name with timestamp", + ) + + return ( + submit_btn, + clear_btn, + frame_filter, + show_cam, + show_mesh, + filter_black_bg, + filter_white_bg, + apply_mask_checkbox, + process_res_method_dropdown, + save_to_gallery_checkbox, + gallery_name_input, + ) + + def create_example_scenes_section(self) -> List[Dict[str, Any]]: + """ + Create the example scenes section. + + Returns: + List of scene information dictionaries + """ + # Use assets/examples directory for example scenes + examples_dir = os.environ.get("DA3_EXAMPLES_DIR", "assets/examples") + + # Get scene information + scenes = get_scene_info(examples_dir) + + return scenes + + def create_example_scene_grid(self, scenes: List[Dict[str, Any]]) -> List: + """ + Create the example scene grid. + + Args: + scenes: List of scene information dictionaries + + Returns: + List of scene components (gr.Image or gr.Video) in same order as scenes + """ + scene_components = [] + + if scenes: + for i in range(0, len(scenes), 4): # Process 4 scenes per row + with gr.Row(): + for j in range(4): + scene_idx = i + j + if scene_idx < len(scenes): + scene = scenes[scene_idx] + scene_type = scene.get("type", "images") + + with gr.Column(scale=1, elem_classes=["clickable-thumbnail"]): + # Use Image for both image and video scenes + # (video scenes use first frame as thumbnail) + scene_component = gr.Image( + value=scene["thumbnail"], + height=150, + interactive=False, + show_label=False, + elem_id=f"scene_thumb_{scene['name']}", + sources=[], + ) + scene_components.append(scene_component) + + if scene_type == "video": + # Scene name for video + gr.Markdown( + f"**{scene['name']}** \n 🎬 video", + elem_classes=["scene-info"], + ) + else: + # Scene name and image count + gr.Markdown( + f"**{scene['name']}** \n {scene['num_images']} images", + elem_classes=["scene-info"], + ) + else: + # Empty column to maintain grid structure + with gr.Column(scale=1): + pass + + return scene_components + + def create_header_section(self) -> gr.HTML: + """ + Create the header section with logo and title. + + Returns: + Header HTML component + """ + from depth_anything_3.app.css_and_html import get_header_html + + return gr.HTML(get_header_html(get_logo_base64())) + + def create_description_section(self) -> gr.HTML: + """ + Create the description section. + + Returns: + Description HTML component + """ + from depth_anything_3.app.css_and_html import get_description_html + + return gr.HTML(get_description_html()) + + def create_acknowledgements_section(self) -> gr.HTML: + """ + Create the acknowledgements section. + + Returns: + Acknowledgements HTML component + """ + from depth_anything_3.app.css_and_html import get_acknowledgements_html + + return gr.HTML(get_acknowledgements_html()) diff --git a/src/depth_anything_3/app/modules/utils.py b/src/depth_anything_3/app/modules/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..cd7733ace0444ba9f92ad366eb7edf99b0cf325f --- /dev/null +++ b/src/depth_anything_3/app/modules/utils.py @@ -0,0 +1,269 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utility functions for Depth Anything 3 Gradio app. + +This module contains helper functions for data processing, visualization, +and file operations. +""" + + +import json +import os +import shutil +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np + + +def create_depth_visualization(depth: np.ndarray) -> Optional[np.ndarray]: + """ + Create a colored depth visualization. + + Args: + depth: Depth array + + Returns: + Colored depth visualization or None + """ + if depth is None: + return None + + # Normalize depth to 0-1 range + depth_min = depth[depth > 0].min() if (depth > 0).any() else 0 + depth_max = depth.max() + + if depth_max <= depth_min: + return None + + # Normalize depth + depth_norm = (depth - depth_min) / (depth_max - depth_min) + depth_norm = np.clip(depth_norm, 0, 1) + + # Apply colormap (using matplotlib's viridis colormap) + import matplotlib.cm as cm + + # Convert to colored image + depth_colored = cm.viridis(depth_norm)[:, :, :3] # Remove alpha channel + depth_colored = (depth_colored * 255).astype(np.uint8) + + return depth_colored + + +def save_to_gallery_func( + target_dir: str, processed_data: dict, gallery_name: Optional[str] = None +) -> Tuple[bool, str]: + """ + Save the current reconstruction results to the gallery directory. + + Args: + target_dir: Source directory containing reconstruction results + processed_data: Processed data dictionary + gallery_name: Name for the gallery folder + + Returns: + Tuple of (success, message) + """ + try: + # Get gallery directory from environment variable or use default + gallery_dir = os.environ.get( + "DA3_GALLERY_DIR", + "workspace/gallery", + ) + if not os.path.exists(gallery_dir): + os.makedirs(gallery_dir) + + # Use provided name or create a unique name + if gallery_name is None or gallery_name.strip() == "": + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + gallery_name = f"reconstruction_{timestamp}" + + gallery_path = os.path.join(gallery_dir, gallery_name) + + # Check if directory already exists + if os.path.exists(gallery_path): + return False, f"Save failed: folder '{gallery_name}' already exists" + + # Create the gallery directory + os.makedirs(gallery_path, exist_ok=True) + + # Copy GLB file + glb_source = os.path.join(target_dir, "scene.glb") + glb_dest = os.path.join(gallery_path, "scene.glb") + if os.path.exists(glb_source): + shutil.copy2(glb_source, glb_dest) + + # Copy depth visualization images + depth_vis_dir = os.path.join(target_dir, "depth_vis") + if os.path.exists(depth_vis_dir): + gallery_depth_vis = os.path.join(gallery_path, "depth_vis") + shutil.copytree(depth_vis_dir, gallery_depth_vis) + + # Copy original images + images_source = os.path.join(target_dir, "images") + if os.path.exists(images_source): + gallery_images = os.path.join(gallery_path, "images") + shutil.copytree(images_source, gallery_images) + + scene_preview_source = os.path.join(target_dir, "scene.jpg") + scene_preview_dest = os.path.join(gallery_path, "scene.jpg") + shutil.copy2(scene_preview_source, scene_preview_dest) + + # Save metadata + metadata = { + "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"), + "num_images": len(processed_data) if processed_data else 0, + "gallery_name": gallery_name, + } + + with open(os.path.join(gallery_path, "metadata.json"), "w") as f: + json.dump(metadata, f, indent=2) + + print(f"Saved reconstruction to gallery: {gallery_path}") + return True, f"Save successful: saved to {gallery_path}" + + except Exception as e: + print(f"Error saving to gallery: {e}") + return False, f"Save failed: {str(e)}" + + +def _extract_video_thumbnail(video_path: str) -> str: + """ + Extract the first frame of a video as a thumbnail image. + + Args: + video_path: Path to the video file + + Returns: + Path to the thumbnail image (or video path if extraction fails) + """ + import tempfile + + import cv2 + + try: + cap = cv2.VideoCapture(video_path) + ret, frame = cap.read() + cap.release() + + if ret and frame is not None: + # Save thumbnail to temp directory + video_name = os.path.splitext(os.path.basename(video_path))[0] + thumbnail_dir = os.path.join(tempfile.gettempdir(), "da3_video_thumbnails") + os.makedirs(thumbnail_dir, exist_ok=True) + thumbnail_path = os.path.join(thumbnail_dir, f"{video_name}_thumb.jpg") + cv2.imwrite(thumbnail_path, frame) + return thumbnail_path + except Exception as e: + print(f"Error extracting video thumbnail: {e}") + + # Fallback to video path if extraction fails + return video_path + + +def get_scene_info(examples_dir: str) -> List[Dict[str, Any]]: + """ + Get information about scenes in the examples directory. + + Supports: + - Folders containing images (scene folders) + - Video files at the root level + + Args: + examples_dir: Path to examples directory + + Returns: + List of scene information dictionaries + """ + import glob + + scenes = [] + if not os.path.exists(examples_dir): + return scenes + + for item in sorted(os.listdir(examples_dir)): + item_path = os.path.join(examples_dir, item) + + if os.path.isdir(item_path): + # Find all image files in the scene folder + image_extensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.tiff", "*.tif"] + image_files = [] + for ext in image_extensions: + image_files.extend(glob.glob(os.path.join(item_path, ext))) + image_files.extend(glob.glob(os.path.join(item_path, ext.upper()))) + + if image_files: + # Sort images and get the first one for thumbnail + image_files = sorted(image_files) + first_image = image_files[0] + num_images = len(image_files) + + scenes.append( + { + "name": item, + "path": item_path, + "thumbnail": first_image, + "num_images": num_images, + "image_files": image_files, + "type": "images", + } + ) + + elif os.path.isfile(item_path): + # Check if it's a video file + video_extensions = [".mp4", ".avi", ".mov", ".mkv", ".webm"] + ext = os.path.splitext(item)[1].lower() + if ext in video_extensions: + name = os.path.splitext(item)[0] + # Extract first frame as thumbnail + thumbnail_path = _extract_video_thumbnail(item_path) + scenes.append( + { + "name": name, + "path": item_path, + "thumbnail": thumbnail_path, # First frame as thumbnail + "num_images": 0, + "image_files": [], + "video_file": item_path, + "type": "video", + } + ) + + return scenes + + +# NOTE: cleanup was moved to a single canonical helper in +# `depth_anything_3.utils.memory.cleanup_cuda_memory`. +# Callers should import and call that directly instead of using this module. + + +def get_logo_base64() -> Optional[str]: + """ + Convert WAI logo to base64 for embedding in HTML. + + Returns: + Base64 encoded logo string or None + """ + import base64 + + logo_path = "examples/WAI-Logo/wai_logo.png" + try: + with open(logo_path, "rb") as img_file: + img_data = img_file.read() + base64_str = base64.b64encode(img_data).decode() + return f"data:image/png;base64,{base64_str}" + except FileNotFoundError: + return None diff --git a/src/depth_anything_3/app/modules/visualization.py b/src/depth_anything_3/app/modules/visualization.py new file mode 100644 index 0000000000000000000000000000000000000000..642d81fdad7b62ab7afac93e1b13f58b554dc6b3 --- /dev/null +++ b/src/depth_anything_3/app/modules/visualization.py @@ -0,0 +1,435 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Visualization module for Depth Anything 3 Gradio app. + +This module handles visualization updates, navigation, and measurement functionality. +""" + +import os +from typing import Any, Dict, List, Optional, Tuple + +import cv2 +import gradio as gr +import numpy as np + + +class VisualizationHandler: + """ + Handles visualization updates and navigation for the Gradio app. + """ + + def __init__(self): + """Initialize the visualization handler.""" + + def update_view_selectors( + self, processed_data: Optional[dict] + ) -> Tuple[gr.Dropdown, gr.Dropdown]: + """ + Update view selector dropdowns based on available views. + + Args: + processed_data: Processed data dictionary + + Returns: + Tuple of (depth_view_selector, measure_view_selector) + """ + if processed_data is None or len(processed_data) == 0: + choices = ["View 1"] + else: + num_views = len(processed_data) + choices = [f"View {i + 1}" for i in range(num_views)] + + return ( + gr.Dropdown(choices=choices, value=choices[0]), # depth_view_selector + gr.Dropdown(choices=choices, value=choices[0]), # measure_view_selector + ) + + def get_view_data_by_index( + self, processed_data: Optional[dict], view_index: int + ) -> Optional[Dict[str, Any]]: + """ + Get view data by index, handling bounds. + + Args: + processed_data: Processed data dictionary + view_index: Index of the view to get + + Returns: + View data dictionary or None + """ + if processed_data is None or len(processed_data) == 0: + return None + + view_keys = list(processed_data.keys()) + if view_index < 0 or view_index >= len(view_keys): + view_index = 0 + + return processed_data[view_keys[view_index]] + + def update_depth_view( + self, processed_data: Optional[dict], view_index: int + ) -> Optional[str]: + """ + Update depth view for a specific view index. + + Args: + processed_data: Processed data dictionary + view_index: Index of the view to update + + Returns: + Path to depth visualization image or None + """ + view_data = self.get_view_data_by_index(processed_data, view_index) + if view_data is None or view_data.get("depth_image") is None: + return None + + # Return the depth visualization image directly + return view_data["depth_image"] + + def navigate_depth_view( + self, + processed_data: Optional[dict], + current_selector_value: str, + direction: int, + ) -> Tuple[str, Optional[str]]: + """ + Navigate depth view (direction: -1 for previous, +1 for next). + + Args: + processed_data: Processed data dictionary + current_selector_value: Current selector value + direction: Direction to navigate (-1 for previous, +1 for next) + + Returns: + Tuple of (new_selector_value, depth_vis) + """ + if processed_data is None or len(processed_data) == 0: + return "View 1", None + + # Parse current view number + try: + current_view = int(current_selector_value.split()[1]) - 1 + except: # noqa + current_view = 0 + + num_views = len(processed_data) + new_view = (current_view + direction) % num_views + + new_selector_value = f"View {new_view + 1}" + depth_vis = self.update_depth_view(processed_data, new_view) + + return new_selector_value, depth_vis + + def update_measure_view( + self, processed_data: Optional[dict], view_index: int + ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], List]: + """ + Update measure view for a specific view index. + + Args: + processed_data: Processed data dictionary + view_index: Index of the view to update + + Returns: + Tuple of (measure_image, depth_right_half, measure_points) + """ + view_data = self.get_view_data_by_index(processed_data, view_index) + if view_data is None: + return None, None, [] # image, depth_right_half, measure_points + + # Get the processed (resized) image + if "image" in view_data and view_data["image"] is not None: + image = view_data["image"].copy() + else: + return None, None, [] + + # Ensure image is in uint8 format + if image.dtype != np.uint8: + if image.max() <= 1.0: + image = (image * 255).astype(np.uint8) + else: + image = image.astype(np.uint8) + + # Extract right half of the depth visualization (pure depth part) + depth_image_path = view_data.get("depth_image", None) + depth_right_half = None + + if depth_image_path and os.path.exists(depth_image_path): + try: + # Load the combined depth visualization image + depth_combined = cv2.imread(depth_image_path) + depth_combined = cv2.cvtColor(depth_combined, cv2.COLOR_BGR2RGB) + if depth_combined is not None: + height, width = depth_combined.shape[:2] + # Extract right half (depth visualization part) + depth_right_half = depth_combined[:, width // 2 :] + except Exception as e: + print(f"Error extracting depth right half: {e}") + + return image, depth_right_half, [] + + def navigate_measure_view( + self, + processed_data: Optional[dict], + current_selector_value: str, + direction: int, + ) -> Tuple[str, Optional[np.ndarray], Optional[str], List]: + """ + Navigate measure view (direction: -1 for previous, +1 for next). + + Args: + processed_data: Processed data dictionary + current_selector_value: Current selector value + direction: Direction to navigate (-1 for previous, +1 for next) + + Returns: + Tuple of (new_selector_value, measure_image, depth_image_path, measure_points) + """ + if processed_data is None or len(processed_data) == 0: + return "View 1", None, None, [] + + # Parse current view number + try: + current_view = int(current_selector_value.split()[1]) - 1 + except: # noqa + current_view = 0 + + num_views = len(processed_data) + new_view = (current_view + direction) % num_views + + new_selector_value = f"View {new_view + 1}" + measure_image, depth_right_half, measure_points = self.update_measure_view( + processed_data, new_view + ) + + return new_selector_value, measure_image, depth_right_half, measure_points + + def populate_visualization_tabs( + self, processed_data: Optional[dict] + ) -> Tuple[Optional[str], Optional[np.ndarray], Optional[str], List]: + """ + Populate the depth and measure tabs with processed data. + + Args: + processed_data: Processed data dictionary + + Returns: + Tuple of (depth_vis, measure_img, depth_image_path, measure_points) + """ + if processed_data is None or len(processed_data) == 0: + return None, None, None, [] + + # Use update function to get depth visualization + depth_vis = self.update_depth_view(processed_data, 0) + measure_img, depth_right_half, _ = self.update_measure_view(processed_data, 0) + + return depth_vis, measure_img, depth_right_half, [] + + def reset_measure( + self, processed_data: Optional[dict] + ) -> Tuple[Optional[np.ndarray], List, str]: + """ + Reset measure points. + + Args: + processed_data: Processed data dictionary + + Returns: + Tuple of (image, measure_points, text) + """ + if processed_data is None or len(processed_data) == 0: + return None, [], "" + + # Return the first view image + first_view = list(processed_data.values())[0] + return first_view["image"], [], "" + + def measure( + self, + processed_data: Optional[dict], + measure_points: List, + current_view_selector: str, + event: gr.SelectData, + ) -> List: + """ + Handle measurement on images. + + Args: + processed_data: Processed data dictionary + measure_points: List of current measure points + current_view_selector: Current view selector value + event: Gradio select event + + Returns: + List of [image, depth_right_half, measure_points, text] + """ + try: + print(f"Measure function called with selector: {current_view_selector}") + + if processed_data is None or len(processed_data) == 0: + return [None, [], "No data available"] + + # Use the currently selected view instead of always using the first view + try: + current_view_index = int(current_view_selector.split()[1]) - 1 + except: # noqa + current_view_index = 0 + + print(f"Using view index: {current_view_index}") + + # Get view data safely + if current_view_index < 0 or current_view_index >= len(processed_data): + current_view_index = 0 + + view_keys = list(processed_data.keys()) + current_view = processed_data[view_keys[current_view_index]] + + if current_view is None: + return [None, [], "No view data available"] + + point2d = event.index[0], event.index[1] + print(f"Clicked point: {point2d}") + + measure_points.append(point2d) + + # Get image and depth visualization + image, depth_right_half, _ = self.update_measure_view( + processed_data, current_view_index + ) + if image is None: + return [None, [], "No image available"] + + image = image.copy() + + # Ensure image is in uint8 format for proper cv2 operations + try: + if image.dtype != np.uint8: + if image.max() <= 1.0: + # Image is in [0, 1] range, convert to [0, 255] + image = (image * 255).astype(np.uint8) + else: + # Image is already in [0, 255] range + image = image.astype(np.uint8) + except Exception as e: + print(f"Image conversion error: {e}") + return [None, [], f"Image conversion error: {e}"] + + # Draw circles for points + try: + for p in measure_points: + if 0 <= p[0] < image.shape[1] and 0 <= p[1] < image.shape[0]: + image = cv2.circle(image, p, radius=5, color=(255, 0, 0), thickness=2) + except Exception as e: + print(f"Drawing error: {e}") + return [None, [], f"Drawing error: {e}"] + + # Get depth information from processed_data + depth_text = "" + try: + for i, p in enumerate(measure_points): + if ( + current_view["depth"] is not None + and 0 <= p[1] < current_view["depth"].shape[0] + and 0 <= p[0] < current_view["depth"].shape[1] + ): + d = current_view["depth"][p[1], p[0]] + depth_text += f"- **P{i + 1} depth: {d:.2f}m**\n" + else: + depth_text += f"- **P{i + 1}: Click position ({p[0]}, {p[1]}) - No depth information**\n" # noqa: E501 + except Exception as e: + print(f"Depth text error: {e}") + depth_text = f"Error computing depth: {e}\n" + + if len(measure_points) == 2: + try: + point1, point2 = measure_points + # Draw line + if ( + 0 <= point1[0] < image.shape[1] + and 0 <= point1[1] < image.shape[0] + and 0 <= point2[0] < image.shape[1] + and 0 <= point2[1] < image.shape[0] + ): + image = cv2.line(image, point1, point2, color=(255, 0, 0), thickness=2) + + # Compute 3D distance using depth information and camera intrinsics + distance_text = "- **Distance: Unable to calculate 3D distance**" + if ( + current_view["depth"] is not None + and 0 <= point1[1] < current_view["depth"].shape[0] + and 0 <= point1[0] < current_view["depth"].shape[1] + and 0 <= point2[1] < current_view["depth"].shape[0] + and 0 <= point2[0] < current_view["depth"].shape[1] + ): + try: + # Get depth values at the two points + d1 = current_view["depth"][point1[1], point1[0]] + d2 = current_view["depth"][point2[1], point2[0]] + + # Convert 2D pixel coordinates to 3D world coordinates + if current_view["intrinsics"] is not None: + # Get camera intrinsics + K = current_view["intrinsics"] # 3x3 intrinsic matrix + fx, fy = K[0, 0], K[1, 1] # focal lengths + cx, cy = K[0, 2], K[1, 2] # principal point + + # Convert pixel coordinates to normalized camera coordinates + # Point 1: (u1, v1) -> (x1, y1, z1) + u1, v1 = point1[0], point1[1] + x1 = (u1 - cx) * d1 / fx + y1 = (v1 - cy) * d1 / fy + z1 = d1 + + # Point 2: (u2, v2) -> (x2, y2, z2) + u2, v2 = point2[0], point2[1] + x2 = (u2 - cx) * d2 / fx + y2 = (v2 - cy) * d2 / fy + z2 = d2 + + # Calculate 3D Euclidean distance + p1_3d = np.array([x1, y1, z1]) + p2_3d = np.array([x2, y2, z2]) + distance_3d = np.linalg.norm(p1_3d - p2_3d) + + distance_text = f"- **Distance: {distance_3d:.2f}m**" + else: + # Fallback to simplified calculation if no intrinsics + pixel_distance = np.sqrt( + (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2 + ) + avg_depth = (d1 + d2) / 2 + scale_factor = avg_depth / 1000 # Rough scaling factor + estimated_3d_distance = pixel_distance * scale_factor + distance_text = f"- **Distance: {estimated_3d_distance:.2f}m (estimated, no intrinsics)**" # noqa: E501 + + except Exception as e: + print(f"Distance computation error: {e}") + distance_text = f"- **Distance computation error: {e}**" + + measure_points = [] + text = depth_text + distance_text + print(f"Measurement complete: {text}") + return [image, depth_right_half, measure_points, text] + except Exception as e: + print(f"Final measurement error: {e}") + return [None, [], f"Measurement error: {e}"] + else: + print(f"Single point measurement: {depth_text}") + return [image, depth_right_half, measure_points, depth_text] + + except Exception as e: + print(f"Overall measure function error: {e}") + return [None, [], f"Measure function error: {e}"] diff --git a/src/depth_anything_3/cache.py b/src/depth_anything_3/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..1e2d63982e2994c517ca70f38ac79c9a90e46e8d --- /dev/null +++ b/src/depth_anything_3/cache.py @@ -0,0 +1,190 @@ +""" +Model caching utilities for Depth Anything 3. + +Provides model caching functionality to avoid reloading model weights on every instantiation. +This significantly reduces latency for repeated model creation (2-5s gain). +""" + +from __future__ import annotations + +import threading +from typing import Dict, Optional, Tuple + +import torch +import torch.nn as nn + +from depth_anything_3.utils.logger import logger + + +class ModelCache: + """ + Thread-safe singleton cache for Depth Anything 3 models. + + Caches loaded model weights to avoid reloading from disk on every instantiation. + Each unique combination of (model_name, device) is cached separately. + + Usage: + cache = ModelCache() + model = cache.get(model_name, device, loader_fn) + # loader_fn is only called if cache miss + + Thread Safety: + Uses threading.Lock to ensure thread-safe access to cache. + + Memory Management: + - Models are kept in cache until explicitly cleared + - Use clear() to free memory when needed + - Use clear_device() to clear specific device models + """ + + _instance: Optional["ModelCache"] = None + _lock = threading.Lock() + + def __new__(cls): + """Singleton pattern to ensure single cache instance.""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + """Initialize cache storage.""" + if self._initialized: + return + + self._cache: Dict[Tuple[str, str], nn.Module] = {} + self._cache_lock = threading.Lock() + self._initialized = True + logger.info("ModelCache initialized") + + def get( + self, + model_name: str, + device: torch.device | str, + loader_fn: callable, + ) -> nn.Module: + """ + Get cached model or load if not in cache. + + Args: + model_name: Name of the model (e.g., "da3-large") + device: Target device (cuda, mps, cpu) + loader_fn: Function to load model if cache miss + Should return nn.Module + + Returns: + Cached or freshly loaded model on specified device + + Example: + >>> cache = ModelCache() + >>> model = cache.get( + ... "da3-large", + ... "cuda", + ... lambda: create_model() + ... ) + """ + device_str = str(device) + cache_key = (model_name, device_str) + + with self._cache_lock: + if cache_key in self._cache: + logger.debug(f"Model cache HIT: {model_name} on {device_str}") + return self._cache[cache_key] + + logger.info(f"Model cache MISS: {model_name} on {device_str}. Loading...") + model = loader_fn() + self._cache[cache_key] = model + logger.info(f"Model cached: {model_name} on {device_str}") + + return model + + def clear(self) -> None: + """ + Clear entire cache and free memory. + + Removes all cached models and forces garbage collection. + Useful when switching between many different models. + """ + with self._cache_lock: + num_cached = len(self._cache) + self._cache.clear() + + # Force garbage collection to free GPU memory + import gc + + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + if hasattr(torch, "mps") and torch.backends.mps.is_available(): + torch.mps.empty_cache() + + logger.info(f"Model cache cleared ({num_cached} models removed)") + + def clear_device(self, device: torch.device | str) -> None: + """ + Clear all models on specific device. + + Args: + device: Device to clear (e.g., "cuda", "mps", "cpu") + + Example: + >>> cache = ModelCache() + >>> cache.clear_device("cuda") # Clear all CUDA models + """ + device_str = str(device) + + with self._cache_lock: + keys_to_remove = [key for key in self._cache if key[1] == device_str] + for key in keys_to_remove: + del self._cache[key] + + # Free device memory + if "cuda" in device_str and torch.cuda.is_available(): + torch.cuda.empty_cache() + elif "mps" in device_str and hasattr(torch, "mps") and torch.backends.mps.is_available(): + torch.mps.empty_cache() + + logger.info(f"Model cache cleared for device {device_str} ({len(keys_to_remove)} models removed)") + + def get_cache_info(self) -> Dict[str, int]: + """ + Get cache statistics. + + Returns: + Dictionary with cache info: + - total: Total number of cached models + - by_device: Number of models per device + """ + with self._cache_lock: + info = { + "total": len(self._cache), + "by_device": {}, + } + + for model_name, device_str in self._cache.keys(): + if device_str not in info["by_device"]: + info["by_device"][device_str] = 0 + info["by_device"][device_str] += 1 + + return info + + +# Global singleton instance +_global_cache = ModelCache() + + +def get_model_cache() -> ModelCache: + """ + Get global model cache instance. + + Returns: + Singleton ModelCache instance + + Example: + >>> from depth_anything_3.cache import get_model_cache + >>> cache = get_model_cache() + >>> cache.clear() + """ + return _global_cache diff --git a/src/depth_anything_3/cfg.py b/src/depth_anything_3/cfg.py new file mode 100644 index 0000000000000000000000000000000000000000..60b1e14d0001cbc8cf52d09564b1a32d23d1d2cb --- /dev/null +++ b/src/depth_anything_3/cfg.py @@ -0,0 +1,145 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Configuration utility functions +""" + +import importlib +from pathlib import Path +from typing import Any, Callable, List, Union + +from omegaconf import DictConfig, ListConfig, OmegaConf + +try: + OmegaConf.register_new_resolver("eval", eval) +except Exception as e: + # if eval is not available, we can just pass + print(f"Error registering eval resolver: {e}") + + +def load_config(path: str, argv: List[str] = None) -> Union[DictConfig, ListConfig]: + """ + Load a configuration. Will resolve inheritance. + Supports both file paths and module paths (e.g., depth_anything_3.configs.giant). + """ + # Check if path is a module path (contains dots but no slashes and doesn't end with .yaml) + if "." in path and "/" not in path and not path.endswith(".yaml"): + # It's a module path, load from package resources + path_parts = path.split(".")[1:] + config_path = Path(__file__).resolve().parent + for part in path_parts: + config_path = config_path.joinpath(part) + config_path = config_path.with_suffix(".yaml") + config = OmegaConf.load(str(config_path)) + else: + # It's a file path (absolute, relative, or with .yaml extension) + config = OmegaConf.load(path) + + if argv is not None: + config_argv = OmegaConf.from_dotlist(argv) + config = OmegaConf.merge(config, config_argv) + config = resolve_recursive(config, resolve_inheritance) + return config + + +def resolve_recursive( + config: Any, + resolver: Callable[[Union[DictConfig, ListConfig]], Union[DictConfig, ListConfig]], +) -> Any: + config = resolver(config) + if isinstance(config, DictConfig): + for k in config.keys(): + v = config.get(k) + if isinstance(v, (DictConfig, ListConfig)): + config[k] = resolve_recursive(v, resolver) + if isinstance(config, ListConfig): + for i in range(len(config)): + v = config.get(i) + if isinstance(v, (DictConfig, ListConfig)): + config[i] = resolve_recursive(v, resolver) + return config + + +def resolve_inheritance(config: Union[DictConfig, ListConfig]) -> Any: + """ + Recursively resolve inheritance if the config contains: + __inherit__: path/to/parent.yaml or a ListConfig of such paths. + """ + if isinstance(config, DictConfig): + inherit = config.pop("__inherit__", None) + + if inherit: + inherit_list = inherit if isinstance(inherit, ListConfig) else [inherit] + + parent_config = None + for parent_path in inherit_list: + assert isinstance(parent_path, str) + parent_config = ( + load_config(parent_path) + if parent_config is None + else OmegaConf.merge(parent_config, load_config(parent_path)) + ) + + if len(config.keys()) > 0: + config = OmegaConf.merge(parent_config, config) + else: + config = parent_config + return config + + +def import_item(path: str, name: str) -> Any: + """ + Import a python item. Example: import_item("path.to.file", "MyClass") -> MyClass + """ + return getattr(importlib.import_module(path), name) + + +def create_object(config: DictConfig) -> Any: + """ + Create an object from config. + The config is expected to contains the following: + __object__: + path: path.to.module + name: MyClass + args: as_config | as_params (default to as_config) + """ + config = DictConfig(config) + item = import_item( + path=config.__object__.path, + name=config.__object__.name, + ) + args = config.__object__.get("args", "as_config") + if args == "as_config": + return item(config) + if args == "as_params": + config = OmegaConf.to_object(config) + config.pop("__object__") + return item(**config) + raise NotImplementedError(f"Unknown args type: {args}") + + +def create_dataset(path: str, *args, **kwargs) -> Any: + """ + Create a dataset. Requires the file to contain a "create_dataset" function. + """ + return import_item(path, "create_dataset")(*args, **kwargs) + + +def to_dict_recursive(config_obj): + if isinstance(config_obj, DictConfig): + return {k: to_dict_recursive(v) for k, v in config_obj.items()} + elif isinstance(config_obj, ListConfig): + return [to_dict_recursive(item) for item in config_obj] + return config_obj diff --git a/src/depth_anything_3/cli.py b/src/depth_anything_3/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..d9f421f8948a54dc647bb494be3eacefca7b6dc8 --- /dev/null +++ b/src/depth_anything_3/cli.py @@ -0,0 +1,905 @@ +# flake8: noqa: E402 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Refactored Depth Anything 3 CLI +Clean, modular command-line interface +""" + +from __future__ import annotations + +import os + +import typer + +from depth_anything_3.services import start_server +from depth_anything_3.services.gallery import gallery as gallery_main +from depth_anything_3.services.inference_service import run_inference +from depth_anything_3.services.input_handlers import ( + ColmapHandler, + ImageHandler, + ImagesHandler, + InputHandler, + VideoHandler, + parse_export_feat, +) +from depth_anything_3.utils.constants import ( + DEFAULT_EXPORT_DIR, + DEFAULT_GALLERY_DIR, + DEFAULT_GRADIO_DIR, + DEFAULT_MODEL, +) + +os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" + +app = typer.Typer(help="Depth Anything 3 - Video depth estimation CLI", add_completion=False) + + +# ============================================================================ +# Input type detection utilities +# ============================================================================ + +# Supported file extensions +IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".tif"} +VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm", ".m4v"} + + +def detect_input_type(input_path: str) -> str: + """ + Detect input type from path. + + Returns: + - "image": Single image file + - "images": Directory containing images + - "video": Video file + - "colmap": COLMAP directory structure + - "unknown": Cannot determine type + """ + if not os.path.exists(input_path): + return "unknown" + + # Check if it's a file + if os.path.isfile(input_path): + ext = os.path.splitext(input_path)[1].lower() + if ext in IMAGE_EXTENSIONS: + return "image" + elif ext in VIDEO_EXTENSIONS: + return "video" + return "unknown" + + # Check if it's a directory + if os.path.isdir(input_path): + # Check for COLMAP structure + images_dir = os.path.join(input_path, "images") + sparse_dir = os.path.join(input_path, "sparse") + + if os.path.isdir(images_dir) and os.path.isdir(sparse_dir): + return "colmap" + + # Check if directory contains image files + for item in os.listdir(input_path): + item_path = os.path.join(input_path, item) + if os.path.isfile(item_path): + ext = os.path.splitext(item)[1].lower() + if ext in IMAGE_EXTENSIONS: + return "images" + + return "unknown" + + return "unknown" + + +# ============================================================================ +# Common parameters and configuration +# ============================================================================ + +# ============================================================================ +# Inference commands +# ============================================================================ + + +@app.command() +def auto( + input_path: str = typer.Argument( + ..., help="Path to input (image, directory, video, or COLMAP)" + ), + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + export_dir: str = typer.Option(DEFAULT_EXPORT_DIR, help="Export directory"), + export_format: str = typer.Option("glb", help="Export format"), + device: str = typer.Option("cuda", help="Device to use"), + use_backend: bool = typer.Option(False, help="Use backend service for inference"), + backend_url: str = typer.Option( + "http://localhost:8008", help="Backend URL (default: http://localhost:8008)" + ), + process_res: int = typer.Option(504, help="Processing resolution"), + process_res_method: str = typer.Option( + "upper_bound_resize", help="Processing resolution method" + ), + export_feat: str = typer.Option( + "", + help="[FEAT_VIS]Export features from specified layers using comma-separated indices (e.g., '0,1,2').", + ), + auto_cleanup: bool = typer.Option( + False, help="Automatically clean export directory if it exists (no prompt)" + ), + # Batching options + batch_size: str = typer.Option( + "all", + help="Batch size: 'all' (single batch), 'auto' (adaptive GPU memory), or integer (fixed size)", + ), + max_batch_size: int = typer.Option( + 64, help="[Batching] Maximum batch size for adaptive batching" + ), + target_memory_utilization: float = typer.Option( + 0.85, help="[Batching] Target GPU memory usage (0.0-1.0) for adaptive batching" + ), + # Video-specific options + fps: float = typer.Option(1.0, help="[Video] Sampling FPS for frame extraction"), + # COLMAP-specific options + sparse_subdir: str = typer.Option( + "", help="[COLMAP] Sparse reconstruction subdirectory (e.g., '0' for sparse/0/)" + ), + align_to_input_ext_scale: bool = typer.Option( + True, help="[COLMAP] Align prediction to input extrinsics scale" + ), + # Pose estimation options + use_ray_pose: bool = typer.Option( + False, help="Use ray-based pose estimation instead of camera decoder" + ), + ref_view_strategy: str = typer.Option( + "saddle_balanced", + help="Reference view selection strategy: empty, first, middle, saddle_balanced, saddle_sim_range", + ), + # GLB export options + conf_thresh_percentile: float = typer.Option( + 40.0, help="[GLB] Lower percentile for adaptive confidence threshold" + ), + num_max_points: int = typer.Option( + 1_000_000, help="[GLB] Maximum number of points in the point cloud" + ), + show_cameras: bool = typer.Option( + True, help="[GLB] Show camera wireframes in the exported scene" + ), + # Feat_vis export options + feat_vis_fps: int = typer.Option(15, help="[FEAT_VIS] Frame rate for output video"), +): + """ + Automatically detect input type and run appropriate processing. + + Supports: + - Single image file (.jpg, .png, etc.) + - Directory of images + - Video file (.mp4, .avi, etc.) + - COLMAP directory (with 'images' and 'sparse' subdirectories) + """ + # Detect input type + input_type = detect_input_type(input_path) + + if input_type == "unknown": + typer.echo(f"❌ Error: Cannot determine input type for: {input_path}", err=True) + typer.echo("Supported inputs:", err=True) + typer.echo(" - Single image file (.jpg, .png, etc.)", err=True) + typer.echo(" - Directory containing images", err=True) + typer.echo(" - Video file (.mp4, .avi, etc.)", err=True) + typer.echo(" - COLMAP directory (with 'images/' and 'sparse/' subdirectories)", err=True) + raise typer.Exit(1) + + # Display detected type + typer.echo(f"🔍 Detected input type: {input_type.upper()}") + typer.echo(f"📁 Input path: {input_path}") + typer.echo() + + # Determine backend URL based on use_backend flag + final_backend_url = backend_url if use_backend else None + + # Parse export_feat parameter + export_feat_layers = parse_export_feat(export_feat) + + # Parse batch_size: convert to int if numeric, otherwise keep as string ("all" or "auto") + parsed_batch_size: int | str = batch_size + if batch_size not in ("all", "auto"): + try: + parsed_batch_size = int(batch_size) + except ValueError: + typer.echo(f"Invalid batch_size: {batch_size}. Use 'all', 'auto', or an integer.", err=True) + raise typer.Exit(1) + + # Route to appropriate handler + if input_type == "image": + typer.echo("Processing single image...") + # Process input + image_files = ImageHandler.process(input_path) + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + elif input_type == "images": + typer.echo("Processing directory of images...") + # Process input - use default extensions + image_files = ImagesHandler.process(input_path, "png,jpg,jpeg") + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + elif input_type == "video": + typer.echo(f"Processing video with FPS={fps}...") + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Process input + image_files = VideoHandler.process(input_path, export_dir, fps) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + elif input_type == "colmap": + typer.echo( + f"Processing COLMAP directory (sparse subdirectory: '{sparse_subdir or 'default'}')..." + ) + # Process input + image_files, extrinsics, intrinsics = ColmapHandler.process(input_path, sparse_subdir) + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + extrinsics=extrinsics, + intrinsics=intrinsics, + align_to_input_ext_scale=align_to_input_ext_scale, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + typer.echo() + typer.echo("✅ Processing completed successfully!") + + +@app.command() +def image( + image_path: str = typer.Argument(..., help="Path to input image file"), + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + export_dir: str = typer.Option(DEFAULT_EXPORT_DIR, help="Export directory"), + export_format: str = typer.Option("glb", help="Export format"), + device: str = typer.Option("cuda", help="Device to use"), + use_backend: bool = typer.Option(False, help="Use backend service for inference"), + backend_url: str = typer.Option( + "http://localhost:8008", help="Backend URL (default: http://localhost:8008)" + ), + process_res: int = typer.Option(504, help="Processing resolution"), + process_res_method: str = typer.Option( + "upper_bound_resize", help="Processing resolution method" + ), + export_feat: str = typer.Option( + "", + help="[FEAT_VIS] Export features from specified layers using comma-separated indices (e.g., '0,1,2').", + ), + auto_cleanup: bool = typer.Option( + False, help="Automatically clean export directory if it exists (no prompt)" + ), + # Pose estimation options + use_ray_pose: bool = typer.Option( + False, help="Use ray-based pose estimation instead of camera decoder" + ), + ref_view_strategy: str = typer.Option( + "saddle_balanced", + help="Reference view selection strategy: empty, first, middle, saddle_balanced, saddle_sim_range", + ), + # GLB export options + conf_thresh_percentile: float = typer.Option( + 40.0, help="[GLB] Lower percentile for adaptive confidence threshold" + ), + num_max_points: int = typer.Option( + 1_000_000, help="[GLB] Maximum number of points in the point cloud" + ), + show_cameras: bool = typer.Option( + True, help="[GLB] Show camera wireframes in the exported scene" + ), + # Feat_vis export options + feat_vis_fps: int = typer.Option(15, help="[FEAT_VIS] Frame rate for output video"), +): + """Run camera pose and depth estimation on a single image.""" + # Process input + image_files = ImageHandler.process(image_path) + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Parse export_feat parameter + export_feat_layers = parse_export_feat(export_feat) + + # Determine backend URL based on use_backend flag + final_backend_url = backend_url if use_backend else None + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + ) + + +@app.command() +def images( + images_dir: str = typer.Argument(..., help="Path to directory containing input images"), + image_extensions: str = typer.Option( + "png,jpg,jpeg", help="Comma-separated image file extensions to process" + ), + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + export_dir: str = typer.Option(DEFAULT_EXPORT_DIR, help="Export directory"), + export_format: str = typer.Option("glb", help="Export format"), + device: str = typer.Option("cuda", help="Device to use"), + use_backend: bool = typer.Option(False, help="Use backend service for inference"), + backend_url: str = typer.Option( + "http://localhost:8008", help="Backend URL (default: http://localhost:8008)" + ), + process_res: int = typer.Option(504, help="Processing resolution"), + process_res_method: str = typer.Option( + "upper_bound_resize", help="Processing resolution method" + ), + export_feat: str = typer.Option( + "", + help="[FEAT_VIS] Export features from specified layers using comma-separated indices (e.g., '0,1,2').", + ), + auto_cleanup: bool = typer.Option( + False, help="Automatically clean export directory if it exists (no prompt)" + ), + # Batching options + batch_size: str = typer.Option( + "all", + help="Batch size: 'all' (single batch), 'auto' (adaptive GPU memory), or integer (fixed size)", + ), + max_batch_size: int = typer.Option( + 64, help="[Batching] Maximum batch size for adaptive batching" + ), + target_memory_utilization: float = typer.Option( + 0.85, help="[Batching] Target GPU memory usage (0.0-1.0) for adaptive batching" + ), + # Pose estimation options + use_ray_pose: bool = typer.Option( + False, help="Use ray-based pose estimation instead of camera decoder" + ), + ref_view_strategy: str = typer.Option( + "saddle_balanced", + help="Reference view selection strategy: empty, first, middle, saddle_balanced, saddle_sim_range", + ), + # GLB export options + conf_thresh_percentile: float = typer.Option( + 40.0, help="[GLB] Lower percentile for adaptive confidence threshold" + ), + num_max_points: int = typer.Option( + 1_000_000, help="[GLB] Maximum number of points in the point cloud" + ), + show_cameras: bool = typer.Option( + True, help="[GLB] Show camera wireframes in the exported scene" + ), + # Feat_vis export options + feat_vis_fps: int = typer.Option(15, help="[FEAT_VIS] Frame rate for output video"), +): + """Run camera pose and depth estimation on a directory of images.""" + # Process input + image_files = ImagesHandler.process(images_dir, image_extensions) + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Parse export_feat parameter + export_feat_layers = parse_export_feat(export_feat) + + # Determine backend URL based on use_backend flag + final_backend_url = backend_url if use_backend else None + + # Parse batch_size + parsed_batch_size: int | str = batch_size + if batch_size not in ("all", "auto"): + try: + parsed_batch_size = int(batch_size) + except ValueError: + typer.echo(f"Invalid batch_size: {batch_size}. Use 'all', 'auto', or an integer.", err=True) + raise typer.Exit(1) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + +@app.command() +def colmap( + colmap_dir: str = typer.Argument( + ..., help="Path to COLMAP directory containing 'images' and 'sparse' subdirectories" + ), + sparse_subdir: str = typer.Option( + "", help="Sparse reconstruction subdirectory (e.g., '0' for sparse/0/, empty for sparse/)" + ), + align_to_input_ext_scale: bool = typer.Option( + True, help="Align prediction to input extrinsics scale" + ), + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + export_dir: str = typer.Option(DEFAULT_EXPORT_DIR, help="Export directory"), + export_format: str = typer.Option("glb", help="Export format"), + device: str = typer.Option("cuda", help="Device to use"), + use_backend: bool = typer.Option(False, help="Use backend service for inference"), + backend_url: str = typer.Option( + "http://localhost:8008", help="Backend URL (default: http://localhost:8008)" + ), + process_res: int = typer.Option(504, help="Processing resolution"), + process_res_method: str = typer.Option( + "upper_bound_resize", help="Processing resolution method" + ), + export_feat: str = typer.Option( + "", + help="Export features from specified layers using comma-separated indices (e.g., '0,1,2').", + ), + auto_cleanup: bool = typer.Option( + False, help="Automatically clean export directory if it exists (no prompt)" + ), + # Batching options + batch_size: str = typer.Option( + "all", + help="Batch size: 'all' (single batch), 'auto' (adaptive GPU memory), or integer (fixed size)", + ), + max_batch_size: int = typer.Option( + 64, help="[Batching] Maximum batch size for adaptive batching" + ), + target_memory_utilization: float = typer.Option( + 0.85, help="[Batching] Target GPU memory usage (0.0-1.0) for adaptive batching" + ), + # Pose estimation options + use_ray_pose: bool = typer.Option( + False, help="Use ray-based pose estimation instead of camera decoder" + ), + ref_view_strategy: str = typer.Option( + "saddle_balanced", + help="Reference view selection strategy: empty, first, middle, saddle_balanced, saddle_sim_range", + ), + # GLB export options + conf_thresh_percentile: float = typer.Option( + 40.0, help="[GLB] Lower percentile for adaptive confidence threshold" + ), + num_max_points: int = typer.Option( + 1_000_000, help="[GLB] Maximum number of points in the point cloud" + ), + show_cameras: bool = typer.Option( + True, help="[GLB] Show camera wireframes in the exported scene" + ), + # Feat_vis export options + feat_vis_fps: int = typer.Option(15, help="[FEAT_VIS] Frame rate for output video"), +): + """Run pose conditioned depth estimation on COLMAP data.""" + # Process input + image_files, extrinsics, intrinsics = ColmapHandler.process(colmap_dir, sparse_subdir) + + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Parse export_feat parameter + export_feat_layers = parse_export_feat(export_feat) + + # Determine backend URL based on use_backend flag + final_backend_url = backend_url if use_backend else None + + # Parse batch_size + parsed_batch_size: int | str = batch_size + if batch_size not in ("all", "auto"): + try: + parsed_batch_size = int(batch_size) + except ValueError: + typer.echo(f"Invalid batch_size: {batch_size}. Use 'all', 'auto', or an integer.", err=True) + raise typer.Exit(1) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + extrinsics=extrinsics, + intrinsics=intrinsics, + align_to_input_ext_scale=align_to_input_ext_scale, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + +@app.command() +def video( + video_path: str = typer.Argument(..., help="Path to input video file"), + fps: float = typer.Option(1.0, help="Sampling FPS for frame extraction"), + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + export_dir: str = typer.Option(DEFAULT_EXPORT_DIR, help="Export directory"), + export_format: str = typer.Option("glb", help="Export format"), + device: str = typer.Option("cuda", help="Device to use"), + use_backend: bool = typer.Option(False, help="Use backend service for inference"), + backend_url: str = typer.Option( + "http://localhost:8008", help="Backend URL (default: http://localhost:8008)" + ), + process_res: int = typer.Option(504, help="Processing resolution"), + process_res_method: str = typer.Option( + "upper_bound_resize", help="Processing resolution method" + ), + export_feat: str = typer.Option( + "", + help="[FEAT_VIS] Export features from specified layers using comma-separated indices (e.g., '0,1,2').", + ), + auto_cleanup: bool = typer.Option( + False, help="Automatically clean export directory if it exists (no prompt)" + ), + # Batching options + batch_size: str = typer.Option( + "all", + help="Batch size: 'all' (single batch), 'auto' (adaptive GPU memory), or integer (fixed size)", + ), + max_batch_size: int = typer.Option( + 64, help="[Batching] Maximum batch size for adaptive batching" + ), + target_memory_utilization: float = typer.Option( + 0.85, help="[Batching] Target GPU memory usage (0.0-1.0) for adaptive batching" + ), + # Pose estimation options + use_ray_pose: bool = typer.Option( + False, help="Use ray-based pose estimation instead of camera decoder" + ), + ref_view_strategy: str = typer.Option( + "saddle_balanced", + help="Reference view selection strategy: empty, first, middle, saddle_balanced, saddle_sim_range", + ), + # GLB export options + conf_thresh_percentile: float = typer.Option( + 40.0, help="[GLB] Lower percentile for adaptive confidence threshold" + ), + num_max_points: int = typer.Option( + 1_000_000, help="[GLB] Maximum number of points in the point cloud" + ), + show_cameras: bool = typer.Option( + True, help="[GLB] Show camera wireframes in the exported scene" + ), + # Feat_vis export options + feat_vis_fps: int = typer.Option(15, help="[FEAT_VIS] Frame rate for output video"), +): + """Run depth estimation on video by extracting frames and processing them.""" + # Handle export directory + export_dir = InputHandler.handle_export_dir(export_dir, auto_cleanup) + + # Process input + image_files = VideoHandler.process(video_path, export_dir, fps) + + # Parse export_feat parameter + export_feat_layers = parse_export_feat(export_feat) + + # Determine backend URL based on use_backend flag + final_backend_url = backend_url if use_backend else None + + # Parse batch_size + parsed_batch_size: int | str = batch_size + if batch_size not in ("all", "auto"): + try: + parsed_batch_size = int(batch_size) + except ValueError: + typer.echo(f"Invalid batch_size: {batch_size}. Use 'all', 'auto', or an integer.", err=True) + raise typer.Exit(1) + + # Run inference + run_inference( + image_paths=image_files, + export_dir=export_dir, + model_dir=model_dir, + device=device, + backend_url=final_backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=parsed_batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) + + +# ============================================================================ +# Service management commands +# ============================================================================ + + +@app.command() +def backend( + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + device: str = typer.Option("cuda", help="Device to use"), + host: str = typer.Option("127.0.0.1", help="Host to bind to"), + port: int = typer.Option(8008, help="Port to bind to"), + gallery_dir: str = typer.Option(DEFAULT_GALLERY_DIR, help="Gallery directory path (optional)"), +): + """Start model backend service with integrated gallery.""" + typer.echo("=" * 60) + typer.echo("🚀 Starting Depth Anything 3 Backend Server") + typer.echo("=" * 60) + typer.echo(f"Model directory: {model_dir}") + typer.echo(f"Device: {device}") + + # Check if gallery directory exists + if gallery_dir and os.path.exists(gallery_dir): + typer.echo(f"Gallery directory: {gallery_dir}") + else: + gallery_dir = None # Disable gallery if directory doesn't exist + + typer.echo() + typer.echo("📡 Server URLs (Ctrl/CMD+Click to open):") + typer.echo(f" 🏠 Home: http://{host}:{port}") + typer.echo(f" 📊 Dashboard: http://{host}:{port}/dashboard") + typer.echo(f" 📈 API Status: http://{host}:{port}/status") + + if gallery_dir: + typer.echo(f" 🎨 Gallery: http://{host}:{port}/gallery/") + + typer.echo("=" * 60) + + try: + start_server(model_dir, device, host, port, gallery_dir) + except KeyboardInterrupt: + typer.echo("\n👋 Backend server stopped.") + except Exception as e: + typer.echo(f"❌ Failed to start backend: {e}") + raise typer.Exit(1) + + +# ============================================================================ +# Application launch commands +# ============================================================================ + + +@app.command() +def gradio( + model_dir: str = typer.Option(DEFAULT_MODEL, help="Model directory path"), + workspace_dir: str = typer.Option(DEFAULT_GRADIO_DIR, help="Workspace directory path"), + gallery_dir: str = typer.Option(DEFAULT_GALLERY_DIR, help="Gallery directory path"), + host: str = typer.Option("127.0.0.1", help="Host address to bind to"), + port: int = typer.Option(7860, help="Port number to bind to"), + share: bool = typer.Option(False, help="Create a public link for the app"), + debug: bool = typer.Option(False, help="Enable debug mode"), + cache_examples: bool = typer.Option( + False, help="Pre-cache all example scenes at startup for faster loading" + ), + cache_gs_tag: str = typer.Option( + "", + help="Tag to match scene names for high-res+3DGS caching (e.g., 'dl3dv'). Scenes containing this tag will use high_res and infer_gs=True; others will use low_res only.", + ), +): + """Launch Depth Anything 3 Gradio interactive web application""" + from depth_anything_3.app.gradio_app import DepthAnything3App + + # Create necessary directories + os.makedirs(workspace_dir, exist_ok=True) + os.makedirs(gallery_dir, exist_ok=True) + + typer.echo("Launching Depth Anything 3 Gradio application...") + typer.echo(f"Model directory: {model_dir}") + typer.echo(f"Workspace directory: {workspace_dir}") + typer.echo(f"Gallery directory: {gallery_dir}") + typer.echo(f"Host: {host}") + typer.echo(f"Port: {port}") + typer.echo(f"Share: {share}") + typer.echo(f"Debug mode: {debug}") + typer.echo(f"Cache examples: {cache_examples}") + if cache_examples: + if cache_gs_tag: + typer.echo( + f"Cache GS Tag: '{cache_gs_tag}' (scenes matching this tag will use high-res + 3DGS)" + ) + else: + typer.echo("Cache GS Tag: None (all scenes will use low-res only)") + + try: + # Initialize and launch application + app = DepthAnything3App( + model_dir=model_dir, workspace_dir=workspace_dir, gallery_dir=gallery_dir + ) + + # Pre-cache examples if requested + if cache_examples: + typer.echo("\n" + "=" * 60) + typer.echo("Pre-caching mode enabled") + if cache_gs_tag: + typer.echo(f"Scenes containing '{cache_gs_tag}' will use HIGH-RES + 3DGS") + typer.echo("Other scenes will use LOW-RES only") + else: + typer.echo("All scenes will use LOW-RES only") + typer.echo("=" * 60) + app.cache_examples( + show_cam=True, + filter_black_bg=False, + filter_white_bg=False, + save_percentage=20.0, + num_max_points=1000, + cache_gs_tag=cache_gs_tag, + gs_trj_mode="smooth", + gs_video_quality="low", + ) + + # Prepare launch arguments + launch_kwargs = {"share": share, "debug": debug} + + app.launch(host=host, port=port, **launch_kwargs) + + except KeyboardInterrupt: + typer.echo("\nGradio application stopped.") + except Exception as e: + typer.echo(f"Failed to launch Gradio application: {e}") + raise typer.Exit(1) + + +@app.command() +def gallery( + gallery_dir: str = typer.Option(DEFAULT_GALLERY_DIR, help="Gallery root directory"), + host: str = typer.Option("127.0.0.1", help="Host address to bind to"), + port: int = typer.Option(8007, help="Port number to bind to"), + open_browser: bool = typer.Option(False, help="Open browser after launch"), +): + """Launch Depth Anything 3 Gallery server""" + + # Validate gallery directory + if not os.path.exists(gallery_dir): + raise typer.BadParameter(f"Gallery directory not found: {gallery_dir}") + + typer.echo("Launching Depth Anything 3 Gallery server...") + typer.echo(f"Gallery directory: {gallery_dir}") + typer.echo(f"Host: {host}") + typer.echo(f"Port: {port}") + typer.echo(f"Auto-open browser: {open_browser}") + + try: + # Set command line arguments + import sys + + sys.argv = ["gallery", "--dir", gallery_dir, "--host", host, "--port", str(port)] + if open_browser: + sys.argv.append("--open") + + # Launch gallery server + gallery_main() + + except KeyboardInterrupt: + typer.echo("\nGallery server stopped.") + except Exception as e: + typer.echo(f"Failed to launch Gallery server: {e}") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() diff --git a/src/depth_anything_3/configs/da3-base.yaml b/src/depth_anything_3/configs/da3-base.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c52a7e5018388a174841469f9a94dc995e14f220 --- /dev/null +++ b/src/depth_anything_3/configs/da3-base.yaml @@ -0,0 +1,45 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitb + out_layers: [5, 7, 9, 11] + alt_start: 4 + qknorm_start: 4 + rope_start: 4 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 1536 + output_dim: 2 + features: &head_features 128 + out_channels: &head_out_channels [96, 192, 384, 768] + + +cam_enc: + __object__: + path: depth_anything_3.model.cam_enc + name: CameraEnc + args: as_params + + dim_out: 768 + +cam_dec: + __object__: + path: depth_anything_3.model.cam_dec + name: CameraDec + args: as_params + + dim_in: 1536 diff --git a/src/depth_anything_3/configs/da3-giant.yaml b/src/depth_anything_3/configs/da3-giant.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f5a75c043353aa3e5b7c5368e4b26416c2b0b8b0 --- /dev/null +++ b/src/depth_anything_3/configs/da3-giant.yaml @@ -0,0 +1,71 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitg + out_layers: [19, 27, 33, 39] + alt_start: 13 + qknorm_start: 13 + rope_start: 13 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 3072 + output_dim: 2 + features: &head_features 256 + out_channels: &head_out_channels [256, 512, 1024, 1024] + + +cam_enc: + __object__: + path: depth_anything_3.model.cam_enc + name: CameraEnc + args: as_params + + dim_out: 1536 + +cam_dec: + __object__: + path: depth_anything_3.model.cam_dec + name: CameraDec + args: as_params + + dim_in: 3072 + + +gs_head: + __object__: + path: depth_anything_3.model.gsdpt + name: GSDPT + args: as_params + + dim_in: *head_dim_in + output_dim: 38 # should align with gs_adapter's setting, for gs params + features: *head_features + out_channels: *head_out_channels + + +gs_adapter: + __object__: + path: depth_anything_3.model.gs_adapter + name: GaussianAdapter + args: as_params + + sh_degree: 2 + pred_color: false # predict SH coefficient if false + pred_offset_depth: true + pred_offset_xy: true + gaussian_scale_min: 1e-5 + gaussian_scale_max: 30.0 diff --git a/src/depth_anything_3/configs/da3-large.yaml b/src/depth_anything_3/configs/da3-large.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4fa367c9d9b46eb9a62aef7041f68c709eb4c6e3 --- /dev/null +++ b/src/depth_anything_3/configs/da3-large.yaml @@ -0,0 +1,45 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitl + out_layers: [11, 15, 19, 23] + alt_start: 8 + qknorm_start: 8 + rope_start: 8 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 2048 + output_dim: 2 + features: &head_features 256 + out_channels: &head_out_channels [256, 512, 1024, 1024] + + +cam_enc: + __object__: + path: depth_anything_3.model.cam_enc + name: CameraEnc + args: as_params + + dim_out: 1024 + +cam_dec: + __object__: + path: depth_anything_3.model.cam_dec + name: CameraDec + args: as_params + + dim_in: 2048 diff --git a/src/depth_anything_3/configs/da3-small.yaml b/src/depth_anything_3/configs/da3-small.yaml new file mode 100644 index 0000000000000000000000000000000000000000..10887437697fc9f2614c03add73fe9858b309d91 --- /dev/null +++ b/src/depth_anything_3/configs/da3-small.yaml @@ -0,0 +1,45 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vits + out_layers: [5, 7, 9, 11] + alt_start: 4 + qknorm_start: 4 + rope_start: 4 + cat_token: True + +head: + __object__: + path: depth_anything_3.model.dualdpt + name: DualDPT + args: as_params + + dim_in: &head_dim_in 768 + output_dim: 2 + features: &head_features 64 + out_channels: &head_out_channels [48, 96, 192, 384] + + +cam_enc: + __object__: + path: depth_anything_3.model.cam_enc + name: CameraEnc + args: as_params + + dim_out: 384 + +cam_dec: + __object__: + path: depth_anything_3.model.cam_dec + name: CameraDec + args: as_params + + dim_in: 768 diff --git a/src/depth_anything_3/configs/da3metric-large.yaml b/src/depth_anything_3/configs/da3metric-large.yaml new file mode 100644 index 0000000000000000000000000000000000000000..124635cfd952c25c8857ee3da63ce0444c4377f2 --- /dev/null +++ b/src/depth_anything_3/configs/da3metric-large.yaml @@ -0,0 +1,28 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitl + out_layers: [4, 11, 17, 23] + alt_start: -1 # -1 means disable + qknorm_start: -1 + rope_start: -1 + cat_token: False + +head: + __object__: + path: depth_anything_3.model.dpt + name: DPT + args: as_params + + dim_in: 1024 + output_dim: 1 + features: 256 + out_channels: [256, 512, 1024, 1024] diff --git a/src/depth_anything_3/configs/da3mono-large.yaml b/src/depth_anything_3/configs/da3mono-large.yaml new file mode 100644 index 0000000000000000000000000000000000000000..124635cfd952c25c8857ee3da63ce0444c4377f2 --- /dev/null +++ b/src/depth_anything_3/configs/da3mono-large.yaml @@ -0,0 +1,28 @@ +__object__: + path: depth_anything_3.model.da3 + name: DepthAnything3Net + args: as_params + +net: + __object__: + path: depth_anything_3.model.dinov2.dinov2 + name: DinoV2 + args: as_params + + name: vitl + out_layers: [4, 11, 17, 23] + alt_start: -1 # -1 means disable + qknorm_start: -1 + rope_start: -1 + cat_token: False + +head: + __object__: + path: depth_anything_3.model.dpt + name: DPT + args: as_params + + dim_in: 1024 + output_dim: 1 + features: 256 + out_channels: [256, 512, 1024, 1024] diff --git a/src/depth_anything_3/configs/da3nested-giant-large.yaml b/src/depth_anything_3/configs/da3nested-giant-large.yaml new file mode 100644 index 0000000000000000000000000000000000000000..595c122b1dc976ecfec58b133b2b60d8f724618c --- /dev/null +++ b/src/depth_anything_3/configs/da3nested-giant-large.yaml @@ -0,0 +1,10 @@ +__object__: + path: depth_anything_3.model.da3 + name: NestedDepthAnything3Net + args: as_params + +anyview: + __inherit__: depth_anything_3.configs.da3-giant + +metric: + __inherit__: depth_anything_3.configs.da3metric-large diff --git a/src/depth_anything_3/model/__init__.py b/src/depth_anything_3/model/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..57a2a45132eeae8d58a11a26036c54feef9cfe16 --- /dev/null +++ b/src/depth_anything_3/model/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from depth_anything_3.model.da3 import DepthAnything3Net, NestedDepthAnything3Net + +__export__ = [ + NestedDepthAnything3Net, + DepthAnything3Net, +] diff --git a/src/depth_anything_3/model/cam_dec.py b/src/depth_anything_3/model/cam_dec.py new file mode 100644 index 0000000000000000000000000000000000000000..3353b403683bf556b3823081863573dc7f5f719e --- /dev/null +++ b/src/depth_anything_3/model/cam_dec.py @@ -0,0 +1,45 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn as nn + + +class CameraDec(nn.Module): + def __init__(self, dim_in=1536): + super().__init__() + output_dim = dim_in + self.backbone = nn.Sequential( + nn.Linear(output_dim, output_dim), + nn.ReLU(), + nn.Linear(output_dim, output_dim), + nn.ReLU(), + ) + self.fc_t = nn.Linear(output_dim, 3) + self.fc_qvec = nn.Linear(output_dim, 4) + self.fc_fov = nn.Sequential(nn.Linear(output_dim, 2), nn.ReLU()) + + def forward(self, feat, camera_encoding=None, *args, **kwargs): + B, N = feat.shape[:2] + feat = feat.reshape(B * N, -1) + feat = self.backbone(feat) + out_t = self.fc_t(feat.float()).reshape(B, N, 3) + if camera_encoding is None: + out_qvec = self.fc_qvec(feat.float()).reshape(B, N, 4) + out_fov = self.fc_fov(feat.float()).reshape(B, N, 2) + else: + out_qvec = camera_encoding[..., 3:7] + out_fov = camera_encoding[..., -2:] + pose_enc = torch.cat([out_t, out_qvec, out_fov], dim=-1) + return pose_enc diff --git a/src/depth_anything_3/model/cam_enc.py b/src/depth_anything_3/model/cam_enc.py new file mode 100644 index 0000000000000000000000000000000000000000..bf28e701442fa73d89e54b409800908c138a93d8 --- /dev/null +++ b/src/depth_anything_3/model/cam_enc.py @@ -0,0 +1,80 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch.nn as nn + +from depth_anything_3.model.utils.attention import Mlp +from depth_anything_3.model.utils.block import Block +from depth_anything_3.model.utils.transform import extri_intri_to_pose_encoding +from depth_anything_3.utils.geometry import affine_inverse + + +class CameraEnc(nn.Module): + """ + CameraHead predicts camera parameters from token representations using iterative refinement. + + It applies a series of transformer blocks (the "trunk") to dedicated camera tokens. + """ + + def __init__( + self, + dim_out: int = 1024, + dim_in: int = 9, + trunk_depth: int = 4, + target_dim: int = 9, + num_heads: int = 16, + mlp_ratio: int = 4, + init_values: float = 0.01, + **kwargs, + ): + super().__init__() + self.target_dim = target_dim + self.trunk_depth = trunk_depth + self.trunk = nn.Sequential( + *[ + Block( + dim=dim_out, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + init_values=init_values, + ) + for _ in range(trunk_depth) + ] + ) + self.token_norm = nn.LayerNorm(dim_out) + self.trunk_norm = nn.LayerNorm(dim_out) + self.pose_branch = Mlp( + in_features=dim_in, + hidden_features=dim_out // 2, + out_features=dim_out, + drop=0, + ) + + def forward( + self, + ext, + ixt, + image_size, + ) -> tuple: + c2ws = affine_inverse(ext) + pose_encoding = extri_intri_to_pose_encoding( + c2ws, + ixt, + image_size, + ) + pose_tokens = self.pose_branch(pose_encoding) + pose_tokens = self.token_norm(pose_tokens) + pose_tokens = self.trunk(pose_tokens) + pose_tokens = self.trunk_norm(pose_tokens) + return pose_tokens diff --git a/src/depth_anything_3/model/da3.py b/src/depth_anything_3/model/da3.py new file mode 100644 index 0000000000000000000000000000000000000000..ca979b7c4111ce2c97f781b9c6d6ff6de1ad55b6 --- /dev/null +++ b/src/depth_anything_3/model/da3.py @@ -0,0 +1,442 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.nn as nn +from addict import Dict +from omegaconf import DictConfig, OmegaConf + +from depth_anything_3.cfg import create_object +from depth_anything_3.model.utils.transform import pose_encoding_to_extri_intri +from depth_anything_3.utils.alignment import ( + apply_metric_scaling, + compute_alignment_mask, + compute_sky_mask, + least_squares_scale_scalar, + sample_tensor_for_quantile, + set_sky_regions_to_max_depth, +) +from depth_anything_3.utils.geometry import affine_inverse, as_homogeneous, map_pdf_to_opacity +from depth_anything_3.utils.ray_utils import get_extrinsic_from_camray + + +def _wrap_cfg(cfg_obj): + return OmegaConf.create(cfg_obj) + + +class DepthAnything3Net(nn.Module): + """ + Depth Anything 3 network for depth estimation and camera pose estimation. + + This network consists of: + - Backbone: DinoV2 feature extractor + - Head: DPT or DualDPT for depth prediction + - Optional camera decoders for pose estimation + - Optional GSDPT for 3DGS prediction + + Args: + preset: Configuration preset containing network dimensions and settings + + Returns: + Dictionary containing: + - depth: Predicted depth map (B, H, W) + - depth_conf: Depth confidence map (B, H, W) + - extrinsics: Camera extrinsics (B, N, 4, 4) + - intrinsics: Camera intrinsics (B, N, 3, 3) + - gaussians: 3D Gaussian Splats (world space), type: model.gs_adapter.Gaussians + - aux: Auxiliary features for specified layers + """ + + # Patch size for feature extraction + PATCH_SIZE = 14 + + def __init__(self, net, head, cam_dec=None, cam_enc=None, gs_head=None, gs_adapter=None): + """ + Initialize DepthAnything3Net with given yaml-initialized configuration. + """ + super().__init__() + self.backbone = net if isinstance(net, nn.Module) else create_object(_wrap_cfg(net)) + self.head = head if isinstance(head, nn.Module) else create_object(_wrap_cfg(head)) + self.cam_dec, self.cam_enc = None, None + if cam_dec is not None: + self.cam_dec = ( + cam_dec if isinstance(cam_dec, nn.Module) else create_object(_wrap_cfg(cam_dec)) + ) + self.cam_enc = ( + cam_enc if isinstance(cam_enc, nn.Module) else create_object(_wrap_cfg(cam_enc)) + ) + self.gs_adapter, self.gs_head = None, None + if gs_head is not None and gs_adapter is not None: + self.gs_adapter = ( + gs_adapter + if isinstance(gs_adapter, nn.Module) + else create_object(_wrap_cfg(gs_adapter)) + ) + gs_out_dim = self.gs_adapter.d_in + 1 + if isinstance(gs_head, nn.Module): + assert ( + gs_head.out_dim == gs_out_dim + ), f"gs_head.out_dim should be {gs_out_dim}, got {gs_head.out_dim}" + self.gs_head = gs_head + else: + assert ( + gs_head["output_dim"] == gs_out_dim + ), f"gs_head output_dim should set to {gs_out_dim}, got {gs_head['output_dim']}" + self.gs_head = create_object(_wrap_cfg(gs_head)) + + def forward( + self, + x: torch.Tensor, + extrinsics: torch.Tensor | None = None, + intrinsics: torch.Tensor | None = None, + export_feat_layers: list[int] | None = [], + infer_gs: bool = False, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + ) -> Dict[str, torch.Tensor]: + """ + Forward pass through the network. + + Args: + x: Input images (B, N, 3, H, W) + extrinsics: Camera extrinsics (B, N, 4, 4) + intrinsics: Camera intrinsics (B, N, 3, 3) + feat_layers: List of layer indices to extract features from + infer_gs: Enable Gaussian Splatting branch + use_ray_pose: Use ray-based pose estimation + ref_view_strategy: Strategy for selecting reference view + + Returns: + Dictionary containing predictions and auxiliary features + """ + # Extract features using backbone + if extrinsics is not None: + with torch.autocast(device_type=x.device.type, enabled=False): + cam_token = self.cam_enc(extrinsics, intrinsics, x.shape[-2:]) + else: + cam_token = None + + feats, aux_feats = self.backbone( + x, cam_token=cam_token, export_feat_layers=export_feat_layers, ref_view_strategy=ref_view_strategy + ) + # feats = [[item for item in feat] for feat in feats] + H, W = x.shape[-2], x.shape[-1] + + # Process features through depth head + with torch.autocast(device_type=x.device.type, enabled=False): + output = self._process_depth_head(feats, H, W) + if use_ray_pose: + output = self._process_ray_pose_estimation(output, H, W) + else: + output = self._process_camera_estimation(feats, H, W, output) + if infer_gs: + output = self._process_gs_head(feats, H, W, output, x, extrinsics, intrinsics) + + output = self._process_mono_sky_estimation(output) + + # Extract auxiliary features if requested + output.aux = self._extract_auxiliary_features(aux_feats, export_feat_layers, H, W) + + return output + + def _process_mono_sky_estimation( + self, output: Dict[str, torch.Tensor] + ) -> Dict[str, torch.Tensor]: + """Process mono sky estimation.""" + if "sky" not in output: + return output + non_sky_mask = compute_sky_mask(output.sky, threshold=0.3) + if non_sky_mask.sum() <= 10: + return output + if (~non_sky_mask).sum() <= 10: + return output + + non_sky_depth = output.depth[non_sky_mask] + if non_sky_depth.numel() > 100000: + idx = torch.randint(0, non_sky_depth.numel(), (100000,), device=non_sky_depth.device) + sampled_depth = non_sky_depth[idx] + else: + sampled_depth = non_sky_depth + non_sky_max = torch.quantile(sampled_depth, 0.99) + + # Set sky regions to maximum depth and high confidence + output.depth, _ = set_sky_regions_to_max_depth( + output.depth, None, non_sky_mask, max_depth=non_sky_max + ) + return output + + def _process_ray_pose_estimation( + self, output: Dict[str, torch.Tensor], height: int, width: int + ) -> Dict[str, torch.Tensor]: + """Process ray pose estimation if ray pose decoder is available.""" + if "ray" in output and "ray_conf" in output: + pred_extrinsic, pred_focal_lengths, pred_principal_points = get_extrinsic_from_camray( + output.ray, + output.ray_conf, + output.ray.shape[-3], + output.ray.shape[-2], + ) + pred_extrinsic = affine_inverse(pred_extrinsic) # w2c -> c2w + pred_extrinsic = pred_extrinsic[:, :, :3, :] + pred_intrinsic = torch.eye(3, 3)[None, None].repeat(pred_extrinsic.shape[0], pred_extrinsic.shape[1], 1, 1).clone().to(pred_extrinsic.device) + pred_intrinsic[:, :, 0, 0] = pred_focal_lengths[:, :, 0] / 2 * width + pred_intrinsic[:, :, 1, 1] = pred_focal_lengths[:, :, 1] / 2 * height + pred_intrinsic[:, :, 0, 2] = pred_principal_points[:, :, 0] * width * 0.5 + pred_intrinsic[:, :, 1, 2] = pred_principal_points[:, :, 1] * height * 0.5 + del output.ray + del output.ray_conf + output.extrinsics = pred_extrinsic + output.intrinsics = pred_intrinsic + return output + + def _process_depth_head( + self, feats: list[torch.Tensor], H: int, W: int + ) -> Dict[str, torch.Tensor]: + """Process features through the depth prediction head.""" + return self.head(feats, H, W, patch_start_idx=0) + + def _process_camera_estimation( + self, feats: list[torch.Tensor], H: int, W: int, output: Dict[str, torch.Tensor] + ) -> Dict[str, torch.Tensor]: + """Process camera pose estimation if camera decoder is available.""" + if self.cam_dec is not None: + pose_enc = self.cam_dec(feats[-1][1]) + # Remove ray information as it's not needed for pose estimation + if "ray" in output: + del output.ray + if "ray_conf" in output: + del output.ray_conf + + # Convert pose encoding to extrinsics and intrinsics + c2w, ixt = pose_encoding_to_extri_intri(pose_enc, (H, W)) + output.extrinsics = affine_inverse(c2w) + output.intrinsics = ixt + + return output + + def _process_gs_head( + self, + feats: list[torch.Tensor], + H: int, + W: int, + output: Dict[str, torch.Tensor], + in_images: torch.Tensor, + extrinsics: torch.Tensor | None = None, + intrinsics: torch.Tensor | None = None, + ) -> Dict[str, torch.Tensor]: + """Process 3DGS parameters estimation if 3DGS head is available.""" + if self.gs_head is None or self.gs_adapter is None: + return output + assert output.get("depth", None) is not None, "must provide MV depth for the GS head." + + # The depth is defined in the DA3 model's camera space, + # so even with provided GT camera poses, + # we instead use the predicted camera poses for better alignment. + ctx_extr = output.get("extrinsics", None) + ctx_intr = output.get("intrinsics", None) + assert ( + ctx_extr is not None and ctx_intr is not None + ), "must process camera info first if GT is not available" + + gt_extr = extrinsics + # homo the extr if needed + ctx_extr = as_homogeneous(ctx_extr) + if gt_extr is not None: + gt_extr = as_homogeneous(gt_extr) + + # forward through the gs_dpt head to get 'camera space' parameters + gs_outs = self.gs_head( + feats=feats, + H=H, + W=W, + patch_start_idx=0, + images=in_images, + ) + raw_gaussians = gs_outs.raw_gs + densities = gs_outs.raw_gs_conf + + # convert to 'world space' 3DGS parameters; ready to export and render + # gt_extr could be None, and will be used to align the pose scale if available + gs_world = self.gs_adapter( + extrinsics=ctx_extr, + intrinsics=ctx_intr, + depths=output.depth, + opacities=map_pdf_to_opacity(densities), + raw_gaussians=raw_gaussians, + image_shape=(H, W), + gt_extrinsics=gt_extr, + ) + output.gaussians = gs_world + + return output + + def _extract_auxiliary_features( + self, feats: list[torch.Tensor], feat_layers: list[int], H: int, W: int + ) -> Dict[str, torch.Tensor]: + """Extract auxiliary features from specified layers.""" + aux_features = Dict() + assert len(feats) == len(feat_layers) + for feat, feat_layer in zip(feats, feat_layers): + # Reshape features to spatial dimensions + feat_reshaped = feat.reshape( + [ + feat.shape[0], + feat.shape[1], + H // self.PATCH_SIZE, + W // self.PATCH_SIZE, + feat.shape[-1], + ] + ) + aux_features[f"feat_layer_{feat_layer}"] = feat_reshaped + + return aux_features + + +class NestedDepthAnything3Net(nn.Module): + """ + Nested Depth Anything 3 network with metric scaling capabilities. + + This network combines two DepthAnything3Net branches: + - Main branch: Standard depth estimation + - Metric branch: Metric depth estimation for scaling alignment + + The network performs depth alignment using least squares scaling + and handles sky region masking for improved depth estimation. + + Args: + preset: Configuration for the main depth estimation branch + second_preset: Configuration for the metric depth branch + """ + + def __init__(self, anyview: DictConfig, metric: DictConfig): + """ + Initialize NestedDepthAnything3Net with two branches. + + Args: + preset: Configuration for main depth estimation branch + second_preset: Configuration for metric depth branch + """ + super().__init__() + self.da3 = create_object(anyview) + self.da3_metric = create_object(metric) + + def forward( + self, + x: torch.Tensor, + extrinsics: torch.Tensor | None = None, + intrinsics: torch.Tensor | None = None, + export_feat_layers: list[int] | None = [], + infer_gs: bool = False, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + ) -> Dict[str, torch.Tensor]: + """ + Forward pass through both branches with metric scaling alignment. + + Args: + x: Input images (B, N, 3, H, W) + extrinsics: Camera extrinsics (B, N, 4, 4) - unused + intrinsics: Camera intrinsics (B, N, 3, 3) - unused + feat_layers: List of layer indices to extract features from + infer_gs: Enable Gaussian Splatting branch + use_ray_pose: Use ray-based pose estimation + ref_view_strategy: Strategy for selecting reference view + + Returns: + Dictionary containing aligned depth predictions and camera parameters + """ + # Get predictions from both branches + output = self.da3( + x, extrinsics, intrinsics, export_feat_layers=export_feat_layers, infer_gs=infer_gs, use_ray_pose=use_ray_pose, ref_view_strategy=ref_view_strategy + ) + metric_output = self.da3_metric(x) + + # Apply metric scaling and alignment + output = self._apply_metric_scaling(output, metric_output) + output = self._apply_depth_alignment(output, metric_output) + output = self._handle_sky_regions(output, metric_output) + + return output + + def _apply_metric_scaling( + self, output: Dict[str, torch.Tensor], metric_output: Dict[str, torch.Tensor] + ) -> Dict[str, torch.Tensor]: + """Apply metric scaling to the metric depth output.""" + # Scale metric depth based on camera intrinsics + metric_output.depth = apply_metric_scaling( + metric_output.depth, + output.intrinsics, + ) + return output + + def _apply_depth_alignment( + self, output: Dict[str, torch.Tensor], metric_output: Dict[str, torch.Tensor] + ) -> Dict[str, torch.Tensor]: + """Apply depth alignment using least squares scaling.""" + # Compute non-sky mask + non_sky_mask = compute_sky_mask(metric_output.sky, threshold=0.3) + + # Ensure we have enough non-sky pixels + assert non_sky_mask.sum() > 10, "Insufficient non-sky pixels for alignment" + + # Sample depth confidence for quantile computation + depth_conf_ns = output.depth_conf[non_sky_mask] + depth_conf_sampled = sample_tensor_for_quantile(depth_conf_ns, max_samples=100000) + median_conf = torch.quantile(depth_conf_sampled, 0.5) + + # Compute alignment mask + align_mask = compute_alignment_mask( + output.depth_conf, non_sky_mask, output.depth, metric_output.depth, median_conf + ) + + # Compute scale factor using least squares + valid_depth = output.depth[align_mask] + valid_metric_depth = metric_output.depth[align_mask] + scale_factor = least_squares_scale_scalar(valid_metric_depth, valid_depth) + + # Apply scaling to depth and extrinsics + output.depth *= scale_factor + output.extrinsics[:, :, :3, 3] *= scale_factor + output.is_metric = 1 + output.scale_factor = scale_factor.item() + + return output + + def _handle_sky_regions( + self, + output: Dict[str, torch.Tensor], + metric_output: Dict[str, torch.Tensor], + sky_depth_def: float = 200.0, + ) -> Dict[str, torch.Tensor]: + """Handle sky regions by setting them to maximum depth.""" + non_sky_mask = compute_sky_mask(metric_output.sky, threshold=0.3) + + # Compute maximum depth for non-sky regions + # Use sampling to safely compute quantile on large tensors + non_sky_depth = output.depth[non_sky_mask] + if non_sky_depth.numel() > 100000: + idx = torch.randint(0, non_sky_depth.numel(), (100000,), device=non_sky_depth.device) + sampled_depth = non_sky_depth[idx] + else: + sampled_depth = non_sky_depth + non_sky_max = min(torch.quantile(sampled_depth, 0.99), sky_depth_def) + + # Set sky regions to maximum depth and high confidence + output.depth, output.depth_conf = set_sky_regions_to_max_depth( + output.depth, output.depth_conf, non_sky_mask, max_depth=non_sky_max + ) + + return output diff --git a/src/depth_anything_3/model/dinov2/dinov2.py b/src/depth_anything_3/model/dinov2/dinov2.py new file mode 100644 index 0000000000000000000000000000000000000000..3bfe2dd8d36feb794d5dc309c117a0782727461b --- /dev/null +++ b/src/depth_anything_3/model/dinov2/dinov2.py @@ -0,0 +1,65 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the Apache License, Version 2.0 +# found in the LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/main/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py + + +from typing import List + +import torch.nn as nn + +from depth_anything_3.model.dinov2.vision_transformer import ( + vit_base, + vit_giant2, + vit_large, + vit_small, +) + + +class DinoV2(nn.Module): + def __init__( + self, + name: str, + out_layers: List[int], + alt_start: int = -1, + qknorm_start: int = -1, + rope_start: int = -1, + cat_token: bool = True, + **kwargs, + ): + super().__init__() + assert name in {"vits", "vitb", "vitl", "vitg"} + self.name = name + self.out_layers = out_layers + self.alt_start = alt_start + self.qknorm_start = qknorm_start + self.rope_start = rope_start + self.cat_token = cat_token + encoder_map = { + "vits": vit_small, + "vitb": vit_base, + "vitl": vit_large, + "vitg": vit_giant2, + } + encoder_fn = encoder_map[self.name] + ffn_layer = "swiglufused" if self.name == "vitg" else "mlp" + self.pretrained = encoder_fn( + img_size=518, + patch_size=14, + ffn_layer=ffn_layer, + alt_start=alt_start, + qknorm_start=qknorm_start, + rope_start=rope_start, + cat_token=cat_token, + ) + + def forward(self, x, **kwargs): + return self.pretrained.get_intermediate_layers( + x, + self.out_layers, + **kwargs, + ) diff --git a/src/depth_anything_3/model/dinov2/layers/__init__.py b/src/depth_anything_3/model/dinov2/layers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..07c4e5f93c57a481e86db7b7b097c9e3d6874621 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/__init__.py @@ -0,0 +1,33 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from .attention import ( + FLASH_ATTN_AVAILABLE, + FLASH_ATTN_VERSION, + Attention, + get_attention_backend, +) +from .block import Block +from .layer_scale import LayerScale +from .mlp import Mlp +from .patch_embed import PatchEmbed +from .rope import PositionGetter, RotaryPositionEmbedding2D +from .swiglu_ffn import SwiGLUFFN, SwiGLUFFNFused + +__all__ = [ + "Attention", + "FLASH_ATTN_AVAILABLE", + "FLASH_ATTN_VERSION", + "get_attention_backend", + "Mlp", + "PatchEmbed", + "SwiGLUFFN", + "SwiGLUFFNFused", + "Block", + "LayerScale", + "PositionGetter", + "RotaryPositionEmbedding2D", +] diff --git a/src/depth_anything_3/model/dinov2/layers/attention.py b/src/depth_anything_3/model/dinov2/layers/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..e901a6c45a9ef3b975a0956af32b8d8deedf2332 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/attention.py @@ -0,0 +1,228 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# +# Flash Attention integration by Delanoe Pirard / Aedelon - Apache 2.0 + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py + +import logging +import os + +import torch +import torch.nn.functional as F +from torch import Tensor, nn + +logger = logging.getLogger("dinov2") + +# Flash Attention availability detection +FLASH_ATTN_AVAILABLE = False +FLASH_ATTN_VERSION = None + +try: + from flash_attn import __version__ as flash_attn_version + from flash_attn import flash_attn_func + + FLASH_ATTN_AVAILABLE = True + FLASH_ATTN_VERSION = flash_attn_version + logger.info(f"Flash Attention v{flash_attn_version} available") +except ImportError: + logger.debug("flash-attn not installed, using PyTorch SDPA backend") + + +def get_attention_backend() -> str: + """ + Determine the best attention backend for current hardware. + + Returns: + str: 'flash_attn' if flash-attn is available and on CUDA, + 'sdpa' for PyTorch scaled_dot_product_attention, + 'manual' for fallback attention. + """ + # Check environment override + env_backend = os.environ.get("DA3_ATTENTION_BACKEND", "").lower() + if env_backend in ("flash_attn", "sdpa", "manual"): + return env_backend + + # Auto-detect best backend + if FLASH_ATTN_AVAILABLE and torch.cuda.is_available(): + return "flash_attn" + return "sdpa" + + +class Attention(nn.Module): + """ + Multi-head attention with Flash Attention support. + + Supports three backends: + - flash_attn: Flash Attention v2/v3 (fastest on CUDA, requires flash-attn package) + - sdpa: PyTorch scaled_dot_product_attention (default, may use Flash internally) + - manual: Classic attention implementation (slowest, for debugging) + + Backend selection: + - Auto-detected based on hardware and package availability + - Override via DA3_ATTENTION_BACKEND env var or attn_backend parameter + """ + + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = False, + proj_bias: bool = True, + attn_drop: float = 0.0, + proj_drop: float = 0.0, + norm_layer: nn.Module = nn.LayerNorm, + qk_norm: bool = False, + fused_attn: bool = True, # legacy param, kept for compatibility + attn_backend: str | None = None, # 'flash_attn', 'sdpa', 'manual', or None for auto + rope=None, + ) -> None: + super().__init__() + assert dim % num_heads == 0, "dim should be divisible by num_heads" + self.num_heads = num_heads + self.head_dim = dim // num_heads + self.scale = self.head_dim**-0.5 + self.fused_attn = fused_attn + + # Determine attention backend + if attn_backend is not None: + self.attn_backend = attn_backend + elif not fused_attn: + self.attn_backend = "manual" + else: + self.attn_backend = get_attention_backend() + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.attn_drop = nn.Dropout(attn_drop) + self.attn_drop_p = attn_drop + self.proj = nn.Linear(dim, dim, bias=proj_bias) + self.proj_drop = nn.Dropout(proj_drop) + self.rope = rope + + logger.debug(f"Attention initialized with backend: {self.attn_backend}") + + def _flash_attention( + self, q: Tensor, k: Tensor, v: Tensor, attn_mask: Tensor | None + ) -> Tensor: + """ + Flash Attention v2/v3 forward pass. + + Note: flash_attn_func expects (B, N, H, D) format, not (B, H, N, D). + Attention mask support is limited in Flash Attention. + """ + # flash_attn expects (B, N, H, D), we have (B, H, N, D) + q = q.transpose(1, 2) # (B, N, H, D) + k = k.transpose(1, 2) + v = v.transpose(1, 2) + + # Flash Attention requires contiguous tensors in fp16/bf16 + if q.dtype == torch.float32: + q = q.to(torch.bfloat16) + k = k.to(torch.bfloat16) + v = v.to(torch.bfloat16) + cast_back = True + else: + cast_back = False + + dropout_p = self.attn_drop_p if self.training else 0.0 + + # Flash Attention v2 does not support arbitrary attention masks + # It supports causal masking via causal=True flag + # For non-causal with custom mask, fall back to SDPA + if attn_mask is not None: + logger.debug("Flash Attention: custom mask not supported, falling back to SDPA") + return self._sdpa_attention( + q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_mask + ) + + out = flash_attn_func(q, k, v, dropout_p=dropout_p, causal=False) + + if cast_back: + out = out.to(torch.float32) + + # Back to (B, H, N, D) then (B, N, C) + return out.transpose(1, 2) + + def _sdpa_attention( + self, q: Tensor, k: Tensor, v: Tensor, attn_mask: Tensor | None + ) -> Tensor: + """PyTorch scaled_dot_product_attention (may use Flash internally on CUDA).""" + return F.scaled_dot_product_attention( + q, + k, + v, + dropout_p=self.attn_drop.p if self.training else 0.0, + attn_mask=( + attn_mask[:, None].repeat(1, self.num_heads, 1, 1) + if attn_mask is not None + else None + ), + ) + + def _manual_attention( + self, q: Tensor, k: Tensor, v: Tensor, attn_mask: Tensor | None + ) -> Tensor: + """Classic attention implementation for debugging.""" + q = q * self.scale + attn = q @ k.transpose(-2, -1) + if attn_mask is not None: + attn = attn + attn_mask[:, None].repeat(1, self.num_heads, 1, 1) + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + return attn @ v + + def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor: + B, N, C = x.shape + qkv = ( + self.qkv(x) + .reshape(B, N, 3, self.num_heads, self.head_dim) + .permute(2, 0, 3, 1, 4) + ) + q, k, v = qkv[0], qkv[1], qkv[2] + q, k = self.q_norm(q), self.k_norm(k) + + # Apply RoPE if available (before attention) + if self.rope is not None and pos is not None: + q = self.rope(q, pos) + k = self.rope(k, pos) + + # Select attention backend + if self.attn_backend == "flash_attn" and FLASH_ATTN_AVAILABLE: + x = self._flash_attention(q, k, v, attn_mask) + elif self.attn_backend == "sdpa" or ( + self.attn_backend == "flash_attn" and not FLASH_ATTN_AVAILABLE + ): + x = self._sdpa_attention(q, k, v, attn_mask) + else: + x = self._manual_attention(q, k, v, attn_mask) + + x = x.transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + def _forward(self, x: Tensor) -> Tensor: + B, N, C = x.shape + qkv = ( + self.qkv(x) + .reshape(B, N, 3, self.num_heads, C // self.num_heads) + .permute(2, 0, 3, 1, 4) + ) + + q, k, v = qkv[0] * self.scale, qkv[1], qkv[2] + attn = q @ k.transpose(-2, -1) + + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + + x = (attn @ v).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x diff --git a/src/depth_anything_3/model/dinov2/layers/block.py b/src/depth_anything_3/model/dinov2/layers/block.py new file mode 100644 index 0000000000000000000000000000000000000000..88ad28ada4578a2c345429f3da28e7d9dcd15005 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/block.py @@ -0,0 +1,144 @@ +# flake8: noqa: F821 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py + +import logging +from typing import Callable, Optional + +import torch +from torch import Tensor, nn + +from .attention import Attention +from .drop_path import DropPath +from .layer_scale import LayerScale +from .mlp import Mlp + +logger = logging.getLogger("dinov2") +XFORMERS_AVAILABLE = True + + +class Block(nn.Module): + def __init__( + self, + dim: int, + num_heads: int, + mlp_ratio: float = 4.0, + qkv_bias: bool = False, + proj_bias: bool = True, + ffn_bias: bool = True, + drop: float = 0.0, + attn_drop: float = 0.0, + init_values=None, + drop_path: float = 0.0, + act_layer: Callable[..., nn.Module] = nn.GELU, + norm_layer: Callable[..., nn.Module] = nn.LayerNorm, + attn_class: Callable[..., nn.Module] = Attention, + ffn_layer: Callable[..., nn.Module] = Mlp, + qk_norm: bool = False, + rope=None, + ln_eps: float = 1e-6, + ) -> None: + super().__init__() + # print(f"biases: qkv: {qkv_bias}, proj: {proj_bias}, ffn: {ffn_bias}") + self.norm1 = norm_layer(dim, eps=ln_eps) + self.attn = attn_class( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + proj_bias=proj_bias, + attn_drop=attn_drop, + proj_drop=drop, + qk_norm=qk_norm, + rope=rope, + ) + self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + self.norm2 = norm_layer(dim, eps=ln_eps) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = ffn_layer( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + bias=ffn_bias, + ) + self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + self.sample_drop_ratio = drop_path + + def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor: + def attn_residual_func(x: Tensor, pos=None, attn_mask=None) -> Tensor: + return self.ls1(self.attn(self.norm1(x), pos=pos, attn_mask=attn_mask)) + + def ffn_residual_func(x: Tensor) -> Tensor: + return self.ls2(self.mlp(self.norm2(x))) + + if self.training and self.sample_drop_ratio > 0.1: + # the overhead is compensated only for a drop path rate larger than 0.1 + x = drop_add_residual_stochastic_depth( + x, + residual_func=attn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + pos=pos, + ) + x = drop_add_residual_stochastic_depth( + x, + residual_func=ffn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + ) + elif self.training and self.sample_drop_ratio > 0.0: + x = x + self.drop_path1(attn_residual_func(x, pos=pos, attn_mask=attn_mask)) + x = x + self.drop_path1(ffn_residual_func(x)) # FIXME: drop_path2 + else: + x = x + attn_residual_func(x, pos=pos, attn_mask=attn_mask) + x = x + ffn_residual_func(x) + return x + + +def drop_add_residual_stochastic_depth( + x: Tensor, + residual_func: Callable[[Tensor], Tensor], + sample_drop_ratio: float = 0.0, + pos: Optional[Tensor] = None, +) -> Tensor: + # 1) extract subset using permutation + b, n, d = x.shape + sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1) + brange = (torch.randperm(b, device=x.device))[:sample_subset_size] + x_subset = x[brange] + + # 2) apply residual_func to get residual + if pos is not None: + # if necessary, apply rope to the subset + pos = pos[brange] + residual = residual_func(x_subset, pos=pos) + else: + residual = residual_func(x_subset) + + x_flat = x.flatten(1) + residual = residual.flatten(1) + + residual_scale_factor = b / sample_subset_size + + # 3) add the residual + x_plus_residual = torch.index_add( + x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor + ) + return x_plus_residual.view_as(x) + + +def get_branges_scales(x, sample_drop_ratio=0.0): + b, n, d = x.shape + sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1) + brange = (torch.randperm(b, device=x.device))[:sample_subset_size] + residual_scale_factor = b / sample_subset_size + return brange, residual_scale_factor diff --git a/src/depth_anything_3/model/dinov2/layers/drop_path.py b/src/depth_anything_3/model/dinov2/layers/drop_path.py new file mode 100644 index 0000000000000000000000000000000000000000..1c2cc94e969711f1eb9f62093b79a0139b9bfb1e --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/drop_path.py @@ -0,0 +1,35 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/drop.py + + +from torch import nn + + +def drop_path(x, drop_prob: float = 0.0, training: bool = False): + if drop_prob == 0.0 or not training: + return x + keep_prob = 1 - drop_prob + shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets + random_tensor = x.new_empty(shape).bernoulli_(keep_prob) + if keep_prob > 0.0: + random_tensor.div_(keep_prob) + output = x * random_tensor + return output + + +class DropPath(nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, drop_prob=None): + super().__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) diff --git a/src/depth_anything_3/model/dinov2/layers/layer_scale.py b/src/depth_anything_3/model/dinov2/layers/layer_scale.py new file mode 100644 index 0000000000000000000000000000000000000000..63160361048c03226b6cc5acff1b0a8829c2164b --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/layer_scale.py @@ -0,0 +1,32 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110 # noqa: E501 + +from typing import Union + +import torch +from torch import Tensor, nn + + +class LayerScale(nn.Module): + def __init__( + self, + dim: int, + init_values: Union[float, Tensor] = 1e-5, + inplace: bool = False, + ) -> None: + super().__init__() + self.dim = dim + self.inplace = inplace + self.init_values = init_values + self.gamma = nn.Parameter(init_values * torch.ones(dim)) + + def forward(self, x: Tensor) -> Tensor: + return x.mul_(self.gamma) if self.inplace else x * self.gamma + + def extra_repr(self) -> str: + return f"{self.dim}, init_values={self.init_values}, inplace={self.inplace}" diff --git a/src/depth_anything_3/model/dinov2/layers/mlp.py b/src/depth_anything_3/model/dinov2/layers/mlp.py new file mode 100644 index 0000000000000000000000000000000000000000..5e4b315f972f9a9f54aef1e4ef4e81b52976f018 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/mlp.py @@ -0,0 +1,41 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/mlp.py + + +from typing import Callable, Optional + +from torch import Tensor, nn + + +class Mlp(nn.Module): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = nn.GELU, + drop: float = 0.0, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias) + self.drop = nn.Dropout(drop) + + def forward(self, x: Tensor) -> Tensor: + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x diff --git a/src/depth_anything_3/model/dinov2/layers/patch_embed.py b/src/depth_anything_3/model/dinov2/layers/patch_embed.py new file mode 100644 index 0000000000000000000000000000000000000000..b185ee8e677427fe784ae5ca3adc3e19bd859f81 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/patch_embed.py @@ -0,0 +1,95 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py + +from typing import Callable, Optional, Tuple, Union + +import torch.nn as nn +from torch import Tensor + + +def make_2tuple(x): + if isinstance(x, tuple): + assert len(x) == 2 + return x + + assert isinstance(x, int) + return (x, x) + + +class PatchEmbed(nn.Module): + """ + 2D image to patch embedding: (B,C,H,W) -> (B,N,D) + + Args: + img_size: Image size. + patch_size: Patch token size. + in_chans: Number of input image channels. + embed_dim: Number of linear projection output channels. + norm_layer: Normalization layer. + """ + + def __init__( + self, + img_size: Union[int, Tuple[int, int]] = 224, + patch_size: Union[int, Tuple[int, int]] = 16, + in_chans: int = 3, + embed_dim: int = 768, + norm_layer: Optional[Callable] = None, + flatten_embedding: bool = True, + ) -> None: + super().__init__() + + image_HW = make_2tuple(img_size) + patch_HW = make_2tuple(patch_size) + patch_grid_size = ( + image_HW[0] // patch_HW[0], + image_HW[1] // patch_HW[1], + ) + + self.img_size = image_HW + self.patch_size = patch_HW + self.patches_resolution = patch_grid_size + self.num_patches = patch_grid_size[0] * patch_grid_size[1] + + self.in_chans = in_chans + self.embed_dim = embed_dim + + self.flatten_embedding = flatten_embedding + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_HW, stride=patch_HW) + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + def forward(self, x: Tensor) -> Tensor: + _, _, H, W = x.shape + patch_H, patch_W = self.patch_size + + assert ( + H % patch_H == 0 + ), f"Input image height {H} is not a multiple of patch height {patch_H}" + assert ( + W % patch_W == 0 + ), f"Input image width {W} is not a multiple of patch width: {patch_W}" + + x = self.proj(x) # B C H W + H, W = x.size(2), x.size(3) + x = x.flatten(2).transpose(1, 2) # B HW C + x = self.norm(x) + if not self.flatten_embedding: + x = x.reshape(-1, H, W, self.embed_dim) # B H W C + return x + + def flops(self) -> float: + Ho, Wo = self.patches_resolution + flops = ( + Ho * Wo * self.embed_dim * self.in_chans * (self.patch_size[0] * self.patch_size[1]) + ) + if self.norm is not None: + flops += Ho * Wo * self.embed_dim + return flops diff --git a/src/depth_anything_3/model/dinov2/layers/rope.py b/src/depth_anything_3/model/dinov2/layers/rope.py new file mode 100644 index 0000000000000000000000000000000000000000..36af5a8057d6708abee5a9f9d47e94a9d22f4ddc --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/rope.py @@ -0,0 +1,201 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the Apache License, Version 2.0 +# found in the LICENSE file in the root directory of this source tree. + + +# Implementation of 2D Rotary Position Embeddings (RoPE). + +# This module provides a clean implementation of 2D Rotary Position Embeddings, +# which extends the original RoPE concept to handle 2D spatial positions. + +# Inspired by: +# https://github.com/meta-llama/codellama/blob/main/llama/model.py +# https://github.com/naver-ai/rope-vit + + +from typing import Dict, Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class PositionGetter: + """Generates and caches 2D spatial positions for patches in a grid. + + This class efficiently manages the generation of spatial coordinates for patches + in a 2D grid, caching results to avoid redundant computations. + + Attributes: + position_cache: Dictionary storing precomputed position tensors for different + grid dimensions. + """ + + def __init__(self): + """Initializes the position generator with an empty cache.""" + self.position_cache: Dict[Tuple[int, int], torch.Tensor] = {} + + def __call__( + self, batch_size: int, height: int, width: int, device: torch.device + ) -> torch.Tensor: + """Generates spatial positions for a batch of patches. + + Args: + batch_size: Number of samples in the batch. + height: Height of the grid in patches. + width: Width of the grid in patches. + device: Target device for the position tensor. + + Returns: + Tensor of shape (batch_size, height*width, 2) containing y,x coordinates + for each position in the grid, repeated for each batch item. + """ + if (height, width) not in self.position_cache: + y_coords = torch.arange(height, device=device) + x_coords = torch.arange(width, device=device) + positions = torch.cartesian_prod(y_coords, x_coords) + self.position_cache[height, width] = positions + + cached_positions = self.position_cache[height, width] + return cached_positions.view(1, height * width, 2).expand(batch_size, -1, -1).clone() + + +class RotaryPositionEmbedding2D(nn.Module): + """2D Rotary Position Embedding implementation. + + This module applies rotary position embeddings to input tokens based on their + 2D spatial positions. It handles the position-dependent rotation of features + separately for vertical and horizontal dimensions. + + Args: + frequency: Base frequency for the position embeddings. Default: 100.0 + scaling_factor: Scaling factor for frequency computation. Default: 1.0 + + Attributes: + base_frequency: Base frequency for computing position embeddings. + scaling_factor: Factor to scale the computed frequencies. + frequency_cache: Cache for storing precomputed frequency components. + """ + + def __init__(self, frequency: float = 100.0, scaling_factor: float = 1.0): + """Initializes the 2D RoPE module.""" + super().__init__() + self.base_frequency = frequency + self.scaling_factor = scaling_factor + self.frequency_cache: Dict[Tuple, Tuple[torch.Tensor, torch.Tensor]] = {} + + def _compute_frequency_components( + self, dim: int, seq_len: int, device: torch.device, dtype: torch.dtype + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Computes frequency components for rotary embeddings. + + Args: + dim: Feature dimension (must be even). + seq_len: Maximum sequence length. + device: Target device for computations. + dtype: Data type for the computed tensors. + + Returns: + Tuple of (cosine, sine) tensors for frequency components. + """ + cache_key = (dim, seq_len, device, dtype) + if cache_key not in self.frequency_cache: + # Compute frequency bands + exponents = torch.arange(0, dim, 2, device=device).float() / dim + inv_freq = 1.0 / (self.base_frequency**exponents) + + # Generate position-dependent frequencies + positions = torch.arange(seq_len, device=device, dtype=inv_freq.dtype) + angles = torch.einsum("i,j->ij", positions, inv_freq) + + # Compute and cache frequency components + angles = angles.to(dtype) + angles = torch.cat((angles, angles), dim=-1) + cos_components = angles.cos().to(dtype) + sin_components = angles.sin().to(dtype) + self.frequency_cache[cache_key] = (cos_components, sin_components) + + return self.frequency_cache[cache_key] + + @staticmethod + def _rotate_features(x: torch.Tensor) -> torch.Tensor: + """Performs feature rotation by splitting and recombining feature dimensions. + + Args: + x: Input tensor to rotate. + + Returns: + Rotated feature tensor. + """ + feature_dim = x.shape[-1] + x1, x2 = x[..., : feature_dim // 2], x[..., feature_dim // 2 :] + return torch.cat((-x2, x1), dim=-1) + + def _apply_1d_rope( + self, + tokens: torch.Tensor, + positions: torch.Tensor, + cos_comp: torch.Tensor, + sin_comp: torch.Tensor, + ) -> torch.Tensor: + """Applies 1D rotary position embeddings along one dimension. + + Args: + tokens: Input token features. + positions: Position indices. + cos_comp: Cosine components for rotation. + sin_comp: Sine components for rotation. + + Returns: + Tokens with applied rotary position embeddings. + """ + # Embed positions with frequency components + cos = F.embedding(positions, cos_comp)[:, None, :, :] + sin = F.embedding(positions, sin_comp)[:, None, :, :] + # Apply rotation + return (tokens * cos) + (self._rotate_features(tokens) * sin) + + def forward(self, tokens: torch.Tensor, positions: torch.Tensor) -> torch.Tensor: + """Applies 2D rotary position embeddings to input tokens. + + Args: + tokens: Input tensor of shape (batch_size, n_heads, n_tokens, dim). + The feature dimension (dim) must be divisible by 4. + positions: Position tensor of shape (batch_size, n_tokens, 2) containing + the y and x coordinates for each token. + + Returns: + Tensor of same shape as input with applied 2D rotary position embeddings. + + Raises: + AssertionError: If input dimensions are invalid or positions are malformed. + """ + # Validate inputs + assert tokens.size(-1) % 2 == 0, "Feature dimension must be even" + assert ( + positions.ndim == 3 and positions.shape[-1] == 2 + ), "Positions must have shape (batch_size, n_tokens, 2)" + + # Compute feature dimension for each spatial direction + feature_dim = tokens.size(-1) // 2 + + # Get frequency components + max_position = int(positions.max()) + 1 + cos_comp, sin_comp = self._compute_frequency_components( + feature_dim, max_position, tokens.device, tokens.dtype + ) + + # Split features for vertical and horizontal processing + vertical_features, horizontal_features = tokens.chunk(2, dim=-1) + + # Apply RoPE separately for each dimension + vertical_features = self._apply_1d_rope( + vertical_features, positions[..., 0], cos_comp, sin_comp + ) + horizontal_features = self._apply_1d_rope( + horizontal_features, positions[..., 1], cos_comp, sin_comp + ) + + # Combine processed features + return torch.cat((vertical_features, horizontal_features), dim=-1) diff --git a/src/depth_anything_3/model/dinov2/layers/swiglu_ffn.py b/src/depth_anything_3/model/dinov2/layers/swiglu_ffn.py new file mode 100644 index 0000000000000000000000000000000000000000..e82999e9b09b41cd6aba9edbc4c05d51ab663a1e --- /dev/null +++ b/src/depth_anything_3/model/dinov2/layers/swiglu_ffn.py @@ -0,0 +1,63 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Callable, Optional + +import torch.nn.functional as F +from torch import Tensor, nn + + +class SwiGLUFFN(nn.Module): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = None, + drop: float = 0.0, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) + self.w3 = nn.Linear(hidden_features, out_features, bias=bias) + + def forward(self, x: Tensor) -> Tensor: + x12 = self.w12(x) + x1, x2 = x12.chunk(2, dim=-1) + hidden = F.silu(x1) * x2 + return self.w3(hidden) + + +try: + from xformers.ops import SwiGLU + + XFORMERS_AVAILABLE = True +except ImportError: + SwiGLU = SwiGLUFFN + XFORMERS_AVAILABLE = False + + +class SwiGLUFFNFused(SwiGLU): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = None, + drop: float = 0.0, + bias: bool = True, + ) -> None: + out_features = out_features or in_features + hidden_features = hidden_features or in_features + hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8 + super().__init__( + in_features=in_features, + hidden_features=hidden_features, + out_features=out_features, + bias=bias, + ) diff --git a/src/depth_anything_3/model/dinov2/vision_transformer.py b/src/depth_anything_3/model/dinov2/vision_transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..4574bd1290dd9cceaa0d1ab6a1857c3132580c64 --- /dev/null +++ b/src/depth_anything_3/model/dinov2/vision_transformer.py @@ -0,0 +1,455 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the Apache License, Version 2.0 +# found in the LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/main/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py + +import math +from typing import Callable, List, Sequence, Tuple, Union + +import numpy as np +import torch +import torch.nn as nn +import torch.utils.checkpoint +from einops import rearrange + +from depth_anything_3.model.reference_view_selector import ( + reorder_by_reference, + restore_original_order, + select_reference_view, +) +from depth_anything_3.utils.constants import THRESH_FOR_REF_SELECTION +from depth_anything_3.utils.logger import logger + +from .layers import ( # noqa: F401 + Block, + LayerScale, # noqa: F401 + Mlp, # noqa: F401 + PatchEmbed, + PositionGetter, + RotaryPositionEmbedding2D, + SwiGLUFFNFused, +) + +# logger = logging.getLogger("dinov2") + + +def get_1d_sincos_pos_embed_from_grid(embed_dim, pos): + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size (M,) + out: (M, D) + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=float) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # (D/2,) + + pos = pos.reshape(-1) # (M,) + out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product + + emb_sin = np.sin(out) # (M, D/2) + emb_cos = np.cos(out) # (M, D/2) + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D) + return emb + + +def named_apply( + fn: Callable, module: nn.Module, name="", depth_first=True, include_root=False +) -> nn.Module: + if not depth_first and include_root: + fn(module=module, name=name) + for child_name, child_module in module.named_children(): + child_name = ".".join((name, child_name)) if name else child_name + named_apply( + fn=fn, module=child_module, name=child_name, depth_first=depth_first, include_root=True + ) + if depth_first and include_root: + fn(module=module, name=name) + return module + + +class BlockChunk(nn.ModuleList): + def forward(self, x): + for b in self: + x = b(x) + return x + + +class DinoVisionTransformer(nn.Module): + def __init__( + self, + img_size=224, + patch_size=16, + in_chans=3, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4.0, + qkv_bias=True, + ffn_bias=True, + proj_bias=True, + drop_path_rate=0.0, + drop_path_uniform=False, + init_values=1.0, # for layerscale: None or 0 => no layerscale + embed_layer=PatchEmbed, + act_layer=nn.GELU, + block_fn=Block, + ffn_layer="mlp", + block_chunks=1, + num_register_tokens=0, + interpolate_antialias=False, + interpolate_offset=0.1, + alt_start=-1, + qknorm_start=-1, + rope_start=-1, + rope_freq=100, + plus_cam_token=False, + cat_token=True, + ): + """ + Args: + img_size (int, tuple): input image size + patch_size (int, tuple): patch size + in_chans (int): number of input channels + embed_dim (int): embedding dimension + depth (int): depth of transformer + num_heads (int): number of attention heads + mlp_ratio (int): ratio of mlp hidden dim to embedding dim + qkv_bias (bool): enable bias for qkv if True + proj_bias (bool): enable bias for proj in attn if True + ffn_bias (bool): enable bias for ffn if True + weight_init (str): weight init scheme + init_values (float): layer-scale init values + embed_layer (nn.Module): patch embedding layer + act_layer (nn.Module): MLP activation layer + block_fn (nn.Module): transformer block class + ffn_layer (str): "mlp", "swiglu", "swiglufused" or "identity" + block_chunks: (int) split block sequence into block_chunks units for FSDP wrap + num_register_tokens: (int) number of extra cls tokens (so-called "registers") + interpolate_antialias: (str) flag to apply anti-aliasing when interpolating + positional embeddings + interpolate_offset: (float) work-around offset to apply when interpolating + positional embeddings + """ + super().__init__() + self.patch_start_idx = 1 + norm_layer = nn.LayerNorm + self.num_features = self.embed_dim = ( + embed_dim # num_features for consistency with other models + ) + self.alt_start = alt_start + self.qknorm_start = qknorm_start + self.rope_start = rope_start + self.cat_token = cat_token + self.num_tokens = 1 + self.n_blocks = depth + self.num_heads = num_heads + self.patch_size = patch_size + self.num_register_tokens = num_register_tokens + self.interpolate_antialias = interpolate_antialias + self.interpolate_offset = interpolate_offset + + self.patch_embed = embed_layer( + img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim + ) + num_patches = self.patch_embed.num_patches + self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) + if self.alt_start != -1: + self.camera_token = nn.Parameter(torch.randn(1, 2, embed_dim)) + self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + self.num_tokens, embed_dim)) + assert num_register_tokens >= 0 + self.register_tokens = ( + nn.Parameter(torch.zeros(1, num_register_tokens, embed_dim)) + if num_register_tokens + else None + ) + + if drop_path_uniform is True: + dpr = [drop_path_rate] * depth + else: + dpr = [ + x.item() for x in torch.linspace(0, drop_path_rate, depth) + ] # stochastic depth decay rule + if ffn_layer == "mlp": + logger.info("using MLP layer as FFN") + ffn_layer = Mlp + elif ffn_layer == "swiglufused" or ffn_layer == "swiglu": + logger.info("using SwiGLU layer as FFN") + ffn_layer = SwiGLUFFNFused + elif ffn_layer == "identity": + logger.info("using Identity layer as FFN") + + def f(*args, **kwargs): + return nn.Identity() + + ffn_layer = f + else: + raise NotImplementedError + + if self.rope_start != -1: + self.rope = RotaryPositionEmbedding2D(frequency=rope_freq) if rope_freq > 0 else None + self.position_getter = PositionGetter() if self.rope is not None else None + else: + self.rope = None + blocks_list = [ + block_fn( + dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + proj_bias=proj_bias, + ffn_bias=ffn_bias, + drop_path=dpr[i], + norm_layer=norm_layer, + act_layer=act_layer, + ffn_layer=ffn_layer, + init_values=init_values, + qk_norm=i >= qknorm_start if qknorm_start != -1 else False, + rope=self.rope if i >= rope_start and rope_start != -1 else None, + ) + for i in range(depth) + ] + self.blocks = nn.ModuleList(blocks_list) + self.norm = norm_layer(embed_dim) + + def interpolate_pos_encoding(self, x, w, h): + previous_dtype = x.dtype + npatch = x.shape[1] - 1 + N = self.pos_embed.shape[1] - 1 + if npatch == N and w == h: + return self.pos_embed + pos_embed = self.pos_embed.float() + class_pos_embed = pos_embed[:, 0] + patch_pos_embed = pos_embed[:, 1:] + dim = x.shape[-1] + w0 = w // self.patch_size + h0 = h // self.patch_size + M = int(math.sqrt(N)) # Recover the number of patches in each dimension + assert N == M * M + kwargs = {} + if self.interpolate_offset: + # Historical kludge: add a small number to avoid floating point error in the + # interpolation, see https://github.com/facebookresearch/dino/issues/8 + # Note: still needed for backward-compatibility, the underlying operators are using + # both output size and scale factors + sx = float(w0 + self.interpolate_offset) / M + sy = float(h0 + self.interpolate_offset) / M + kwargs["scale_factor"] = (sx, sy) + else: + # Simply specify an output size instead of a scale factor + kwargs["size"] = (w0, h0) + patch_pos_embed = nn.functional.interpolate( + patch_pos_embed.reshape(1, M, M, dim).permute(0, 3, 1, 2), + mode="bicubic", + antialias=self.interpolate_antialias, + **kwargs, + ) + assert (w0, h0) == patch_pos_embed.shape[-2:] + patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim) + return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1).to(previous_dtype) + + def prepare_cls_token(self, B, S): + cls_token = self.cls_token.expand(B, S, -1) + cls_token = cls_token.reshape(B * S, -1, self.embed_dim) + return cls_token + + def prepare_tokens_with_masks(self, x, masks=None, cls_token=None, **kwargs): + B, S, nc, w, h = x.shape + x = rearrange(x, "b s c h w -> (b s) c h w") + x = self.patch_embed(x) + if masks is not None: + x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x) + cls_token = self.prepare_cls_token(B, S) + x = torch.cat((cls_token, x), dim=1) + x = x + self.interpolate_pos_encoding(x, w, h) + if self.register_tokens is not None: + x = torch.cat( + ( + x[:, :1], + self.register_tokens.expand(x.shape[0], -1, -1), + x[:, 1:], + ), + dim=1, + ) + x = rearrange(x, "(b s) n c -> b s n c", b=B, s=S) + return x + + def _prepare_rope(self, B, S, H, W, device): + pos = None + pos_nodiff = None + if self.rope is not None: + pos = self.position_getter( + B * S, H // self.patch_size, W // self.patch_size, device=device + ) + pos = rearrange(pos, "(b s) n c -> b s n c", b=B) + pos_nodiff = torch.zeros_like(pos).to(pos.dtype) + if self.patch_start_idx > 0: + pos = pos + 1 + pos_special = torch.zeros(B * S, self.patch_start_idx, 2).to(device).to(pos.dtype) + pos_special = rearrange(pos_special, "(b s) n c -> b s n c", b=B) + pos = torch.cat([pos_special, pos], dim=2) + pos_nodiff = pos_nodiff + 1 + pos_nodiff = torch.cat([pos_special, pos_nodiff], dim=2) + return pos, pos_nodiff + + def _get_intermediate_layers_not_chunked(self, x, n=1, export_feat_layers=[], **kwargs): + B, S, _, H, W = x.shape + x = self.prepare_tokens_with_masks(x) + output, total_block_len, aux_output = [], len(self.blocks), [] + blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n + pos, pos_nodiff = self._prepare_rope(B, S, H, W, x.device) + + for i, blk in enumerate(self.blocks): + if i < self.rope_start or self.rope is None: + g_pos, l_pos = None, None + else: + g_pos = pos_nodiff + l_pos = pos + + if self.alt_start != -1 and (i == self.alt_start - 1) and x.shape[1] >= THRESH_FOR_REF_SELECTION: + # Select reference view using configured strategy + strategy = kwargs.get("ref_view_strategy", "saddle_balanced") + logger.info(f"Selecting reference view using strategy: {strategy}") + b_idx = select_reference_view(x, strategy=strategy) + # Reorder views to place reference view first + x = reorder_by_reference(x, b_idx) + + if self.alt_start != -1 and i == self.alt_start: + if kwargs.get("cam_token", None) is not None: + logger.info("Using camera conditions provided by the user") + cam_token = kwargs.get("cam_token") + else: + ref_token = self.camera_token[:, :1].expand(B, -1, -1) + src_token = self.camera_token[:, 1:].expand(B, S - 1, -1) + cam_token = torch.cat([ref_token, src_token], dim=1) + x[:, :, 0] = cam_token + + if self.alt_start != -1 and i >= self.alt_start and i % 2 == 1: + x = self.process_attention( + x, blk, "global", pos=g_pos, attn_mask=kwargs.get("attn_mask", None) + ) + else: + x = self.process_attention(x, blk, "local", pos=l_pos) + local_x = x + + if i in blocks_to_take: + out_x = torch.cat([local_x, x], dim=-1) if self.cat_token else x + # Restore original view order if reordering was applied + if x.shape[1] >= THRESH_FOR_REF_SELECTION and self.alt_start != -1 and 'b_idx' in locals(): + out_x = restore_original_order(out_x, b_idx) + output.append((out_x[:, :, 0], out_x)) + if i in export_feat_layers: + aux_output.append(x) + return output, aux_output + + def process_attention(self, x, block, attn_type="global", pos=None, attn_mask=None): + b, s, n = x.shape[:3] + if attn_type == "local": + x = rearrange(x, "b s n c -> (b s) n c") + if pos is not None: + pos = rearrange(pos, "b s n c -> (b s) n c") + elif attn_type == "global": + x = rearrange(x, "b s n c -> b (s n) c") + if pos is not None: + pos = rearrange(pos, "b s n c -> b (s n) c") + else: + raise ValueError(f"Invalid attention type: {attn_type}") + + x = block(x, pos=pos, attn_mask=attn_mask) + + if attn_type == "local": + x = rearrange(x, "(b s) n c -> b s n c", b=b, s=s) + elif attn_type == "global": + x = rearrange(x, "b (s n) c -> b s n c", b=b, s=s) + return x + + def get_intermediate_layers( + self, + x: torch.Tensor, + n: Union[int, Sequence] = 1, # Layers or n last layers to take + export_feat_layers: List[int] = [], + **kwargs, + ) -> Tuple[Union[torch.Tensor, Tuple[torch.Tensor]]]: + outputs, aux_outputs = self._get_intermediate_layers_not_chunked( + x, n, export_feat_layers=export_feat_layers, **kwargs + ) + camera_tokens = [out[0] for out in outputs] + if outputs[0][1].shape[-1] == self.embed_dim: + outputs = [self.norm(out[1]) for out in outputs] + elif outputs[0][1].shape[-1] == (self.embed_dim * 2): + outputs = [ + torch.cat( + [out[1][..., : self.embed_dim], self.norm(out[1][..., self.embed_dim :])], + dim=-1, + ) + for out in outputs + ] + else: + raise ValueError(f"Invalid output shape: {outputs[0][1].shape}") + aux_outputs = [self.norm(out) for out in aux_outputs] + outputs = [out[..., 1 + self.num_register_tokens :, :] for out in outputs] + aux_outputs = [out[..., 1 + self.num_register_tokens :, :] for out in aux_outputs] + return tuple(zip(outputs, camera_tokens)), aux_outputs + + +def vit_small(patch_size=16, num_register_tokens=0, depth=12, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=384, + depth=depth, + num_heads=6, + mlp_ratio=4, + # block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_base(patch_size=16, num_register_tokens=0, depth=12, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=768, + depth=depth, + num_heads=12, + mlp_ratio=4, + # block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_large(patch_size=16, num_register_tokens=0, depth=24, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=1024, + depth=depth, + num_heads=16, + mlp_ratio=4, + # block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_giant2(patch_size=16, num_register_tokens=0, depth=40, **kwargs): + """ + Close to ViT-giant, with embed-dim 1536 and 24 heads => embed-dim per head 64 + """ + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=1536, + depth=depth, + num_heads=24, + mlp_ratio=4, + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model diff --git a/src/depth_anything_3/model/dpt.py b/src/depth_anything_3/model/dpt.py new file mode 100644 index 0000000000000000000000000000000000000000..337a8a964a7f96fae997c8b12e3ae13e99dbaa58 --- /dev/null +++ b/src/depth_anything_3/model/dpt.py @@ -0,0 +1,458 @@ +# flake8: noqa E501 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict as TyDict +from typing import List, Sequence, Tuple +import torch +import torch.nn as nn +from addict import Dict +from einops import rearrange + +from depth_anything_3.model.utils.head_utils import ( + Permute, + create_uv_grid, + custom_interpolate, + position_grid_to_embed, +) + + +class DPT(nn.Module): + """ + DPT for dense prediction (main head + optional sky head, sky always 1 channel). + + Returns: + - Main head: + * If output_dim>1: { head_name, f"{head_name}_conf" } + * If output_dim==1: { head_name } + - Sky head (if use_sky_head=True): { sky_name } # [B, S, 1, H/down_ratio, W/down_ratio] + """ + + def __init__( + self, + dim_in: int, + *, + patch_size: int = 14, + output_dim: int = 1, + activation: str = "exp", + conf_activation: str = "expp1", + features: int = 256, + out_channels: Sequence[int] = (256, 512, 1024, 1024), + pos_embed: bool = False, + down_ratio: int = 1, + head_name: str = "depth", + # ---- sky head (fixed 1 channel) ---- + use_sky_head: bool = True, + sky_name: str = "sky", + sky_activation: str = "relu", # 'sigmoid' / 'relu' / 'linear' + use_ln_for_heads: bool = False, # If needed, apply LayerNorm on intermediate features of both heads + norm_type: str = "idt", # use to match legacy GS-DPT head, "idt" / "layer" + fusion_block_inplace: bool = False, + ) -> None: + super().__init__() + + # -------------------- configuration -------------------- + self.patch_size = patch_size + self.activation = activation + self.conf_activation = conf_activation + self.pos_embed = pos_embed + self.down_ratio = down_ratio + + # Names + self.head_main = head_name + self.sky_name = sky_name + + # Main head: output dimension and confidence switch + self.out_dim = output_dim + self.has_conf = output_dim > 1 + + # Sky head parameters (always 1 channel) + self.use_sky_head = use_sky_head + self.sky_activation = sky_activation + + # Fixed 4 intermediate outputs + self.intermediate_layer_idx: Tuple[int, int, int, int] = (0, 1, 2, 3) + + # -------------------- token pre-norm + per-stage projection -------------------- + if norm_type == "layer": + self.norm = nn.LayerNorm(dim_in) + elif norm_type == "idt": + self.norm = nn.Identity() + else: + raise Exception(f"Unknown norm_type {norm_type}, should be 'layer' or 'idt'.") + self.projects = nn.ModuleList( + [nn.Conv2d(dim_in, oc, kernel_size=1, stride=1, padding=0) for oc in out_channels] + ) + + # -------------------- Spatial re-size (align to common scale before fusion) -------------------- + # Design consistent with original: relative to patch grid (x4, x2, x1, /2) + self.resize_layers = nn.ModuleList( + [ + nn.ConvTranspose2d( + out_channels[0], out_channels[0], kernel_size=4, stride=4, padding=0 + ), + nn.ConvTranspose2d( + out_channels[1], out_channels[1], kernel_size=2, stride=2, padding=0 + ), + nn.Identity(), + nn.Conv2d(out_channels[3], out_channels[3], kernel_size=3, stride=2, padding=1), + ] + ) + + # -------------------- scratch: stage adapters + main fusion chain -------------------- + self.scratch = _make_scratch(list(out_channels), features, expand=False) + + # Main fusion chain + self.scratch.refinenet1 = _make_fusion_block(features, inplace=fusion_block_inplace) + self.scratch.refinenet2 = _make_fusion_block(features, inplace=fusion_block_inplace) + self.scratch.refinenet3 = _make_fusion_block(features, inplace=fusion_block_inplace) + self.scratch.refinenet4 = _make_fusion_block( + features, has_residual=False, inplace=fusion_block_inplace + ) + + # Heads (shared neck1; then split into two heads) + head_features_1 = features + head_features_2 = 32 + self.scratch.output_conv1 = nn.Conv2d( + head_features_1, head_features_1 // 2, kernel_size=3, stride=1, padding=1 + ) + + ln_seq = ( + [Permute((0, 2, 3, 1)), nn.LayerNorm(head_features_2), Permute((0, 3, 1, 2))] + if use_ln_for_heads + else [] + ) + + # Main head + self.scratch.output_conv2 = nn.Sequential( + nn.Conv2d(head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1), + *ln_seq, + nn.ReLU(inplace=True), + nn.Conv2d(head_features_2, output_dim, kernel_size=1, stride=1, padding=0), + ) + + # Sky head (fixed 1 channel) + if self.use_sky_head: + self.scratch.sky_output_conv2 = nn.Sequential( + nn.Conv2d( + head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1 + ), + *ln_seq, + nn.ReLU(inplace=True), + nn.Conv2d(head_features_2, 1, kernel_size=1, stride=1, padding=0), + ) + + # ------------------------------------------------------------------------- + # Public forward (supports frame chunking to save memory) + # ------------------------------------------------------------------------- + def forward( + self, + feats: List[torch.Tensor], + H: int, + W: int, + patch_start_idx: int, + chunk_size: int = 8, + **kwargs, + ) -> Dict: + """ + Args: + feats: List of 4 entries, each entry is a tensor like [B, S, T, C] (or the 0th element of tuple/list is that tensor). + H, W: Original image dimensions + patch_start_idx: Starting index of patch tokens in sequence (for cropping non-patch tokens) + chunk_size: Chunk size along time dimension S + + Returns: + Dict[str, Tensor] + """ + B, S, N, C = feats[0][0].shape + feats = [feat[0].reshape(B * S, N, C) for feat in feats] + + # update image info, used by the GS-DPT head + extra_kwargs = {} + if "images" in kwargs: + extra_kwargs.update({"images": rearrange(kwargs["images"], "B S ... -> (B S) ...")}) + + if chunk_size is None or chunk_size >= S: + out_dict = self._forward_impl(feats, H, W, patch_start_idx, **extra_kwargs) + out_dict = {k: v.view(B, S, *v.shape[1:]) for k, v in out_dict.items()} + return Dict(out_dict) + + out_dicts: List[TyDict[str, torch.Tensor]] = [] + for s0 in range(0, S, chunk_size): + s1 = min(s0 + chunk_size, S) + kw = {} + if "images" in extra_kwargs: + kw.update({"images": extra_kwargs["images"][s0:s1]}) + out_dicts.append( + self._forward_impl([f[s0:s1] for f in feats], H, W, patch_start_idx, **kw) + ) + out_dict = {k: torch.cat([od[k] for od in out_dicts], dim=0) for k in out_dicts[0].keys()} + out_dict = {k: v.view(B, S, *v.shape[1:]) for k, v in out_dict.items()} + return Dict(out_dict) + + # ------------------------------------------------------------------------- + # Internal forward (single chunk) + # ------------------------------------------------------------------------- + def _forward_impl( + self, + feats: List[torch.Tensor], + H: int, + W: int, + patch_start_idx: int, + ) -> TyDict[str, torch.Tensor]: + B, _, C = feats[0].shape + ph, pw = H // self.patch_size, W // self.patch_size + resized_feats = [] + for stage_idx, take_idx in enumerate(self.intermediate_layer_idx): + x = feats[take_idx][:, patch_start_idx:] # [B*S, N_patch, C] + x = self.norm(x) + # permute -> contiguous before reshape to keep conv input contiguous + x = x.permute(0, 2, 1).contiguous().reshape(B, C, ph, pw) # [B*S, C, ph, pw] + + x = self.projects[stage_idx](x) + if self.pos_embed: + x = self._add_pos_embed(x, W, H) + x = self.resize_layers[stage_idx](x) # Align scale + resized_feats.append(x) + + # 2) Fusion pyramid (main branch only) + fused = self._fuse(resized_feats) + + # 3) Upsample to target resolution, optionally add position encoding again + h_out = int(ph * self.patch_size / self.down_ratio) + w_out = int(pw * self.patch_size / self.down_ratio) + + fused = self.scratch.output_conv1(fused) + fused = custom_interpolate(fused, (h_out, w_out), mode="bilinear", align_corners=True) + if self.pos_embed: + fused = self._add_pos_embed(fused, W, H) + + # 4) Shared neck1 + feat = fused + + # 5) Main head: logits -> activation + main_logits = self.scratch.output_conv2(feat) + outs: TyDict[str, torch.Tensor] = {} + if self.has_conf: + fmap = main_logits.permute(0, 2, 3, 1) + pred = self._apply_activation_single(fmap[..., :-1], self.activation) + conf = self._apply_activation_single(fmap[..., -1], self.conf_activation) + outs[self.head_main] = pred.squeeze(1) + outs[f"{self.head_main}_conf"] = conf.squeeze(1) + else: + outs[self.head_main] = self._apply_activation_single( + main_logits, self.activation + ).squeeze(1) + + # 6) Sky head (fixed 1 channel) + if self.use_sky_head: + sky_logits = self.scratch.sky_output_conv2(feat) + outs[self.sky_name] = self._apply_sky_activation(sky_logits).squeeze(1) + + return outs + + # ------------------------------------------------------------------------- + # Subroutines + # ------------------------------------------------------------------------- + def _fuse(self, feats: List[torch.Tensor]) -> torch.Tensor: + """ + 4-layer top-down fusion, returns finest scale features (after fusion, before neck1). + """ + l1, l2, l3, l4 = feats + + l1_rn = self.scratch.layer1_rn(l1) + l2_rn = self.scratch.layer2_rn(l2) + l3_rn = self.scratch.layer3_rn(l3) + l4_rn = self.scratch.layer4_rn(l4) + + # 4 -> 3 -> 2 -> 1 + out = self.scratch.refinenet4(l4_rn, size=l3_rn.shape[2:]) + out = self.scratch.refinenet3(out, l3_rn, size=l2_rn.shape[2:]) + out = self.scratch.refinenet2(out, l2_rn, size=l1_rn.shape[2:]) + out = self.scratch.refinenet1(out, l1_rn) + return out + + def _apply_activation_single( + self, x: torch.Tensor, activation: str = "linear" + ) -> torch.Tensor: + """ + Apply activation to single channel output, maintaining semantic consistency with value branch in multi-channel case. + Supports: exp / relu / sigmoid / softplus / tanh / linear / expp1 + """ + act = activation.lower() if isinstance(activation, str) else activation + if act == "exp": + return torch.exp(x) + if act == "expp1": + return torch.exp(x) + 1 + if act == "expm1": + return torch.expm1(x) + if act == "relu": + return torch.relu(x) + if act == "sigmoid": + return torch.sigmoid(x) + if act == "softplus": + return torch.nn.functional.softplus(x) + if act == "tanh": + return torch.tanh(x) + # Default linear + return x + + def _apply_sky_activation(self, x: torch.Tensor) -> torch.Tensor: + """ + Sky head activation (fixed 1 channel): + * 'sigmoid' -> Sigmoid probability map + * 'relu' -> ReLU positive domain output + * 'linear' -> Original value (logits) + """ + act = ( + self.sky_activation.lower() + if isinstance(self.sky_activation, str) + else self.sky_activation + ) + if act == "sigmoid": + return torch.sigmoid(x) + if act == "relu": + return torch.relu(x) + # 'linear' + return x + + def _add_pos_embed(self, x: torch.Tensor, W: int, H: int, ratio: float = 0.1) -> torch.Tensor: + """Simple UV position encoding directly added to feature map.""" + pw, ph = x.shape[-1], x.shape[-2] + pe = create_uv_grid(pw, ph, aspect_ratio=W / H, dtype=x.dtype, device=x.device) + pe = position_grid_to_embed(pe, x.shape[1]) * ratio + pe = pe.permute(2, 0, 1)[None].expand(x.shape[0], -1, -1, -1) + return x + pe + + +# ----------------------------------------------------------------------------- +# Building blocks (preserved, consistent with original) +# ----------------------------------------------------------------------------- +def _make_fusion_block( + features: int, + size: Tuple[int, int] = None, + has_residual: bool = True, + groups: int = 1, + inplace: bool = False, +) -> nn.Module: + return FeatureFusionBlock( + features=features, + activation=nn.ReLU(inplace=inplace), + deconv=False, + bn=False, + expand=False, + align_corners=True, + size=size, + has_residual=has_residual, + groups=groups, + ) + + +def _make_scratch( + in_shape: List[int], out_shape: int, groups: int = 1, expand: bool = False +) -> nn.Module: + scratch = nn.Module() + # Optional expansion by stage + c1 = out_shape + c2 = out_shape * (2 if expand else 1) + c3 = out_shape * (4 if expand else 1) + c4 = out_shape * (8 if expand else 1) + + scratch.layer1_rn = nn.Conv2d(in_shape[0], c1, 3, 1, 1, bias=False, groups=groups) + scratch.layer2_rn = nn.Conv2d(in_shape[1], c2, 3, 1, 1, bias=False, groups=groups) + scratch.layer3_rn = nn.Conv2d(in_shape[2], c3, 3, 1, 1, bias=False, groups=groups) + scratch.layer4_rn = nn.Conv2d(in_shape[3], c4, 3, 1, 1, bias=False, groups=groups) + return scratch + + +class ResidualConvUnit(nn.Module): + """Lightweight residual convolution block for fusion""" + + def __init__(self, features: int, activation: nn.Module, bn: bool, groups: int = 1) -> None: + super().__init__() + self.bn = bn + self.groups = groups + self.conv1 = nn.Conv2d(features, features, 3, 1, 1, bias=True, groups=groups) + self.conv2 = nn.Conv2d(features, features, 3, 1, 1, bias=True, groups=groups) + self.norm1 = None + self.norm2 = None + self.activation = activation + self.skip_add = nn.quantized.FloatFunctional() + + def forward(self, x: torch.Tensor) -> torch.Tensor: # type: ignore[override] + out = self.activation(x) + out = self.conv1(out) + if self.norm1 is not None: + out = self.norm1(out) + + out = self.activation(out) + out = self.conv2(out) + if self.norm2 is not None: + out = self.norm2(out) + + return self.skip_add.add(out, x) + + +class FeatureFusionBlock(nn.Module): + """Top-down fusion block: (optional) residual merge + upsampling + 1x1 contraction""" + + def __init__( + self, + features: int, + activation: nn.Module, + deconv: bool = False, + bn: bool = False, + expand: bool = False, + align_corners: bool = True, + size: Tuple[int, int] = None, + has_residual: bool = True, + groups: int = 1, + ) -> None: + super().__init__() + self.align_corners = align_corners + self.size = size + self.has_residual = has_residual + + self.resConfUnit1 = ( + ResidualConvUnit(features, activation, bn, groups=groups) if has_residual else None + ) + self.resConfUnit2 = ResidualConvUnit(features, activation, bn, groups=groups) + + out_features = (features // 2) if expand else features + self.out_conv = nn.Conv2d(features, out_features, 1, 1, 0, bias=True, groups=groups) + self.skip_add = nn.quantized.FloatFunctional() + + def forward(self, *xs: torch.Tensor, size: Tuple[int, int] = None) -> torch.Tensor: # type: ignore[override] + """ + xs: + - xs[0]: Top branch input + - xs[1]: Lateral input (can do residual addition with top branch) + """ + y = xs[0] + if self.has_residual and len(xs) > 1 and self.resConfUnit1 is not None: + y = self.skip_add.add(y, self.resConfUnit1(xs[1])) + + y = self.resConfUnit2(y) + + # Upsampling + if (size is None) and (self.size is None): + up_kwargs = {"scale_factor": 2} + elif size is None: + up_kwargs = {"size": self.size} + else: + up_kwargs = {"size": size} + + y = custom_interpolate(y, **up_kwargs, mode="bilinear", align_corners=self.align_corners) + y = self.out_conv(y) + return y diff --git a/src/depth_anything_3/model/dualdpt.py b/src/depth_anything_3/model/dualdpt.py new file mode 100644 index 0000000000000000000000000000000000000000..e84c5cf55a4698b3df8bff1eda7306778dd63a52 --- /dev/null +++ b/src/depth_anything_3/model/dualdpt.py @@ -0,0 +1,488 @@ +# flake8: noqa E501 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Sequence, Tuple +import torch +import torch.nn as nn +from addict import Dict + +from depth_anything_3.model.dpt import _make_fusion_block, _make_scratch +from depth_anything_3.model.utils.head_utils import ( + Permute, + create_uv_grid, + custom_interpolate, + position_grid_to_embed, +) + + +class DualDPT(nn.Module): + """ + Dual-head DPT for dense prediction with an always-on auxiliary head. + + Architectural notes: + - Sky/object branches are removed. + - `intermediate_layer_idx` is fixed to (0, 1, 2, 3). + - Auxiliary head has its **own** fusion blocks (no fusion_inplace / no sharing). + - Auxiliary head is internally multi-level; **only the final level** is returned. + - Returns a **dict** with keys from `head_names`, e.g.: + { main_name, f"{main_name}_conf", aux_name, f"{aux_name}_conf" } + - `feature_only` is fixed to False. + """ + + def __init__( + self, + dim_in: int, + *, + patch_size: int = 14, + output_dim: int = 2, + activation: str = "exp", + conf_activation: str = "expp1", + features: int = 256, + out_channels: Sequence[int] = (256, 512, 1024, 1024), + pos_embed: bool = True, + down_ratio: int = 1, + aux_pyramid_levels: int = 4, + aux_out1_conv_num: int = 5, + head_names: Tuple[str, str] = ("depth", "ray"), + ) -> None: + super().__init__() + + # -------------------- configuration -------------------- + self.patch_size = patch_size + self.activation = activation + self.conf_activation = conf_activation + self.pos_embed = pos_embed + self.down_ratio = down_ratio + + self.aux_levels = aux_pyramid_levels + self.aux_out1_conv_num = aux_out1_conv_num + + # names ONLY come from config (no hard-coded strings elsewhere) + self.head_main, self.head_aux = head_names + + # Always expect 4 scales; enforce intermediate idx = (0, 1, 2, 3) + self.intermediate_layer_idx: Tuple[int, int, int, int] = (0, 1, 2, 3) + + # -------------------- token pre-norm + per-stage projection -------------------- + self.norm = nn.LayerNorm(dim_in) + self.projects = nn.ModuleList( + [nn.Conv2d(dim_in, oc, kernel_size=1, stride=1, padding=0) for oc in out_channels] + ) + + # -------------------- spatial re-sizers (align to common scale before fusion) -------------------- + # design: stage strides (x4, x2, x1, /2) relative to patch grid to align to a common pivot scale + self.resize_layers = nn.ModuleList( + [ + nn.ConvTranspose2d( + out_channels[0], out_channels[0], kernel_size=4, stride=4, padding=0 + ), + nn.ConvTranspose2d( + out_channels[1], out_channels[1], kernel_size=2, stride=2, padding=0 + ), + nn.Identity(), + nn.Conv2d(out_channels[3], out_channels[3], kernel_size=3, stride=2, padding=1), + ] + ) + + # -------------------- scratch: stage adapters + fusion (main & aux are separate) -------------------- + self.scratch = _make_scratch(list(out_channels), features, expand=False) + + # Main fusion chain (independent) + self.scratch.refinenet1 = _make_fusion_block(features) + self.scratch.refinenet2 = _make_fusion_block(features) + self.scratch.refinenet3 = _make_fusion_block(features) + self.scratch.refinenet4 = _make_fusion_block(features, has_residual=False) + + # Primary head neck + head (independent) + head_features_1 = features + head_features_2 = 32 + self.scratch.output_conv1 = nn.Conv2d( + head_features_1, head_features_1 // 2, kernel_size=3, stride=1, padding=1 + ) + self.scratch.output_conv2 = nn.Sequential( + nn.Conv2d(head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1), + nn.ReLU(inplace=True), + nn.Conv2d(head_features_2, output_dim, kernel_size=1, stride=1, padding=0), + ) + + # Auxiliary fusion chain (completely separate; no sharing, i.e., "fusion_inplace=False") + self.scratch.refinenet1_aux = _make_fusion_block(features) + self.scratch.refinenet2_aux = _make_fusion_block(features) + self.scratch.refinenet3_aux = _make_fusion_block(features) + self.scratch.refinenet4_aux = _make_fusion_block(features, has_residual=False) + + # Aux pre-head per level (we will only *return final level*) + self.scratch.output_conv1_aux = nn.ModuleList( + [self._make_aux_out1_block(head_features_1) for _ in range(self.aux_levels)] + ) + + # Aux final projection per level + use_ln = True + ln_seq = ( + [Permute((0, 2, 3, 1)), nn.LayerNorm(head_features_2), Permute((0, 3, 1, 2))] + if use_ln + else [] + ) + self.scratch.output_conv2_aux = nn.ModuleList( + [ + nn.Sequential( + nn.Conv2d( + head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1 + ), + *ln_seq, + nn.ReLU(inplace=True), + nn.Conv2d(head_features_2, 7, kernel_size=1, stride=1, padding=0), + ) + for _ in range(self.aux_levels) + ] + ) + + # ------------------------------------------------------------------------- + # Public forward (supports frame chunking for memory) + # ------------------------------------------------------------------------- + + def forward( + self, + feats: List[torch.Tensor], + H: int, + W: int, + patch_start_idx: int, + chunk_size: int = 8, + ) -> Dict[str, torch.Tensor]: + """ + Args: + aggregated_tokens_list: List of 4 tensors [B, S, T, C] from transformer. + images: [B, S, 3, H, W], in [0, 1]. + patch_start_idx: Patch-token start in the token sequence (to drop non-patch tokens). + frames_chunk_size: Optional chunking along S for memory. + + Returns: + Dict[str, Tensor] with keys based on `head_names`, e.g.: + self.head_main, f"{self.head_main}_conf", + self.head_aux, f"{self.head_aux}_conf" + Shapes: + main: [B, S, out_dim, H/down_ratio, W/down_ratio] + main_cf: [B, S, 1, H/down_ratio, W/down_ratio] + aux: [B, S, 7, H/down_ratio, W/down_ratio] + aux_cf: [B, S, 1, H/down_ratio, W/down_ratio] + """ + B, S, N, C = feats[0][0].shape + feats = [feat[0].reshape(B * S, N, C) for feat in feats] + if chunk_size is None or chunk_size >= S: + out_dict = self._forward_impl(feats, H, W, patch_start_idx) + out_dict = {k: v.reshape(B, S, *v.shape[1:]) for k, v in out_dict.items()} + return Dict(out_dict) + out_dicts = [] + for s0 in range(0, S, chunk_size): + s1 = min(s0 + chunk_size, S) + out_dict = self._forward_impl( + [feat[s0:s1] for feat in feats], + H, + W, + patch_start_idx, + ) + out_dicts.append(out_dict) + out_dict = { + k: torch.cat([out_dict[k] for out_dict in out_dicts], dim=0) + for k in out_dicts[0].keys() + } + out_dict = {k: v.view(B, S, *v.shape[1:]) for k, v in out_dict.items()} + return Dict(out_dict) + + # ------------------------------------------------------------------------- + # Internal forward (single chunk) + # ------------------------------------------------------------------------- + + def _forward_impl( + self, + feats: List[torch.Tensor], + H: int, + W: int, + patch_start_idx: int, + ) -> Dict[str, torch.Tensor]: + B, _, C = feats[0].shape + ph, pw = H // self.patch_size, W // self.patch_size + resized_feats = [] + for stage_idx, take_idx in enumerate(self.intermediate_layer_idx): + x = feats[take_idx][:, patch_start_idx:] + x = self.norm(x) + x = x.permute(0, 2, 1).reshape(B, C, ph, pw) # [B*S, C, ph, pw] + + x = self.projects[stage_idx](x) + if self.pos_embed: + x = self._add_pos_embed(x, W, H) + x = self.resize_layers[stage_idx](x) # align scales + resized_feats.append(x) + + # 2) Fuse pyramid (main & aux are completely independent) + fused_main, fused_aux_pyr = self._fuse(resized_feats) + + # 3) Upsample to target resolution and (optional) add pos-embed again + h_out = int(ph * self.patch_size / self.down_ratio) + w_out = int(pw * self.patch_size / self.down_ratio) + + fused_main = custom_interpolate( + fused_main, (h_out, w_out), mode="bilinear", align_corners=True + ) + if self.pos_embed: + fused_main = self._add_pos_embed(fused_main, W, H) + + # Primary head: conv1 -> conv2 -> activate + # fused_main = self.scratch.output_conv1(fused_main) + main_logits = self.scratch.output_conv2(fused_main) + fmap = main_logits.permute(0, 2, 3, 1) + main_pred = self._apply_activation_single(fmap[..., :-1], self.activation) + main_conf = self._apply_activation_single(fmap[..., -1], self.conf_activation) + + # Auxiliary head (multi-level inside) -> only last level returned (after activation) + last_aux = fused_aux_pyr[-1] + if self.pos_embed: + last_aux = self._add_pos_embed(last_aux, W, H) + # neck (per-level pre-conv) then final projection (only for last level) + # last_aux = self.scratch.output_conv1_aux[-1](last_aux) + last_aux_logits = self.scratch.output_conv2_aux[-1](last_aux) + fmap_last = last_aux_logits.permute(0, 2, 3, 1) + aux_pred = self._apply_activation_single(fmap_last[..., :-1], "linear") + aux_conf = self._apply_activation_single(fmap_last[..., -1], self.conf_activation) + return { + self.head_main: main_pred.squeeze(-1), + f"{self.head_main}_conf": main_conf, + self.head_aux: aux_pred, + f"{self.head_aux}_conf": aux_conf, + } + + # ------------------------------------------------------------------------- + # Subroutines + # ------------------------------------------------------------------------- + + def _fuse(self, feats: List[torch.Tensor]) -> Tuple[torch.Tensor, List[torch.Tensor]]: + """ + Feature pyramid fusion. + Returns: + fused_main: Tensor at finest scale (after refinenet1) + aux_pyr: List of aux tensors at each level (pre out_conv1_aux) + """ + l1, l2, l3, l4 = feats + + l1_rn = self.scratch.layer1_rn(l1) + l2_rn = self.scratch.layer2_rn(l2) + l3_rn = self.scratch.layer3_rn(l3) + l4_rn = self.scratch.layer4_rn(l4) + + # level 4 -> 3 + out = self.scratch.refinenet4(l4_rn, size=l3_rn.shape[2:]) + aux_out = self.scratch.refinenet4_aux(l4_rn, size=l3_rn.shape[2:]) + aux_list: List[torch.Tensor] = [] + if self.aux_levels >= 4: + aux_list.append(aux_out) + + # level 3 -> 2 + out = self.scratch.refinenet3(out, l3_rn, size=l2_rn.shape[2:]) + aux_out = self.scratch.refinenet3_aux(aux_out, l3_rn, size=l2_rn.shape[2:]) + if self.aux_levels >= 3: + aux_list.append(aux_out) + + # level 2 -> 1 + out = self.scratch.refinenet2(out, l2_rn, size=l1_rn.shape[2:]) + aux_out = self.scratch.refinenet2_aux(aux_out, l2_rn, size=l1_rn.shape[2:]) + if self.aux_levels >= 2: + aux_list.append(aux_out) + + # level 1 (final) + out = self.scratch.refinenet1(out, l1_rn) + aux_out = self.scratch.refinenet1_aux(aux_out, l1_rn) + aux_list.append(aux_out) + + out = self.scratch.output_conv1(out) + aux_list = [self.scratch.output_conv1_aux[i](aux) for i, aux in enumerate(aux_list)] + + return out, aux_list + + def _add_pos_embed(self, x: torch.Tensor, W: int, H: int, ratio: float = 0.1) -> torch.Tensor: + """Simple UV positional embedding added to feature maps.""" + pw, ph = x.shape[-1], x.shape[-2] + pe = create_uv_grid(pw, ph, aspect_ratio=W / H, dtype=x.dtype, device=x.device) + pe = position_grid_to_embed(pe, x.shape[1]) * ratio + pe = pe.permute(2, 0, 1)[None].expand(x.shape[0], -1, -1, -1) + return x + pe + + def _make_aux_out1_block(self, in_ch: int) -> nn.Sequential: + """Factory for the aux pre-head stack before the final 1x1 projection.""" + if self.aux_out1_conv_num == 5: + return nn.Sequential( + nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1), + nn.Conv2d(in_ch // 2, in_ch, 3, 1, 1), + nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1), + nn.Conv2d(in_ch // 2, in_ch, 3, 1, 1), + nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1), + ) + if self.aux_out1_conv_num == 3: + return nn.Sequential( + nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1), + nn.Conv2d(in_ch // 2, in_ch, 3, 1, 1), + nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1), + ) + if self.aux_out1_conv_num == 1: + return nn.Sequential(nn.Conv2d(in_ch, in_ch // 2, 3, 1, 1)) + raise ValueError(f"aux_out1_conv_num {self.aux_out1_conv_num} not supported") + + def _apply_activation_single( + self, x: torch.Tensor, activation: str = "linear" + ) -> torch.Tensor: + """ + Apply activation to single channel output, maintaining semantic consistency with value branch in multi-channel case. + Supports: exp / relu / sigmoid / softplus / tanh / linear / expp1 + """ + act = activation.lower() if isinstance(activation, str) else activation + if act == "exp": + return torch.exp(x) + if act == "expm1": + return torch.expm1(x) + if act == "expp1": + return torch.exp(x) + 1 + if act == "relu": + return torch.relu(x) + if act == "sigmoid": + return torch.sigmoid(x) + if act == "softplus": + return torch.nn.functional.softplus(x) + if act == "tanh": + return torch.tanh(x) + # Default linear + return x + + +# # ----------------------------------------------------------------------------- +# # Building blocks (tidy) +# # ----------------------------------------------------------------------------- + + +# def _make_fusion_block( +# features: int, +# size: Tuple[int, int] = None, +# has_residual: bool = True, +# groups: int = 1, +# inplace: bool = False, # <- activation uses inplace=True by default; not related to "fusion_inplace" +# ) -> nn.Module: +# return FeatureFusionBlock( +# features=features, +# activation=nn.ReLU(inplace=inplace), +# deconv=False, +# bn=False, +# expand=False, +# align_corners=True, +# size=size, +# has_residual=has_residual, +# groups=groups, +# ) + + +# def _make_scratch( +# in_shape: List[int], out_shape: int, groups: int = 1, expand: bool = False +# ) -> nn.Module: +# scratch = nn.Module() +# # optionally expand widths by stage +# c1 = out_shape +# c2 = out_shape * (2 if expand else 1) +# c3 = out_shape * (4 if expand else 1) +# c4 = out_shape * (8 if expand else 1) + +# scratch.layer1_rn = nn.Conv2d(in_shape[0], c1, 3, 1, 1, bias=False, groups=groups) +# scratch.layer2_rn = nn.Conv2d(in_shape[1], c2, 3, 1, 1, bias=False, groups=groups) +# scratch.layer3_rn = nn.Conv2d(in_shape[2], c3, 3, 1, 1, bias=False, groups=groups) +# scratch.layer4_rn = nn.Conv2d(in_shape[3], c4, 3, 1, 1, bias=False, groups=groups) +# return scratch + + +# class ResidualConvUnit(nn.Module): +# """Lightweight residual conv block used within fusion.""" + +# def __init__(self, features: int, activation: nn.Module, bn: bool, groups: int = 1) -> None: +# super().__init__() +# self.bn = bn +# self.groups = groups +# self.conv1 = nn.Conv2d(features, features, 3, 1, 1, bias=True, groups=groups) +# self.conv2 = nn.Conv2d(features, features, 3, 1, 1, bias=True, groups=groups) +# self.norm1 = None +# self.norm2 = None +# self.activation = activation +# self.skip_add = nn.quantized.FloatFunctional() + +# def forward(self, x: torch.Tensor) -> torch.Tensor: # type: ignore[override] +# out = self.activation(x) +# out = self.conv1(out) +# if self.norm1 is not None: +# out = self.norm1(out) + +# out = self.activation(out) +# out = self.conv2(out) +# if self.norm2 is not None: +# out = self.norm2(out) + +# return self.skip_add.add(out, x) + + +# class FeatureFusionBlock(nn.Module): +# """Top-down fusion block: (optional) residual merge + upsample + 1x1 shrink.""" + +# def __init__( +# self, +# features: int, +# activation: nn.Module, +# deconv: bool = False, +# bn: bool = False, +# expand: bool = False, +# align_corners: bool = True, +# size: Tuple[int, int] = None, +# has_residual: bool = True, +# groups: int = 1, +# ) -> None: +# super().__init__() +# self.align_corners = align_corners +# self.size = size +# self.has_residual = has_residual + +# self.resConfUnit1 = ( +# ResidualConvUnit(features, activation, bn, groups=groups) if has_residual else None +# ) +# self.resConfUnit2 = ResidualConvUnit(features, activation, bn, groups=groups) + +# out_features = (features // 2) if expand else features +# self.out_conv = nn.Conv2d(features, out_features, 1, 1, 0, bias=True, groups=groups) +# self.skip_add = nn.quantized.FloatFunctional() + +# def forward(self, *xs: torch.Tensor, size: Tuple[int, int] = None) -> torch.Tensor: # type: ignore[override] +# """ +# xs: +# - xs[0]: top input +# - xs[1]: (optional) lateral (to be added with residual) +# """ +# y = xs[0] +# if self.has_residual and len(xs) > 1 and self.resConfUnit1 is not None: +# y = self.skip_add.add(y, self.resConfUnit1(xs[1])) + +# y = self.resConfUnit2(y) + +# # upsample +# if (size is None) and (self.size is None): +# up_kwargs = {"scale_factor": 2} +# elif size is None: +# up_kwargs = {"size": self.size} +# else: +# up_kwargs = {"size": size} + +# y = custom_interpolate(y, **up_kwargs, mode="bilinear", align_corners=self.align_corners) +# y = self.out_conv(y) +# return y diff --git a/src/depth_anything_3/model/gs_adapter.py b/src/depth_anything_3/model/gs_adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..856b272cf8fe734da8d4bc351afa32829e12d4e4 --- /dev/null +++ b/src/depth_anything_3/model/gs_adapter.py @@ -0,0 +1,201 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +from einops import einsum, rearrange, repeat +from torch import nn + +from depth_anything_3.model.utils.transform import cam_quat_xyzw_to_world_quat_wxyz +from depth_anything_3.specs import Gaussians +from depth_anything_3.utils.geometry import affine_inverse, get_world_rays, sample_image_grid +from depth_anything_3.utils.pose_align import batch_align_poses_umeyama +from depth_anything_3.utils.sh_helpers import rotate_sh + + +class GaussianAdapter(nn.Module): + + def __init__( + self, + sh_degree: int = 0, + pred_color: bool = False, + pred_offset_depth: bool = False, + pred_offset_xy: bool = True, + gaussian_scale_min: float = 1e-5, + gaussian_scale_max: float = 30.0, + ): + super().__init__() + self.sh_degree = sh_degree + self.pred_color = pred_color + self.pred_offset_depth = pred_offset_depth + self.pred_offset_xy = pred_offset_xy + self.gaussian_scale_min = gaussian_scale_min + self.gaussian_scale_max = gaussian_scale_max + + # Create a mask for the spherical harmonics coefficients. This ensures that at + # initialization, the coefficients are biased towards having a large DC + # component and small view-dependent components. + if not pred_color: + self.register_buffer( + "sh_mask", + torch.ones((self.d_sh,), dtype=torch.float32), + persistent=False, + ) + for degree in range(1, sh_degree + 1): + self.sh_mask[degree**2 : (degree + 1) ** 2] = 0.1 * 0.25**degree + + def forward( + self, + extrinsics: torch.Tensor, # "*#batch 4 4" + intrinsics: torch.Tensor, # "*#batch 3 3" + depths: torch.Tensor, # "*#batch" + opacities: torch.Tensor, # "*#batch" | "*#batch _" + raw_gaussians: torch.Tensor, # "*#batch _" + image_shape: tuple[int, int], + eps: float = 1e-8, + gt_extrinsics: Optional[torch.Tensor] = None, # "*#batch 4 4" + **kwargs, + ) -> Gaussians: + device = extrinsics.device + dtype = raw_gaussians.dtype + H, W = image_shape + b, v = raw_gaussians.shape[:2] + + # get cam2worlds and intr_normed to adapt to 3DGS codebase + cam2worlds = affine_inverse(extrinsics) + intr_normed = intrinsics.clone().detach() + intr_normed[..., 0, :] /= W + intr_normed[..., 1, :] /= H + + # 1. compute 3DGS means + # 1.1) offset the predicted depth if needed + if self.pred_offset_depth: + gs_depths = depths + raw_gaussians[..., -1] + raw_gaussians = raw_gaussians[..., :-1] + else: + gs_depths = depths + # 1.2) align predicted poses with GT if needed + if gt_extrinsics is not None and not torch.equal(extrinsics, gt_extrinsics): + try: + _, _, pose_scales = batch_align_poses_umeyama( + gt_extrinsics.detach().float(), + extrinsics.detach().float(), + ) + except Exception: + pose_scales = torch.ones_like(extrinsics[:, 0, 0, 0]) + pose_scales = torch.clamp(pose_scales, min=1 / 3.0, max=3.0) + cam2worlds[:, :, :3, 3] = cam2worlds[:, :, :3, 3] * rearrange( + pose_scales, "b -> b () ()" + ) # [b, i, j] + gs_depths = gs_depths * rearrange(pose_scales, "b -> b () () ()") # [b, v, h, w] + # 1.3) casting xy in image space + xy_ray, _ = sample_image_grid((H, W), device) + xy_ray = xy_ray[None, None, ...].expand(b, v, -1, -1, -1) # b v h w xy + # offset xy if needed + if self.pred_offset_xy: + pixel_size = 1 / torch.tensor((W, H), dtype=xy_ray.dtype, device=device) + offset_xy = raw_gaussians[..., :2] + xy_ray = xy_ray + offset_xy * pixel_size + raw_gaussians = raw_gaussians[..., 2:] # skip the offset_xy + # 1.4) unproject depth + xy to world ray + origins, directions = get_world_rays( + xy_ray, + repeat(cam2worlds, "b v i j -> b v h w i j", h=H, w=W), + repeat(intr_normed, "b v i j -> b v h w i j", h=H, w=W), + ) + gs_means_world = origins + directions * gs_depths[..., None] + gs_means_world = rearrange(gs_means_world, "b v h w d -> b (v h w) d") + + # 2. compute other GS attributes + scales, rotations, sh = raw_gaussians.split((3, 4, 3 * self.d_sh), dim=-1) + + # 2.1) 3DGS scales + # make the scale invarient to resolution + scale_min = self.gaussian_scale_min + scale_max = self.gaussian_scale_max + scales = scale_min + (scale_max - scale_min) * scales.sigmoid() + pixel_size = 1 / torch.tensor((W, H), dtype=dtype, device=device) + multiplier = self.get_scale_multiplier(intr_normed, pixel_size) + gs_scales = scales * gs_depths[..., None] * multiplier[..., None, None, None] + gs_scales = rearrange(gs_scales, "b v h w d -> b (v h w) d") + + # 2.2) 3DGS quaternion (world space) + # due to historical issue, assume quaternion in order xyzw, not wxyz + # Normalize the quaternion features to yield a valid quaternion. + rotations = rotations / (rotations.norm(dim=-1, keepdim=True) + eps) + # rotate them to world space + cam_quat_xyzw = rearrange(rotations, "b v h w c -> b (v h w) c") + c2w_mat = repeat( + cam2worlds, + "b v i j -> b (v h w) i j", + h=H, + w=W, + ) + world_quat_wxyz = cam_quat_xyzw_to_world_quat_wxyz(cam_quat_xyzw, c2w_mat) + gs_rotations_world = world_quat_wxyz # b (v h w) c + + # 2.3) 3DGS color / SH coefficient (world space) + sh = rearrange(sh, "... (xyz d_sh) -> ... xyz d_sh", xyz=3) + if not self.pred_color: + sh = sh * self.sh_mask + + if self.pred_color or self.sh_degree == 0: + # predict pre-computed color or predict only DC band, no need to transform + gs_sh_world = sh + else: + gs_sh_world = rotate_sh(sh, cam2worlds[:, :, None, None, None, :3, :3]) + gs_sh_world = rearrange(gs_sh_world, "b v h w xyz d_sh -> b (v h w) xyz d_sh") + + # 2.4) 3DGS opacity + gs_opacities = rearrange(opacities, "b v h w ... -> b (v h w) ...") + + return Gaussians( + means=gs_means_world, + harmonics=gs_sh_world, + opacities=gs_opacities, + scales=gs_scales, + rotations=gs_rotations_world, + ) + + def get_scale_multiplier( + self, + intrinsics: torch.Tensor, # "*#batch 3 3" + pixel_size: torch.Tensor, # "*#batch 2" + multiplier: float = 0.1, + ) -> torch.Tensor: # " *batch" + xy_multipliers = multiplier * einsum( + intrinsics[..., :2, :2].float().inverse().to(intrinsics), + pixel_size, + "... i j, j -> ... i", + ) + return xy_multipliers.sum(dim=-1) + + @property + def d_sh(self) -> int: + return 1 if self.pred_color else (self.sh_degree + 1) ** 2 + + @property + def d_in(self) -> int: + # provided as reference to the gs_dpt output dim + raw_gs_dim = 0 + if self.pred_offset_xy: + raw_gs_dim += 2 + raw_gs_dim += 3 # scales + raw_gs_dim += 4 # quaternion + raw_gs_dim += 3 * self.d_sh # color + if self.pred_offset_depth: + raw_gs_dim += 1 + + return raw_gs_dim diff --git a/src/depth_anything_3/model/gsdpt.py b/src/depth_anything_3/model/gsdpt.py new file mode 100644 index 0000000000000000000000000000000000000000..9579619f5597f07bff158efb88895a18a9828a39 --- /dev/null +++ b/src/depth_anything_3/model/gsdpt.py @@ -0,0 +1,134 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict as TyDict +from typing import List, Sequence + +import torch +import torch.nn as nn + +from depth_anything_3.model.dpt import DPT +from depth_anything_3.model.utils.head_utils import activate_head_gs, custom_interpolate + + +class GSDPT(DPT): + + def __init__( + self, + dim_in: int, + patch_size: int = 14, + output_dim: int = 4, + activation: str = "linear", + conf_activation: str = "sigmoid", + features: int = 256, + out_channels: Sequence[int] = (256, 512, 1024, 1024), + pos_embed: bool = True, + feature_only: bool = False, + down_ratio: int = 1, + conf_dim: int = 1, + norm_type: str = "idt", # use to match legacy GS-DPT head, "idt" / "layer" + fusion_block_inplace: bool = False, + ) -> None: + super().__init__( + dim_in=dim_in, + patch_size=patch_size, + output_dim=output_dim, + activation=activation, + conf_activation=conf_activation, + features=features, + out_channels=out_channels, + pos_embed=pos_embed, + down_ratio=down_ratio, + head_name="raw_gs", + use_sky_head=False, + norm_type=norm_type, + fusion_block_inplace=fusion_block_inplace, + ) + self.conf_dim = conf_dim + if conf_dim and conf_dim > 1: + assert ( + conf_activation == "linear" + ), "use linear prediction when using view-dependent opacity" + + merger_out_dim = features if feature_only else features // 2 + self.images_merger = nn.Sequential( + nn.Conv2d(3, merger_out_dim // 4, 3, 1, 1), # fewer channels first + nn.GELU(), + nn.Conv2d(merger_out_dim // 4, merger_out_dim // 2, 3, 1, 1), + nn.GELU(), + nn.Conv2d(merger_out_dim // 2, merger_out_dim, 3, 1, 1), + nn.GELU(), + ) + + # ------------------------------------------------------------------------- + # Internal forward (single chunk) + # ------------------------------------------------------------------------- + def _forward_impl( + self, + feats: List[torch.Tensor], + H: int, + W: int, + patch_start_idx: int, + images: torch.Tensor, + ) -> TyDict[str, torch.Tensor]: + B, _, C = feats[0].shape + ph, pw = H // self.patch_size, W // self.patch_size + resized_feats = [] + for stage_idx, take_idx in enumerate(self.intermediate_layer_idx): + x = feats[take_idx][:, patch_start_idx:] # [B*S, N_patch, C] + x = self.norm(x) + x = x.permute(0, 2, 1).reshape(B, C, ph, pw) # [B*S, C, ph, pw] + + x = self.projects[stage_idx](x) + if self.pos_embed: + x = self._add_pos_embed(x, W, H) + x = self.resize_layers[stage_idx](x) # Align scale + resized_feats.append(x) + + # 2) Fusion pyramid (main branch only) + fused = self._fuse(resized_feats) + fused = self.scratch.output_conv1(fused) + + # 3) Upsample to target resolution, optionally add position encoding again + h_out = int(ph * self.patch_size / self.down_ratio) + w_out = int(pw * self.patch_size / self.down_ratio) + + fused = custom_interpolate(fused, (h_out, w_out), mode="bilinear", align_corners=True) + + # inject the image information here + fused = fused + self.images_merger(images) + + if self.pos_embed: + fused = self._add_pos_embed(fused, W, H) + + # 4) Shared neck1 + # feat = self.scratch.output_conv1(fused) + feat = fused + + # 5) Main head: logits -> activate_head or single channel activation + main_logits = self.scratch.output_conv2(feat) + outs: TyDict[str, torch.Tensor] = {} + if self.has_conf: + pred, conf = activate_head_gs( + main_logits, + activation=self.activation, + conf_activation=self.conf_activation, + conf_dim=self.conf_dim, + ) + outs[self.head_main] = pred.squeeze(1) + outs[f"{self.head_main}_conf"] = conf.squeeze(1) + else: + outs[self.head_main] = self._apply_activation_single(main_logits).squeeze(1) + + return outs diff --git a/src/depth_anything_3/model/reference_view_selector.py b/src/depth_anything_3/model/reference_view_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..b0f293f4805c55b57f88c94a3ec25e5d0831e213 --- /dev/null +++ b/src/depth_anything_3/model/reference_view_selector.py @@ -0,0 +1,223 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Reference View Selection Strategies + +This module provides different strategies for selecting a reference view +from multiple input views in multi-view depth estimation. +""" + +from typing import Literal + +import torch + +RefViewStrategy = Literal["first", "middle", "saddle_balanced", "saddle_sim_range"] + + +def select_reference_view( + x: torch.Tensor, + strategy: RefViewStrategy = "saddle_balanced", +) -> torch.Tensor: + """ + Select a reference view from multiple views using the specified strategy. + + Args: + x: Input tensor of shape (B, S, N, C) where + B = batch size + S = number of views + N = number of tokens + C = channel dimension + strategy: Selection strategy, one of: + - "first": Always select the first view + - "middle": Select the middle view + - "saddle_balanced": Select view with balanced features across multiple metrics + - "saddle_sim_range": Select view with largest similarity range + + Returns: + b_idx: Tensor of shape (B,) containing the selected view index for each batch + """ + B, S, N, C = x.shape + + # For single view, no reordering needed + if S <= 1: + return torch.zeros(B, dtype=torch.long, device=x.device) + + # Simple position-based strategies + if strategy == "first": + return torch.zeros(B, dtype=torch.long, device=x.device) + + elif strategy == "middle": + return torch.full((B,), S // 2, dtype=torch.long, device=x.device) + + # Feature-based strategies require normalized class tokens + # Extract and normalize class tokens (first token of each view) + img_class_feat = x[:, :, 0] / x[:, :, 0].norm(dim=-1, keepdim=True) # B S C + + if strategy == "saddle_balanced": + # Select view with balanced features across multiple metrics + # Compute similarity matrix + sim = torch.matmul(img_class_feat, img_class_feat.transpose(1, 2)) # B S S + sim_no_diag = sim - torch.eye(S, device=sim.device).unsqueeze(0) + sim_score = sim_no_diag.sum(dim=-1) / (S - 1) # B S + + feat_norm = x[:, :, 0].norm(dim=-1) # B S + feat_var = img_class_feat.var(dim=-1) # B S + + # Normalize all metrics to [0, 1] + def normalize_metric(metric): + min_val = metric.min(dim=1, keepdim=True).values + max_val = metric.max(dim=1, keepdim=True).values + return (metric - min_val) / (max_val - min_val + 1e-8) + + sim_score_norm = normalize_metric(sim_score) + norm_norm = normalize_metric(feat_norm) + var_norm = normalize_metric(feat_var) + + # Select view closest to the median (0.5) across all metrics + balance_score = ( + (sim_score_norm - 0.5).abs() + + (norm_norm - 0.5).abs() + + (var_norm - 0.5).abs() + ) + b_idx = balance_score.argmin(dim=1) + + elif strategy == "saddle_sim_range": + # Select view with largest similarity range (max - min) + sim = torch.matmul(img_class_feat, img_class_feat.transpose(1, 2)) # B S S + sim_no_diag = sim - torch.eye(S, device=sim.device).unsqueeze(0) + + sim_max = sim_no_diag.max(dim=-1).values # B S + sim_min = sim_no_diag.min(dim=-1).values # B S + sim_range = sim_max - sim_min + b_idx = sim_range.argmax(dim=1) + + else: + raise ValueError( + f"Unknown reference view selection strategy: {strategy}. " + f"Must be one of: 'first', 'middle', 'saddle_balanced', 'saddle_sim_range'" + ) + + return b_idx + + +def reorder_by_reference( + x: torch.Tensor, + b_idx: torch.Tensor, +) -> torch.Tensor: + """ + Reorder views to place the selected reference view first. + + Args: + x: Input tensor of shape (B, S, N, C) + b_idx: Reference view indices of shape (B,) + + Returns: + Reordered tensor with reference view at position 0 + + Example: + If b_idx = [2] and S = 5 (views [0,1,2,3,4]), + result order is [2,0,1,3,4] (ref_idx first, then others in order) + """ + B, S = x.shape[0], x.shape[1] + + # For single view, no reordering needed + if S <= 1: + return x + + # Create position indices: (B, S) where each row is [0, 1, 2, ..., S-1] + positions = torch.arange(S, device=x.device).unsqueeze(0).expand(B, -1) # B S + + # For each position, determine which original index it should take + # Position 0 gets ref_idx + # Position 1 to ref_idx gets indices 0 to ref_idx-1 + # Position ref_idx+1 to S-1 gets indices ref_idx+1 to S-1 + + b_idx_expanded = b_idx.unsqueeze(1) # B 1 + + # Create the reordering indices + # For positions 1 to ref_idx: map to indices 0 to ref_idx-1 (shift by -1) + # For positions > ref_idx: keep the same + reorder_indices = positions.clone() + reorder_indices = torch.where( + (positions > 0) & (positions <= b_idx_expanded), + positions - 1, + positions + ) + # Set position 0 to ref_idx + reorder_indices[:, 0] = b_idx + + # Gather using advanced indexing + batch_indices = torch.arange(B, device=x.device).unsqueeze(1) # B 1 + x_reordered = x[batch_indices, reorder_indices] + + return x_reordered + + +def restore_original_order( + x: torch.Tensor, + b_idx: torch.Tensor, +) -> torch.Tensor: + """ + Restore original view order after processing. + + Args: + x: Reordered tensor of shape (B, S, ...) + b_idx: Original reference view indices of shape (B,) + + Returns: + Tensor with original view order restored + + Example: + If original order was [0, 1, 2, 3, 4] and b_idx=2, + reordered becomes [2, 0, 1, 3, 4] (reference at position 0), + restore should return [0, 1, 2, 3, 4] (original order). + """ + B, S = x.shape[0], x.shape[1] + + # For single view, no restoration needed + if S <= 1: + return x + + # Create target position indices: (B, S) where each row is [0, 1, 2, ..., S-1] + target_positions = torch.arange(S, device=x.device).unsqueeze(0).expand(B, -1) # B S + + # For each target position, determine which current position it comes from + # Target position 0 to ref_idx-1 <- Current position 1 to ref_idx (shift by +1) + # Target position ref_idx <- Current position 0 + # Target position ref_idx+1 to S-1 <- Current position ref_idx+1 to S-1 (no change) + + b_idx_expanded = b_idx.unsqueeze(1) # B 1 + + # Create the restore indices + restore_indices = torch.where( + target_positions < b_idx_expanded, + target_positions + 1, # Positions before ref_idx come from current position + 1 + target_positions # Positions after ref_idx stay the same + ) + # Target position = ref_idx comes from current position 0 + # Use scatter to set specific positions + restore_indices = torch.scatter( + restore_indices, + dim=1, + index=b_idx_expanded, + src=torch.zeros_like(b_idx_expanded) + ) + + # Gather using advanced indexing + batch_indices = torch.arange(B, device=x.device).unsqueeze(1) # B 1 + x_restored = x[batch_indices, restore_indices] + + return x_restored + diff --git a/src/depth_anything_3/model/utils/attention.py b/src/depth_anything_3/model/utils/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..7e5f497bb332a4ef3e223796e8c2b94adc5abfce --- /dev/null +++ b/src/depth_anything_3/model/utils/attention.py @@ -0,0 +1,110 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110 # noqa + +from typing import Callable, Optional, Union + +import torch +import torch.nn.functional as F +from torch import Tensor, nn + + +class Attention(nn.Module): + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = True, + proj_bias: bool = True, + attn_drop: float = 0.0, + proj_drop: float = 0.0, + norm_layer: nn.Module = nn.LayerNorm, + qk_norm: bool = False, + rope=None, + ) -> None: + super().__init__() + assert dim % num_heads == 0, "dim should be divisible by num_heads" + self.num_heads = num_heads + self.head_dim = dim // num_heads + self.scale = self.head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity() + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim, bias=proj_bias) + self.proj_drop = nn.Dropout(proj_drop) + self.rope = rope + + def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor: + # Debug breakpoint removed for production + B, N, C = x.shape + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) + q, k, v = qkv.unbind(0) + q, k = self.q_norm(q), self.k_norm(k) + q = self.rope(q, pos) if self.rope is not None else q + k = self.rope(k, pos) if self.rope is not None else k + x = F.scaled_dot_product_attention( + q, + k, + v, + dropout_p=self.attn_drop.p if self.training else 0.0, + attn_mask=attn_mask, + ) + x = x.transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class LayerScale(nn.Module): + def __init__( + self, + dim: int, + init_values: Union[float, Tensor] = 1e-5, + inplace: bool = False, + ) -> None: + super().__init__() + self.inplace = inplace + self.gamma = nn.Parameter(init_values * torch.ones(dim)) + + def forward(self, x: Tensor) -> Tensor: + return x.mul_(self.gamma) if self.inplace else x * self.gamma + + +class Mlp(nn.Module): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = nn.GELU, + drop: float = 0.0, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias) + self.drop = nn.Dropout(drop) + + def forward(self, x: Tensor) -> Tensor: + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x diff --git a/src/depth_anything_3/model/utils/block.py b/src/depth_anything_3/model/utils/block.py new file mode 100644 index 0000000000000000000000000000000000000000..03270ccb15617ea8f7187f49bb48413409308865 --- /dev/null +++ b/src/depth_anything_3/model/utils/block.py @@ -0,0 +1,82 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Callable + +from torch import Tensor, nn + +from .attention import Attention, LayerScale, Mlp + + +class Block(nn.Module): + def __init__( + self, + dim: int, + num_heads: int, + mlp_ratio: float = 4.0, + qkv_bias: bool = True, + proj_bias: bool = True, + ffn_bias: bool = True, + drop: float = 0.0, + attn_drop: float = 0.0, + init_values=None, + drop_path: float = 0.0, + act_layer: Callable[..., nn.Module] = nn.GELU, + norm_layer: Callable[..., nn.Module] = nn.LayerNorm, + attn_class: Callable[..., nn.Module] = Attention, + ffn_layer: Callable[..., nn.Module] = Mlp, + qk_norm: bool = False, + rope=None, + ) -> None: + super().__init__() + + self.norm1 = norm_layer(dim) + + self.attn = attn_class( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + proj_bias=proj_bias, + attn_drop=attn_drop, + proj_drop=drop, + qk_norm=qk_norm, + rope=rope, + ) + + self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = ffn_layer( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + bias=ffn_bias, + ) + self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + + self.sample_drop_ratio = 0.0 # Equivalent to always having drop_path=0 + + def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor: + def attn_residual_func(x: Tensor, pos=None, attn_mask=None) -> Tensor: + return self.ls1(self.attn(self.norm1(x), pos=pos, attn_mask=attn_mask)) + + def ffn_residual_func(x: Tensor) -> Tensor: + return self.ls2(self.mlp(self.norm2(x))) + + # drop_path is always 0, so always take the else branch + x = x + attn_residual_func(x, pos=pos, attn_mask=attn_mask) + x = x + ffn_residual_func(x) + return x diff --git a/src/depth_anything_3/model/utils/gs_renderer.py b/src/depth_anything_3/model/utils/gs_renderer.py new file mode 100644 index 0000000000000000000000000000000000000000..f91360d9b7079c44698e39762bb1aba3e6a55346 --- /dev/null +++ b/src/depth_anything_3/model/utils/gs_renderer.py @@ -0,0 +1,341 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from math import isqrt +from typing import Literal, Optional + +import torch +from einops import rearrange, repeat +from tqdm import tqdm + +from depth_anything_3.specs import Gaussians +from depth_anything_3.utils.camera_trj_helpers import ( + interpolate_extrinsics, + interpolate_intrinsics, + render_dolly_zoom_path, + render_stabilization_path, + render_wander_path, + render_wobble_inter_path, +) +from depth_anything_3.utils.geometry import affine_inverse, as_homogeneous, get_fov +from depth_anything_3.utils.logger import logger + +try: + from gsplat import rasterization +except ImportError: + logger.warn( + "Dependency `gsplat` is required for rendering 3DGS. " + "Install via: pip install git+https://github.com/nerfstudio-project/" + "gsplat.git@0b4dddf04cb687367602c01196913cde6a743d70" + ) + + +def render_3dgs( + extrinsics: torch.Tensor, # "batch_views 4 4", w2c + intrinsics: torch.Tensor, # "batch_views 3 3", normalized + image_shape: tuple[int, int], + gaussian: Gaussians, + background_color: Optional[torch.Tensor] = None, # "batch_views 3" + use_sh: bool = True, + num_view: int = 1, + color_mode: Literal["RGB+D", "RGB+ED"] = "RGB+D", + **kwargs, +) -> tuple[ + torch.Tensor, # "batch_views 3 height width" + torch.Tensor, # "batch_views height width" +]: + # extract gaussian params + gaussian_means = gaussian.means + gaussian_scales = gaussian.scales + gaussian_quats = gaussian.rotations + gaussian_opacities = gaussian.opacities + gaussian_sh_coefficients = gaussian.harmonics + b, _, _ = extrinsics.shape + + if background_color is None: + background_color = repeat(torch.tensor([0.0, 0.0, 0.0]), "c -> b c", b=b).to( + gaussian_sh_coefficients + ) + + if use_sh: + _, _, _, n = gaussian_sh_coefficients.shape + degree = isqrt(n) - 1 + shs = rearrange(gaussian_sh_coefficients, "b g xyz n -> b g n xyz").contiguous() + else: # use color + shs = ( + gaussian_sh_coefficients.squeeze(-1).sigmoid().contiguous() + ) # (b, g, c), normed to (0, 1) + + h, w = image_shape + + fov_x, fov_y = get_fov(intrinsics).unbind(dim=-1) + tan_fov_x = (0.5 * fov_x).tan() + tan_fov_y = (0.5 * fov_y).tan() + focal_length_x = w / (2 * tan_fov_x) + focal_length_y = h / (2 * tan_fov_y) + + view_matrix = extrinsics.float() + + all_images = [] + all_radii = [] + all_depths = [] + # render view in a batch based, each batch contains one scene + # assume the Gaussian parameters are originally repeated along the view dim + batch_scene = b // num_view + + def index_i_gs_attr(full_attr, idx): + # return rearrange(full_attr, "(b v) ... -> b v ...", v=num_view)[idx, 0] + return full_attr[idx] + + for i in range(batch_scene): + K = repeat( + torch.tensor( + [ + [0, 0, w / 2.0], + [0, 0, h / 2.0], + [0, 0, 1], + ] + ), + "i j -> v i j", + v=num_view, + ).to(gaussian_means) + K[:, 0, 0] = focal_length_x.reshape(batch_scene, num_view)[i] + K[:, 1, 1] = focal_length_y.reshape(batch_scene, num_view)[i] + + i_means = index_i_gs_attr(gaussian_means, i) # [N, 3] + i_scales = index_i_gs_attr(gaussian_scales, i) + i_quats = index_i_gs_attr(gaussian_quats, i) + i_opacities = index_i_gs_attr(gaussian_opacities, i) # [N,] + i_colors = index_i_gs_attr(shs, i) # [N, K, 3] + i_viewmats = rearrange(view_matrix, "(b v) ... -> b v ...", v=num_view)[i] # [v, 4, 4] + i_backgrounds = rearrange(background_color, "(b v) ... -> b v ...", v=num_view)[ + i + ] # [v, 3] + + render_colors, render_alphas, info = rasterization( + means=i_means, + quats=i_quats, # [N, 4] + scales=i_scales, # [N, 3] + opacities=i_opacities, + colors=i_colors, + viewmats=i_viewmats, # [v, 4, 4] + Ks=K, # [v, 3, 3] + backgrounds=i_backgrounds, + render_mode=color_mode, + width=w, + height=h, + packed=False, + sh_degree=degree if use_sh else None, + ) + depth = render_colors[..., -1].unbind(dim=0) + + image = rearrange(render_colors[..., :3], "v h w c -> v c h w").unbind(dim=0) + radii = info["radii"].unbind(dim=0) + try: + info["means2d"].retain_grad() # [1, N, 2] + except Exception: + pass + all_images.extend(image) + all_depths.extend(depth) + all_radii.extend(radii) + + return torch.stack(all_images), torch.stack(all_depths) + + +def run_renderer_in_chunk_w_trj_mode( + gaussians: Gaussians, + extrinsics: torch.Tensor, # world2cam, "batch view 4 4" | "batch view 3 4" + intrinsics: torch.Tensor, # unnormed intrinsics, "batch view 3 3" + image_shape: tuple[int, int], + chunk_size: Optional[int] = 8, + trj_mode: Literal[ + "original", + "smooth", + "interpolate", + "interpolate_smooth", + "wander", + "dolly_zoom", + "extend", + "wobble_inter", + ] = "smooth", + input_shape: Optional[tuple[int, int]] = None, + enable_tqdm: Optional[bool] = False, + **kwargs, +) -> tuple[ + torch.Tensor, # color, "batch view 3 height width" + torch.Tensor, # depth, "batch view height width" +]: + cam2world = affine_inverse(as_homogeneous(extrinsics)) + if input_shape is not None: + in_h, in_w = input_shape + else: + in_h, in_w = image_shape + intr_normed = intrinsics.clone().detach() + intr_normed[..., 0, :] /= in_w + intr_normed[..., 1, :] /= in_h + if extrinsics.shape[1] <= 1: + assert trj_mode in [ + "wander", + "dolly_zoom", + ], "Please set trj_mode to 'wander' or 'dolly_zoom' when n_views=1" + + def _smooth_trj_fn_batch(raw_c2ws, k_size=50): + try: + smooth_c2ws = torch.stack( + [render_stabilization_path(c2w_i, k_size) for c2w_i in raw_c2ws], + dim=0, + ) + except Exception as e: + print(f"[DEBUG] Path smoothing failed with error: {e}.") + smooth_c2ws = raw_c2ws + return smooth_c2ws + + # get rendered trj + if trj_mode == "original": + tgt_c2w = cam2world + tgt_intr = intr_normed + elif trj_mode == "smooth": + tgt_c2w = _smooth_trj_fn_batch(cam2world) + tgt_intr = intr_normed + elif trj_mode in ["interpolate", "interpolate_smooth", "extend"]: + inter_len = 8 + total_len = (cam2world.shape[1] - 1) * inter_len + if total_len > 24 * 18: # no more than 18s + inter_len = max(1, 24 * 10 // (cam2world.shape[1] - 1)) + if total_len < 24 * 2: # no less than 2s + inter_len = max(1, 24 * 2 // (cam2world.shape[1] - 1)) + + if inter_len > 2: + t = torch.linspace(0, 1, inter_len, dtype=torch.float32, device=cam2world.device) + t = (torch.cos(torch.pi * (t + 1)) + 1) / 2 + tgt_c2w_b = [] + tgt_intr_b = [] + for b_idx in range(cam2world.shape[0]): + tgt_c2w = [] + tgt_intr = [] + for cur_idx in range(cam2world.shape[1] - 1): + tgt_c2w.append( + interpolate_extrinsics( + cam2world[b_idx, cur_idx], cam2world[b_idx, cur_idx + 1], t + )[(0 if cur_idx == 0 else 1) :] + ) + tgt_intr.append( + interpolate_intrinsics( + intr_normed[b_idx, cur_idx], intr_normed[b_idx, cur_idx + 1], t + )[(0 if cur_idx == 0 else 1) :] + ) + tgt_c2w_b.append(torch.cat(tgt_c2w)) + tgt_intr_b.append(torch.cat(tgt_intr)) + tgt_c2w = torch.stack(tgt_c2w_b) # b v 4 4 + tgt_intr = torch.stack(tgt_intr_b) # b v 3 3 + else: + tgt_c2w = cam2world + tgt_intr = intr_normed + if trj_mode in ["interpolate_smooth", "extend"]: + tgt_c2w = _smooth_trj_fn_batch(tgt_c2w) + if trj_mode == "extend": + # apply dolly_zoom and wander in the middle frame + assert cam2world.shape[0] == 1, "extend only supports for batch_size=1 currently." + mid_idx = tgt_c2w.shape[1] // 2 + c2w_wd, intr_wd = render_wander_path( + tgt_c2w[0, mid_idx], + tgt_intr[0, mid_idx], + h=in_h, + w=in_w, + num_frames=max(36, min(60, mid_idx // 2)), + max_disp=24.0, + ) + c2w_dz, intr_dz = render_dolly_zoom_path( + tgt_c2w[0, mid_idx], + tgt_intr[0, mid_idx], + h=in_h, + w=in_w, + num_frames=max(36, min(60, mid_idx // 2)), + ) + tgt_c2w = torch.cat( + [ + tgt_c2w[:, :mid_idx], + c2w_wd.unsqueeze(0), + c2w_dz.unsqueeze(0), + tgt_c2w[:, mid_idx:], + ], + dim=1, + ) + tgt_intr = torch.cat( + [ + tgt_intr[:, :mid_idx], + intr_wd.unsqueeze(0), + intr_dz.unsqueeze(0), + tgt_intr[:, mid_idx:], + ], + dim=1, + ) + elif trj_mode in ["wander", "dolly_zoom"]: + if trj_mode == "wander": + render_fn = render_wander_path + extra_kwargs = {"max_disp": 24.0} + else: + render_fn = render_dolly_zoom_path + extra_kwargs = {"D_focus": 30.0, "max_disp": 2.0} + tgt_c2w = [] + tgt_intr = [] + for b_idx in range(cam2world.shape[0]): + c2w_i, intr_i = render_fn( + cam2world[b_idx, 0], intr_normed[b_idx, 0], h=in_h, w=in_w, **extra_kwargs + ) + tgt_c2w.append(c2w_i) + tgt_intr.append(intr_i) + tgt_c2w = torch.stack(tgt_c2w) + tgt_intr = torch.stack(tgt_intr) + elif trj_mode == "wobble_inter": + tgt_c2w, tgt_intr = render_wobble_inter_path( + cam2world=cam2world, + intr_normed=intr_normed, + inter_len=10, + n_skip=3, + ) + else: + raise Exception(f"trj mode [{trj_mode}] is not implemented.") + + _, v = tgt_c2w.shape[:2] + tgt_extr = affine_inverse(tgt_c2w) + if chunk_size is None: + chunk_size = v + chunk_size = min(v, chunk_size) + all_colors = [] + all_depths = [] + for chunk_idx in tqdm( + range(math.ceil(v / chunk_size)), + desc="Rendering novel views", + disable=(not enable_tqdm), + leave=False, + ): + s = int(chunk_idx * chunk_size) + e = int((chunk_idx + 1) * chunk_size) + cur_n_view = tgt_extr[:, s:e].shape[1] + color, depth = render_3dgs( + extrinsics=rearrange(tgt_extr[:, s:e], "b v ... -> (b v) ..."), # w2c + intrinsics=rearrange(tgt_intr[:, s:e], "b v ... -> (b v) ..."), # normed + image_shape=image_shape, + gaussian=gaussians, + num_view=cur_n_view, + **kwargs, + ) + all_colors.append(rearrange(color, "(b v) ... -> b v ...", v=cur_n_view)) + all_depths.append(rearrange(depth, "(b v) ... -> b v ...", v=cur_n_view)) + all_colors = torch.cat(all_colors, dim=1) + all_depths = torch.cat(all_depths, dim=1) + + return all_colors, all_depths diff --git a/src/depth_anything_3/model/utils/head_utils.py b/src/depth_anything_3/model/utils/head_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..0e28f023c4028b4fd58ae6c39e68d13173c7b5fe --- /dev/null +++ b/src/depth_anything_3/model/utils/head_utils.py @@ -0,0 +1,231 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Tuple, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F + +# ----------------------------------------------------------------------------- +# Activation functions +# ----------------------------------------------------------------------------- + + +def activate_head_gs(out, activation="norm_exp", conf_activation="expp1", conf_dim=None): + """ + Process network output to extract GS params and density values. + Density could be view-dependent as SH coefficient + + + Args: + out: Network output tensor (B, C, H, W) + activation: Activation type for 3D points + conf_activation: Activation type for confidence values + + Returns: + Tuple of (3D points tensor, confidence tensor) + """ + # Move channels from last dim to the 4th dimension => (B, H, W, C) + fmap = out.permute(0, 2, 3, 1) # B,H,W,C expected + + # Split into xyz (first C-1 channels) and confidence (last channel) + conf_dim = 1 if conf_dim is None else conf_dim + xyz = fmap[:, :, :, :-conf_dim] + conf = fmap[:, :, :, -1] if conf_dim == 1 else fmap[:, :, :, -conf_dim:] + + if activation == "norm_exp": + d = xyz.norm(dim=-1, keepdim=True).clamp(min=1e-8) + xyz_normed = xyz / d + pts3d = xyz_normed * torch.expm1(d) + elif activation == "norm": + pts3d = xyz / xyz.norm(dim=-1, keepdim=True) + elif activation == "exp": + pts3d = torch.exp(xyz) + elif activation == "relu": + pts3d = F.relu(xyz) + elif activation == "sigmoid": + pts3d = torch.sigmoid(xyz) + elif activation == "linear": + pts3d = xyz + else: + raise ValueError(f"Unknown activation: {activation}") + + if conf_activation == "expp1": + conf_out = 1 + conf.exp() + elif conf_activation == "expp0": + conf_out = conf.exp() + elif conf_activation == "sigmoid": + conf_out = torch.sigmoid(conf) + elif conf_activation == "linear": + conf_out = conf + else: + raise ValueError(f"Unknown conf_activation: {conf_activation}") + + return pts3d, conf_out + + +# ----------------------------------------------------------------------------- +# Other utilities +# ----------------------------------------------------------------------------- + + +class Permute(nn.Module): + """nn.Module wrapper around Tensor.permute for cleaner nn.Sequential usage.""" + + dims: Tuple[int, ...] + + def __init__(self, dims: Tuple[int, ...]) -> None: + super().__init__() + self.dims = dims + + def forward(self, x: torch.Tensor) -> torch.Tensor: # type: ignore[override] + return x.permute(*self.dims) + + +def position_grid_to_embed( + pos_grid: torch.Tensor, embed_dim: int, omega_0: float = 100 +) -> torch.Tensor: + """ + Convert 2D position grid (HxWx2) to sinusoidal embeddings (HxWxC) + + Args: + pos_grid: Tensor of shape (H, W, 2) containing 2D coordinates + embed_dim: Output channel dimension for embeddings + + Returns: + Tensor of shape (H, W, embed_dim) with positional embeddings + """ + H, W, grid_dim = pos_grid.shape + assert grid_dim == 2 + pos_flat = pos_grid.reshape(-1, grid_dim) # Flatten to (H*W, 2) + + # Process x and y coordinates separately + emb_x = make_sincos_pos_embed(embed_dim // 2, pos_flat[:, 0], omega_0=omega_0) # [1, H*W, D/2] + emb_y = make_sincos_pos_embed(embed_dim // 2, pos_flat[:, 1], omega_0=omega_0) # [1, H*W, D/2] + + # Combine and reshape + emb = torch.cat([emb_x, emb_y], dim=-1) # [1, H*W, D] + + return emb.view(H, W, embed_dim) # [H, W, D] + + +def make_sincos_pos_embed(embed_dim: int, pos: torch.Tensor, omega_0: float = 100) -> torch.Tensor: + """ + This function generates a 1D positional embedding from a given grid using sine and cosine functions. # noqa + + Args: + - embed_dim: The embedding dimension. + - pos: The position to generate the embedding from. + + Returns: + - emb: The generated 1D positional embedding. + """ + assert embed_dim % 2 == 0 + omega = torch.arange(embed_dim // 2, dtype=torch.float32, device=pos.device) + omega /= embed_dim / 2.0 + omega = 1.0 / omega_0**omega # (D/2,) + + pos = pos.reshape(-1) # (M,) + out = torch.einsum("m,d->md", pos, omega) # (M, D/2), outer product + + emb_sin = torch.sin(out) # (M, D/2) + emb_cos = torch.cos(out) # (M, D/2) + + emb = torch.cat([emb_sin, emb_cos], dim=1) # (M, D) + return emb.float() + + +# Inspired by https://github.com/microsoft/moge + + +def create_uv_grid( + width: int, + height: int, + aspect_ratio: float = None, + dtype: torch.dtype = None, + device: torch.device = None, +) -> torch.Tensor: + """ + Create a normalized UV grid of shape (width, height, 2). + + The grid spans horizontally and vertically according to an aspect ratio, + ensuring the top-left corner is at (-x_span, -y_span) and the bottom-right + corner is at (x_span, y_span), normalized by the diagonal of the plane. + + Args: + width (int): Number of points horizontally. + height (int): Number of points vertically. + aspect_ratio (float, optional): Width-to-height ratio. Defaults to width/height. + dtype (torch.dtype, optional): Data type of the resulting tensor. + device (torch.device, optional): Device on which the tensor is created. + + Returns: + torch.Tensor: A (width, height, 2) tensor of UV coordinates. + """ + # Derive aspect ratio if not explicitly provided + if aspect_ratio is None: + aspect_ratio = float(width) / float(height) + + # Compute normalized spans for X and Y + diag_factor = (aspect_ratio**2 + 1.0) ** 0.5 + span_x = aspect_ratio / diag_factor + span_y = 1.0 / diag_factor + + # Establish the linspace boundaries + left_x = -span_x * (width - 1) / width + right_x = span_x * (width - 1) / width + top_y = -span_y * (height - 1) / height + bottom_y = span_y * (height - 1) / height + + # Generate 1D coordinates + x_coords = torch.linspace(left_x, right_x, steps=width, dtype=dtype, device=device) + y_coords = torch.linspace(top_y, bottom_y, steps=height, dtype=dtype, device=device) + + # Create 2D meshgrid (width x height) and stack into UV + uu, vv = torch.meshgrid(x_coords, y_coords, indexing="xy") + uv_grid = torch.stack((uu, vv), dim=-1) + + return uv_grid + + +# ----------------------------------------------------------------------------- +# Interpolation (safe interpolation, avoid INT_MAX overflow) +# ----------------------------------------------------------------------------- +def custom_interpolate( + x: torch.Tensor, + size: Union[Tuple[int, int], None] = None, + scale_factor: Union[float, None] = None, + mode: str = "bilinear", + align_corners: bool = True, +) -> torch.Tensor: + """ + Safe interpolation implementation to avoid INT_MAX overflow in torch.nn.functional.interpolate. + """ + if size is None: + assert scale_factor is not None, "Either size or scale_factor must be provided." + size = (int(x.shape[-2] * scale_factor), int(x.shape[-1] * scale_factor)) + + INT_MAX = 1610612736 + total = size[0] * size[1] * x.shape[0] * x.shape[1] + + if total > INT_MAX: + chunks = torch.chunk(x, chunks=(total // INT_MAX) + 1, dim=0) + outs = [ + nn.functional.interpolate(c, size=size, mode=mode, align_corners=align_corners) + for c in chunks + ] + return torch.cat(outs, dim=0).contiguous() + + return nn.functional.interpolate(x, size=size, mode=mode, align_corners=align_corners) diff --git a/src/depth_anything_3/model/utils/transform.py b/src/depth_anything_3/model/utils/transform.py new file mode 100644 index 0000000000000000000000000000000000000000..8d732b093e5ad1578bc0ba5eb0e31b1b69b766ed --- /dev/null +++ b/src/depth_anything_3/model/utils/transform.py @@ -0,0 +1,208 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn.functional as F + + +def extri_intri_to_pose_encoding( + extrinsics, + intrinsics, + image_size_hw=None, +): + """Convert camera extrinsics and intrinsics to a compact pose encoding.""" + + # extrinsics: BxSx3x4 + # intrinsics: BxSx3x3 + R = extrinsics[:, :, :3, :3] # BxSx3x3 + T = extrinsics[:, :, :3, 3] # BxSx3 + + quat = mat_to_quat(R) + # Note the order of h and w here + H, W = image_size_hw + fov_h = 2 * torch.atan((H / 2) / intrinsics[..., 1, 1]) + fov_w = 2 * torch.atan((W / 2) / intrinsics[..., 0, 0]) + pose_encoding = torch.cat([T, quat, fov_h[..., None], fov_w[..., None]], dim=-1).float() + + return pose_encoding + + +def pose_encoding_to_extri_intri( + pose_encoding, + image_size_hw=None, +): + """Convert a pose encoding back to camera extrinsics and intrinsics.""" + + T = pose_encoding[..., :3] + quat = pose_encoding[..., 3:7] + fov_h = pose_encoding[..., 7] + fov_w = pose_encoding[..., 8] + + R = quat_to_mat(quat) + extrinsics = torch.cat([R, T[..., None]], dim=-1) + + H, W = image_size_hw + fy = (H / 2.0) / torch.clamp(torch.tan(fov_h / 2.0), 1e-6) + fx = (W / 2.0) / torch.clamp(torch.tan(fov_w / 2.0), 1e-6) + intrinsics = torch.zeros(pose_encoding.shape[:2] + (3, 3), device=pose_encoding.device) + intrinsics[..., 0, 0] = fx + intrinsics[..., 1, 1] = fy + intrinsics[..., 0, 2] = W / 2 + intrinsics[..., 1, 2] = H / 2 + intrinsics[..., 2, 2] = 1.0 # Set the homogeneous coordinate to 1 + + return extrinsics, intrinsics + + +def quat_to_mat(quaternions: torch.Tensor) -> torch.Tensor: + """ + Quaternion Order: XYZW or say ijkr, scalar-last + + Convert rotations given as quaternions to rotation matrices. + Args: + quaternions: quaternions with real part last, + as tensor of shape (..., 4). + + Returns: + Rotation matrices as tensor of shape (..., 3, 3). + """ + i, j, k, r = torch.unbind(quaternions, -1) + two_s = 2.0 / (quaternions * quaternions).sum(-1) + + o = torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + -1, + ) + return o.reshape(quaternions.shape[:-1] + (3, 3)) + + +def mat_to_quat(matrix: torch.Tensor) -> torch.Tensor: + """ + Convert rotations given as rotation matrices to quaternions. + + Args: + matrix: Rotation matrices as tensor of shape (..., 3, 3). + + Returns: + quaternions with real part last, as tensor of shape (..., 4). + Quaternion Order: XYZW or say ijkr, scalar-last + """ + if matrix.size(-1) != 3 or matrix.size(-2) != 3: + raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.") + + batch_dim = matrix.shape[:-2] + m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind( + matrix.reshape(batch_dim + (9,)), dim=-1 + ) + + q_abs = _sqrt_positive_part( + torch.stack( + [ + 1.0 + m00 + m11 + m22, + 1.0 + m00 - m11 - m22, + 1.0 - m00 + m11 - m22, + 1.0 - m00 - m11 + m22, + ], + dim=-1, + ) + ) + + quat_by_rijk = torch.stack( + [ + torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1), + torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1), + torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1), + torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1), + ], + dim=-2, + ) + + flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device) + quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr)) + + out = quat_candidates[F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :].reshape( + batch_dim + (4,) + ) + + out = out[..., [1, 2, 3, 0]] + + out = standardize_quaternion(out) + + return out + + +def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor: + """ + Returns torch.sqrt(torch.max(0, x)) + but with a zero subgradient where x is 0. + """ + ret = torch.zeros_like(x) + positive_mask = x > 0 + if torch.is_grad_enabled(): + ret[positive_mask] = torch.sqrt(x[positive_mask]) + else: + ret = torch.where(positive_mask, torch.sqrt(x), ret) + return ret + + +def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor: + """ + Convert a unit quaternion to a standard form: one in which the real + part is non negative. + + Args: + quaternions: Quaternions with real part last, + as tensor of shape (..., 4). + + Returns: + Standardized quaternions as tensor of shape (..., 4). + """ + return torch.where(quaternions[..., 3:4] < 0, -quaternions, quaternions) + + +def cam_quat_xyzw_to_world_quat_wxyz(cam_quat_xyzw, c2w): + # cam_quat_xyzw: (b, n, 4) in xyzw + # c2w: (b, n, 4, 4) + b, n = cam_quat_xyzw.shape[:2] + # 1. xyzw -> wxyz + cam_quat_wxyz = torch.cat( + [ + cam_quat_xyzw[..., 3:4], # w + cam_quat_xyzw[..., 0:1], # x + cam_quat_xyzw[..., 1:2], # y + cam_quat_xyzw[..., 2:3], # z + ], + dim=-1, + ) + # 2. Quaternion to matrix + cam_quat_wxyz_flat = cam_quat_wxyz.reshape(-1, 4) + rotmat_cam = quat_to_mat(cam_quat_wxyz_flat).reshape(b, n, 3, 3) + # 3. Transform to world space + rotmat_c2w = c2w[..., :3, :3] + rotmat_world = torch.matmul(rotmat_c2w, rotmat_cam) + # 4. Matrix to quaternion (wxyz) + rotmat_world_flat = rotmat_world.reshape(-1, 3, 3) + world_quat_wxyz_flat = mat_to_quat(rotmat_world_flat) + world_quat_wxyz = world_quat_wxyz_flat.reshape(b, n, 4) + return world_quat_wxyz diff --git a/src/depth_anything_3/registry.py b/src/depth_anything_3/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..96450717d696b395503fedfe93af812e975d2671 --- /dev/null +++ b/src/depth_anything_3/registry.py @@ -0,0 +1,50 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict +from pathlib import Path + + +def get_all_models() -> OrderedDict: + """ + Scans all YAML files in the configs directory and returns a sorted dictionary where: + - Keys are model names (YAML filenames without the .yaml extension) + - Values are absolute paths to the corresponding YAML files + """ + # Get path to the configs directory within the da3 package + # Works both in development and after pip installation + # configs_dir = files("depth_anything_3").joinpath("configs") + configs_dir = Path(__file__).resolve().parent / "configs" + + # Ensure path is a Path object for consistent cross-platform handling + configs_dir = Path(configs_dir) + + model_entries = [] + # Iterate through all items in the configs directory + for item in configs_dir.iterdir(): + # Filter for YAML files (excluding directories) + if item.is_file() and item.suffix == ".yaml": + # Extract model name (filename without .yaml extension) + model_name = item.stem + # Get absolute path (resolve() handles symlinks) + file_abs_path = str(item.resolve()) + model_entries.append((model_name, file_abs_path)) + + # Sort entries by model name and convert to OrderedDict + sorted_entries = sorted(model_entries, key=lambda x: x[0]) + return OrderedDict(sorted_entries) + + +# Global registry for external imports +MODEL_REGISTRY = get_all_models() diff --git a/src/depth_anything_3/services/__init__.py b/src/depth_anything_3/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..07ed8b6b96d68a3f3933dfa6651875f3f88b0aef --- /dev/null +++ b/src/depth_anything_3/services/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Services module for Depth Anything 3. +""" + +from depth_anything_3.services.backend import create_app, start_server + +__all__ = [ + start_server, + create_app, +] diff --git a/src/depth_anything_3/services/backend.py b/src/depth_anything_3/services/backend.py new file mode 100644 index 0000000000000000000000000000000000000000..ef120011ca609026a242b8373e89dabf83b26552 --- /dev/null +++ b/src/depth_anything_3/services/backend.py @@ -0,0 +1,1416 @@ +# flake8: noqa: E501 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Model backend service for Depth Anything 3. +Provides HTTP API for model inference with persistent model loading. +""" + +import os +import posixpath +import time +import uuid +from concurrent.futures import ThreadPoolExecutor +from typing import Any, Dict, List, Optional +from urllib.parse import quote + +import numpy as np +import uvicorn +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse, HTMLResponse +from pydantic import BaseModel + +from ..api import DepthAnything3 +from ..utils.memory import ( + check_memory_availability, + cleanup_cuda_memory, + estimate_memory_requirement, + get_gpu_memory_info, +) + + +class InferenceRequest(BaseModel): + """Request model for inference API.""" + + image_paths: List[str] + export_dir: Optional[str] = None + export_format: str = "mini_npz-glb" + extrinsics: Optional[List[List[List[float]]]] = None + intrinsics: Optional[List[List[List[float]]]] = None + process_res: int = 504 + process_res_method: str = "upper_bound_resize" + export_feat_layers: List[int] = [] + align_to_input_ext_scale: bool = True + # GLB export parameters + conf_thresh_percentile: float = 40.0 + num_max_points: int = 1_000_000 + show_cameras: bool = True + # Feat_vis export parameters + feat_vis_fps: int = 15 + + +class InferenceResponse(BaseModel): + """Response model for inference API.""" + + success: bool + message: str + task_id: Optional[str] = None + export_dir: Optional[str] = None + export_format: str = "mini_npz-glb" + processing_time: Optional[float] = None + + +class TaskStatus(BaseModel): + """Task status model.""" + + task_id: str + status: str # "pending", "running", "completed", "failed" + message: str + progress: Optional[float] = None # 0.0 to 1.0 + created_at: float + started_at: Optional[float] = None + completed_at: Optional[float] = None + export_dir: Optional[str] = None + request: Optional[InferenceRequest] = None # Store the original request + + # Essential task parameters + num_images: Optional[int] = None # Number of input images + export_format: Optional[str] = None # Export format + process_res_method: Optional[str] = None # Processing resolution method + video_path: Optional[str] = None # Source video path + + +class ModelBackend: + """Model backend service with persistent model loading.""" + + def __init__(self, model_dir: str, device: str = "cuda"): + self.model_dir = model_dir + self.device = device + self.model = None + self.model_loaded = False + self.load_time = None + self.load_start_time = None # Time when model loading started + self.load_completed_time = None # Time when model loading completed + self.last_used = None + + def load_model(self): + """Load model if not already loaded.""" + if self.model_loaded and self.model is not None: + self.last_used = time.time() + return self.model + + try: + print(f"Loading model from {self.model_dir}...") + self.load_start_time = time.time() + start_time = time.time() + + self.model = DepthAnything3.from_pretrained(self.model_dir).to(self.device) + self.model.eval() + + self.model_loaded = True + self.load_time = time.time() - start_time + self.load_completed_time = time.time() + self.last_used = time.time() + + print(f"Model loaded successfully in {self.load_time:.2f}s") + return self.model + + except Exception as e: + print(f"Failed to load model: {e}") + raise e + + def get_model(self): + """Get model, loading if necessary.""" + if not self.model_loaded: + return self.load_model() + self.last_used = time.time() + return self.model + + def get_status(self) -> Dict[str, Any]: + """Get backend status information.""" + # Calculate uptime from when model loading completed + uptime = 0 + if self.model_loaded and self.load_completed_time: + uptime = time.time() - self.load_completed_time + + return { + "model_loaded": self.model_loaded, + "model_dir": self.model_dir, + "device": self.device, + "load_time": self.load_time, + "last_used": self.last_used, + "uptime": uptime, + } + + +# Global backend instance +_backend: Optional[ModelBackend] = None +_app: Optional[FastAPI] = None +_tasks: Dict[str, TaskStatus] = {} +_executor = ThreadPoolExecutor(max_workers=1) # Restrict to single-task execution +_running_task_id: Optional[str] = None # Currently running task ID +_task_queue: List[str] = [] # Pending task queue + +# Task cleanup configuration +MAX_TASK_HISTORY = 100 # Maximum number of tasks to keep in memory +CLEANUP_INTERVAL = 300 # Cleanup interval in seconds (5 minutes) + + +def _process_next_task(): + """Process the next task in the queue.""" + global _task_queue, _running_task_id + + if not _task_queue or _running_task_id is not None: + return + + # Get next task from queue + task_id = _task_queue.pop(0) + + # Get task request from tasks dict (we need to store the request) + if task_id not in _tasks: + return + + # Submit task to executor + _executor.submit(_run_inference_task, task_id) + + +# get_gpu_memory_info imported from depth_anything_3.utils.memory + + +# cleanup_cuda_memory imported from depth_anything_3.utils.memory + + +# check_memory_availability imported from depth_anything_3.utils.memory + + +# estimate_memory_requirement imported from depth_anything_3.utils.memory + + +def _run_inference_task(task_id: str): + """Run inference task in background thread with OOM protection.""" + global _tasks, _backend, _running_task_id, _task_queue + + model = None + inference_started = False + start_time = time.time() + + try: + # Get task request + if task_id not in _tasks or _tasks[task_id].request is None: + print(f"[{task_id}] Task not found or request missing") + return + + request = _tasks[task_id].request + num_images = len(request.image_paths) + + # Set current running task + _running_task_id = task_id + + # Update task status to running + _tasks[task_id].status = "running" + _tasks[task_id].started_at = start_time + _tasks[task_id].message = f"[{task_id}] Starting inference on {num_images} frames..." + print(f"[{task_id}] Starting inference on {num_images} frames") + + # Pre-inference cleanup to ensure maximum available memory + print(f"[{task_id}] Pre-inference cleanup...") + cleanup_cuda_memory() + + # Check memory availability + estimated_memory = estimate_memory_requirement(num_images, request.process_res) + mem_available, mem_msg = check_memory_availability(estimated_memory) + print(f"[{task_id}] {mem_msg}") + + if not mem_available: + # Try aggressive cleanup + print(f"[{task_id}] Insufficient memory, attempting aggressive cleanup...") + cleanup_cuda_memory() + time.sleep(0.5) # Give system time to reclaim memory + + # Check again + mem_available, mem_msg = check_memory_availability(estimated_memory) + if not mem_available: + raise RuntimeError( + f"Insufficient GPU memory after cleanup. {mem_msg}\n" + f"Suggestions:\n" + f" 1. Reduce process_res (current: {request.process_res})\n" + f" 2. Process fewer images at once (current: {num_images})\n" + f" 3. Clear other GPU processes" + ) + + # Get model (with error handling) + print(f"[{task_id}] Loading model...") + _tasks[task_id].message = f"[{task_id}] Loading model..." + _tasks[task_id].progress = 0.1 + + try: + model = _backend.get_model() + except RuntimeError as e: + if "out of memory" in str(e).lower(): + cleanup_cuda_memory() + raise RuntimeError( + f"OOM during model loading: {str(e)}\n" + f"Try reducing the batch size or resolution." + ) + raise + + print(f"[{task_id}] Model loaded successfully") + _tasks[task_id].progress = 0.2 + + # Prepare inference parameters + inference_kwargs = { + "image": request.image_paths, + "export_format": request.export_format, + "process_res": request.process_res, + "process_res_method": request.process_res_method, + "export_feat_layers": request.export_feat_layers, + "align_to_input_ext_scale": request.align_to_input_ext_scale, + "conf_thresh_percentile": request.conf_thresh_percentile, + "num_max_points": request.num_max_points, + "show_cameras": request.show_cameras, + "feat_vis_fps": request.feat_vis_fps, + } + + if request.export_dir: + inference_kwargs["export_dir"] = request.export_dir + + if request.extrinsics: + inference_kwargs["extrinsics"] = np.array(request.extrinsics, dtype=np.float32) + + if request.intrinsics: + inference_kwargs["intrinsics"] = np.array(request.intrinsics, dtype=np.float32) + + # Run inference with timing + inference_start_time = time.time() + print(f"[{task_id}] Running model inference...") + _tasks[task_id].message = f"[{task_id}] Running model inference on {num_images} images..." + _tasks[task_id].progress = 0.3 + + inference_started = True + + try: + model.inference(**inference_kwargs) + inference_time = time.time() - inference_start_time + avg_time_per_image = inference_time / num_images if num_images > 0 else 0 + + print( + f"[{task_id}] Inference completed in {inference_time:.2f}s " + f"({avg_time_per_image:.2f}s per image)" + ) + + except RuntimeError as e: + if "out of memory" in str(e).lower(): + cleanup_cuda_memory() + raise RuntimeError( + f"OOM during inference: {str(e)}\n" + f"Settings: {num_images} images, resolution={request.process_res}\n" + f"Suggestions:\n" + f" 1. Reduce process_res to {int(request.process_res * 0.75)}\n" + f" 2. Process images in smaller batches\n" + f" 3. Use process_res_method='resize' instead of 'upper_bound_resize'" + ) + raise + + _tasks[task_id].progress = 0.9 + + # Post-inference cleanup + print(f"[{task_id}] Post-inference cleanup...") + cleanup_cuda_memory() + + # Calculate total processing time + total_time = time.time() - start_time + + # Update task status to completed + _tasks[task_id].status = "completed" + _tasks[task_id].completed_at = time.time() + _tasks[task_id].message = ( + f"[{task_id}] Completed in {total_time:.2f}s " f"({avg_time_per_image:.2f}s per image)" + ) + _tasks[task_id].progress = 1.0 + _tasks[task_id].export_dir = request.export_dir + + # Clear running state + _running_task_id = None + + # Process next task in queue + _process_next_task() + + print(f"[{task_id}] Task completed successfully") + print( + f"[{task_id}] Total time: {total_time:.2f}s, " + f"Inference time: {inference_time:.2f}s, " + f"Avg per image: {avg_time_per_image:.2f}s" + ) + + except Exception as e: + # Update task status to failed + error_msg = str(e) + total_time = time.time() - start_time + + print(f"[{task_id}] Task failed after {total_time:.2f}s: {error_msg}") + + # Always attempt cleanup on failure + cleanup_cuda_memory() + + _tasks[task_id].status = "failed" + _tasks[task_id].completed_at = time.time() + _tasks[task_id].message = f"[{task_id}] Failed after {total_time:.2f}s: {error_msg}" + + # Clear running state + _running_task_id = None + + # Process next task in queue + _process_next_task() + + finally: + # Final cleanup in finally block to ensure it always runs + # This is critical for releasing resources even if unexpected errors occur + try: + if inference_started: + print(f"[{task_id}] Final cleanup in finally block...") + cleanup_cuda_memory() + except Exception as e: + print(f"[{task_id}] Warning: Finally block cleanup failed: {e}") + + # Schedule cleanup after task completion + _schedule_task_cleanup() + + +def _cleanup_old_tasks(): + """Clean up old completed/failed tasks to prevent memory buildup.""" + global _tasks + + current_time = time.time() + tasks_to_remove = [] + + # Find tasks to remove - more aggressive cleanup + for task_id, task in _tasks.items(): + # Remove completed/failed tasks older than 10 minutes (instead of 1 hour) + if ( + task.status in ["completed", "failed"] + and task.completed_at + and current_time - task.completed_at > 600 + ): # 10 minutes + tasks_to_remove.append(task_id) + + # Remove old tasks + for task_id in tasks_to_remove: + del _tasks[task_id] + print(f"[CLEANUP] Removed old task: {task_id}") + + # If still too many tasks, remove oldest completed/failed tasks + if len(_tasks) > MAX_TASK_HISTORY: + completed_tasks = [ + (task_id, task) + for task_id, task in _tasks.items() + if task.status in ["completed", "failed"] + ] + completed_tasks.sort(key=lambda x: x[1].completed_at or 0) + + excess_count = len(_tasks) - MAX_TASK_HISTORY + for i in range(min(excess_count, len(completed_tasks))): + task_id = completed_tasks[i][0] + del _tasks[task_id] + print(f"[CLEANUP] Removed excess task: {task_id}") + + # Count active tasks (only pending and running) + active_count = sum(1 for task in _tasks.values() if task.status in ["pending", "running"]) + print( + "[CLEANUP] Task cleanup completed. " + f"Total tasks: {len(_tasks)}, Active tasks: {active_count}" + ) + + +def _schedule_task_cleanup(): + """Schedule task cleanup in background.""" + + def cleanup_worker(): + try: + time.sleep(2) # Small delay to ensure task status is updated + _cleanup_old_tasks() + except Exception as e: + print(f"[CLEANUP] Cleanup worker failed: {e}") + + # Run cleanup in background thread + _executor.submit(cleanup_worker) + + +# ============================================================================ +# Gallery utilities (extracted from gallery.py) +# ============================================================================ + +GALLERY_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp") + + +def _load_gallery_html() -> str: + """ + Load and modify gallery HTML to work under /gallery/ subdirectory. + Replaces API paths from root to /gallery/ prefix. + """ + from ..services.gallery import HTML_PAGE + + # Replace API paths to be under /gallery/ subdirectory + html = ( + HTML_PAGE.replace("fetch('/manifest.json'", "fetch('/gallery/manifest.json'") + .replace("fetch('/manifest/'+", "fetch('/gallery/manifest/'+") + .replace( + "if(location.pathname!=\"/\")history.replaceState(null,'','/'+location.search)", + "if(!location.pathname.startsWith(\"/gallery\"))history.replaceState(null,'','/gallery/'+location.search)", + ) + ) + + return html + + +def _gallery_url_join(*parts: str) -> str: + """Join URL parts safely.""" + norm = posixpath.join(*[p.replace("\\", "/") for p in parts]) + segs = [s for s in norm.split("/") if s not in ("", ".")] + return "/".join(quote(s) for s in segs) + + +def _is_plain_name(name: str) -> bool: + """Check if name is safe for use in paths.""" + return all(c not in name for c in ("/", "\\")) and name not in (".", "..") + + +def build_group_list(root_dir: str) -> dict: + """Build list of groups from gallery directory.""" + groups = [] + try: + for gname in sorted(os.listdir(root_dir)): + gpath = os.path.join(root_dir, gname) + if not os.path.isdir(gpath): + continue + has_scene = False + try: + for sname in os.listdir(gpath): + spath = os.path.join(gpath, sname) + if not os.path.isdir(spath): + continue + if os.path.exists(os.path.join(spath, "scene.glb")) and os.path.exists( + os.path.join(spath, "scene.jpg") + ): + has_scene = True + break + except Exception: + pass + if has_scene: + groups.append({"id": gname, "title": gname}) + except Exception as e: + print(f"[warn] build_group_list failed: {e}") + return {"groups": groups} + + +def build_group_manifest(root_dir: str, group: str) -> dict: + """Build manifest for a specific group.""" + items = [] + gpath = os.path.join(root_dir, group) + try: + if not os.path.isdir(gpath): + return {"group": group, "items": []} + for sname in sorted(os.listdir(gpath)): + spath = os.path.join(gpath, sname) + if not os.path.isdir(spath): + continue + glb_fs = os.path.join(spath, "scene.glb") + jpg_fs = os.path.join(spath, "scene.jpg") + if not (os.path.exists(glb_fs) and os.path.exists(jpg_fs)): + continue + depth_images = [] + dpath = os.path.join(spath, "depth_vis") + if os.path.isdir(dpath): + files = [ + f + for f in os.listdir(dpath) + if os.path.splitext(f)[1].lower() in GALLERY_IMAGE_EXTS + ] + for fn in sorted(files): + depth_images.append( + "/gallery/" + _gallery_url_join(group, sname, "depth_vis", fn) + ) + items.append( + { + "id": sname, + "title": sname, + "model": "/gallery/" + _gallery_url_join(group, sname, "scene.glb"), + "thumbnail": "/gallery/" + _gallery_url_join(group, sname, "scene.jpg"), + "depth_images": depth_images, + } + ) + except Exception as e: + print(f"[warn] build_group_manifest failed for {group}: {e}") + return {"group": group, "items": items} + + +def create_app(model_dir: str, device: str = "cuda", gallery_dir: Optional[str] = None) -> FastAPI: + """Create FastAPI application with model backend.""" + global _backend, _app + + _backend = ModelBackend(model_dir, device) + _app = FastAPI( + title="Depth Anything 3 Backend", + description="Model inference service for Depth Anything 3", + version="1.0.0", + ) + + # Store gallery directory globally for use in routes + _gallery_dir = gallery_dir + + @_app.get("/", response_class=HTMLResponse) + async def root(): + """Home page with navigation to dashboard and gallery.""" + html_content = ( + """ + + + + + + Depth Anything 3 Backend + + + +
+

Depth Anything 3

+

Model Backend Service

+ + +
+ + + """ + ) + return HTMLResponse(html_content) + + @_app.get("/dashboard", response_class=HTMLResponse) + async def dashboard(): + """HTML dashboard for monitoring backend status and tasks.""" + if _backend is None: + return HTMLResponse("

Backend not initialized

", status_code=500) + + # Get backend status + status = _backend.get_status() + + # Safely format status values + if status["load_time"] is not None: + load_time_str = f"{status['load_time']:.2f}s" + else: + load_time_str = "Not loaded" + + if status["uptime"] is not None: + uptime_str = f"{status['uptime']:.2f}s" + else: + uptime_str = "Not running" + + # Get tasks information + active_tasks = [task for task in _tasks.values() if task.status in ["pending", "running"]] + completed_tasks = [ + task for task in _tasks.values() if task.status in ["completed", "failed"] + ] + + # Generate task HTML + active_tasks_html = "" + if active_tasks: + for task in active_tasks: + task_details = f""" +
+
+ {task.task_id} + {task.status} +
+
{task.message}
+
+ + Images: {task.num_images or 'N/A'} | + Format: {task.export_format or 'N/A'} | + Method: {task.process_res_method or 'N/A'} | + Export Dir: {task.export_dir or 'N/A'} + + {f'
Video: {task.video_path}' if task.video_path else ''} +
+
+ """ + active_tasks_html += task_details + else: + active_tasks_html = "

No active tasks

" + + completed_tasks_html = "" + if completed_tasks: + for task in completed_tasks[-10:]: + task_details = f""" +
+
+ {task.task_id} + {task.status} +
+
{task.message}
+
+ + Images: {task.num_images or 'N/A'} | + Format: {task.export_format or 'N/A'} | + Method: {task.process_res_method or 'N/A'} | + Export Dir: {task.export_dir or 'N/A'} + + {f'
Video: {task.video_path}' if task.video_path else ''} +
+
+ """ + completed_tasks_html += task_details + else: + completed_tasks_html = "

No completed tasks

" + + # Generate HTML + html_content = f""" + + + + + + Depth Anything 3 Backend Dashboard + + + +
+
+

Depth Anything 3 Backend Dashboard

+

Real-time monitoring of model status and inference tasks

+
+ +
+
+

Model Status

+
+ Status: + + {'Online' if status['model_loaded'] else 'Offline'} + +
+
+ Model Directory: + {status['model_dir']} +
+
+ Device: + {status['device']} +
+
+ Load Time: + {load_time_str} +
+
+ Uptime: + {uptime_str} +
+
+ +
+

Task Summary

+
+ Active Tasks: + {len(active_tasks)} +
+
+ Completed Tasks: + {len(completed_tasks)} +
+
+ Total Tasks: + {len(_tasks)} +
+
+
+ +
+

Active Tasks

+ + +
Last updated: {time.strftime('%Y-%m-%d %H:%M:%S')}
+ + {active_tasks_html} +
+ +
+

Recent Completed Tasks

+ {completed_tasks_html} +
+
+ + + + + """ + + return HTMLResponse(html_content) + + @_app.get("/status") + async def get_status(): + """Get backend status with GPU memory information.""" + if _backend is None: + raise HTTPException(status_code=500, detail="Backend not initialized") + + status = _backend.get_status() + + # Add GPU memory information + gpu_memory = get_gpu_memory_info() + if gpu_memory: + status["gpu_memory"] = { + "total_gb": round(gpu_memory["total_gb"], 2), + "allocated_gb": round(gpu_memory["allocated_gb"], 2), + "reserved_gb": round(gpu_memory["reserved_gb"], 2), + "free_gb": round(gpu_memory["free_gb"], 2), + "utilization_percent": round(gpu_memory["utilization"], 1), + } + else: + status["gpu_memory"] = None + + return status + + @_app.post("/inference", response_model=InferenceResponse) + async def run_inference(request: InferenceRequest): + """Submit inference task and return task ID.""" + global _running_task_id + + if _backend is None: + raise HTTPException(status_code=500, detail="Backend not initialized") + + # Generate unique task ID + task_id = str(uuid.uuid4()) + + # Create task status + if _running_task_id is not None: + status_msg = f"[{task_id}] Task queued (waiting for {_running_task_id} to complete)" + else: + status_msg = f"[{task_id}] Task submitted" + + _tasks[task_id] = TaskStatus( + task_id=task_id, + status="pending", + message=status_msg, + created_at=time.time(), + export_dir=request.export_dir, + request=request, + # Record essential parameters + num_images=len(request.image_paths), + export_format=request.export_format, + process_res_method=request.process_res_method, + video_path=( + request.image_paths[0] if request.image_paths else None + ), # Use first image path as video reference + ) + + # Add task to queue + _task_queue.append(task_id) + + # If no task is running, start processing the queue + if _running_task_id is None: + _process_next_task() + + return InferenceResponse( + success=True, + message="Task submitted successfully", + task_id=task_id, + export_dir=request.export_dir, + export_format=request.export_format, + ) + + @_app.get("/task/{task_id}", response_model=TaskStatus) + async def get_task_status(task_id: str): + """Get task status by task ID.""" + if task_id not in _tasks: + raise HTTPException(status_code=404, detail="Task not found") + + return _tasks[task_id] + + @_app.get("/gpu-memory") + async def get_gpu_memory(): + """Get detailed GPU memory information.""" + gpu_memory = get_gpu_memory_info() + if gpu_memory is None: + return { + "available": False, + "message": "CUDA not available or memory info cannot be retrieved", + } + + return { + "available": True, + "total_gb": round(gpu_memory["total_gb"], 2), + "allocated_gb": round(gpu_memory["allocated_gb"], 2), + "reserved_gb": round(gpu_memory["reserved_gb"], 2), + "free_gb": round(gpu_memory["free_gb"], 2), + "utilization_percent": round(gpu_memory["utilization"], 1), + "status": ( + "healthy" + if gpu_memory["utilization"] < 80 + else "warning" if gpu_memory["utilization"] < 95 else "critical" + ), + } + + @_app.get("/tasks") + async def list_tasks(): + """List all tasks.""" + # Separate active and completed tasks + active_tasks = [task for task in _tasks.values() if task.status in ["pending", "running"]] + completed_tasks = [ + task for task in _tasks.values() if task.status in ["completed", "failed"] + ] + + return { + "tasks": list(_tasks.values()), + "active_tasks": active_tasks, + "completed_tasks": completed_tasks, + "active_count": len(active_tasks), + "total_count": len(_tasks), + } + + @_app.post("/cleanup") + async def manual_cleanup(): + """Manually trigger task cleanup.""" + try: + _cleanup_old_tasks() + return {"message": "Cleanup completed", "active_tasks": len(_tasks)} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Cleanup failed: {str(e)}") + + @_app.delete("/task/{task_id}") + async def delete_task(task_id: str): + """Delete a specific task.""" + if task_id not in _tasks: + raise HTTPException(status_code=404, detail="Task not found") + + # Only allow deletion of completed/failed tasks + if _tasks[task_id].status not in ["completed", "failed"]: + raise HTTPException(status_code=400, detail="Cannot delete running or pending tasks") + + del _tasks[task_id] + return {"message": f"Task {task_id} deleted successfully"} + + @_app.post("/reload") + async def reload_model(): + """Reload the model.""" + if _backend is None: + raise HTTPException(status_code=500, detail="Backend not initialized") + + try: + _backend.model = None + _backend.model_loaded = False + _backend.load_model() + return {"message": "Model reloaded successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to reload model: {str(e)}") + + # ============================================================================ + # Gallery routes + # ============================================================================ + + if _gallery_dir and os.path.exists(_gallery_dir): + # Load gallery HTML page (with modified paths for /gallery/ subdirectory) + _gallery_html = _load_gallery_html() + + @_app.get("/gallery/", response_class=HTMLResponse) + @_app.get("/gallery", response_class=HTMLResponse) + async def gallery_home(): + """Gallery home page.""" + return HTMLResponse(_gallery_html) + + @_app.get("/gallery/manifest.json") + async def gallery_manifest(): + """Get gallery group list.""" + try: + return build_group_list(_gallery_dir) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to build group list: {str(e)}" + ) + + @_app.get("/gallery/manifest/{group}.json") + async def gallery_group_manifest(group: str): + """Get manifest for a specific group.""" + if not _is_plain_name(group): + raise HTTPException(status_code=400, detail="Invalid group name") + try: + return build_group_manifest(_gallery_dir, group) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to build group manifest: {str(e)}" + ) + + @_app.get("/gallery/{path:path}") + async def gallery_files(path: str): + """Serve gallery static files (GLB, JPG, etc.).""" + # Security check: prevent directory traversal + path_parts = path.split("/") + if any(not _is_plain_name(part) for part in path_parts if part): + raise HTTPException(status_code=400, detail="Invalid path") + + file_path = os.path.join(_gallery_dir, *path_parts) + + # Ensure the file is within gallery directory + real_file_path = os.path.realpath(file_path) + real_gallery_dir = os.path.realpath(_gallery_dir) + if not real_file_path.startswith(real_gallery_dir): + raise HTTPException(status_code=403, detail="Access denied") + + if not os.path.exists(file_path) or not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail="File not found") + + return FileResponse(file_path) + + return _app + + +def start_server( + model_dir: str, + device: str = "cuda", + host: str = "127.0.0.1", + port: int = 8000, + gallery_dir: Optional[str] = None, +): + """Start the backend server.""" + app = create_app(model_dir, device, gallery_dir) + + print("Starting Depth Anything 3 Backend...") + print(f"Model directory: {model_dir}") + print(f"Device: {device}") + print(f"Server: http://{host}:{port}") + print(f"Dashboard: http://{host}:{port}/dashboard") + print(f"API Status: http://{host}:{port}/status") + + if gallery_dir and os.path.exists(gallery_dir): + print(f"Gallery: http://{host}:{port}/gallery/") + + print("=" * 60) + print("Backend is running! You can now:") + print(f" • Open home page: http://{host}:{port}") + print(f" • Open dashboard: http://{host}:{port}/dashboard") + print(f" • Check API status: http://{host}:{port}/status") + + if gallery_dir and os.path.exists(gallery_dir): + print(f" • Browse gallery: http://{host}:{port}/gallery/") + + print(" • Submit inference tasks via API") + print("=" * 60) + + uvicorn.run(app, host=host, port=port, log_level="info") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Depth Anything 3 Backend Server") + parser.add_argument("--model-dir", required=True, help="Model directory path") + parser.add_argument("--device", default="cuda", help="Device to use") + parser.add_argument("--host", default="127.0.0.1", help="Host to bind to") + parser.add_argument("--port", type=int, default=8000, help="Port to bind to") + parser.add_argument("--gallery-dir", help="Gallery directory path (optional)") + + args = parser.parse_args() + start_server(args.model_dir, args.device, args.host, args.port, args.gallery_dir) diff --git a/src/depth_anything_3/services/gallery.py b/src/depth_anything_3/services/gallery.py new file mode 100644 index 0000000000000000000000000000000000000000..f72bb5e5f6defbc24cf2278a53b7162a8ad5519d --- /dev/null +++ b/src/depth_anything_3/services/gallery.py @@ -0,0 +1,806 @@ +#!/usr/bin/env python3 +# flake8: noqa: E501 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Depth Anything 3 Gallery Server (two-level, single-file) +Now supports paginated depth preview (4 per page). +""" + +import argparse +import json +import mimetypes +import os +import posixpath +import sys +from functools import partial +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import quote, unquote + +# ------------------------------ Embedded HTML ------------------------------ # + +HTML_PAGE = r""" + + + + Depth Anything 3 Gallery + + + + + + + +
+
+ +

Depth Anything 3 Gallery

+ + +
+
Level 1 shows groups only; click a group to browse scenes and previews.
+
+ +
+ +
+

+ 🎯 Depth Anything 3 Gallery +

+

+ Explore 3D reconstructions and depth visualizations from Depth Anything 3. + Browse through groups of scenes, preview 3D models, and examine depth maps interactively. +

+
+ +
+
    + +
    + + +
    + + + + + + + + +""" + +# ------------------------------ Utilities ------------------------------ # + +IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp") + + +def _url_join(*parts: str) -> str: + norm = posixpath.join(*[p.replace("\\", "/") for p in parts]) + segs = [s for s in norm.split("/") if s not in ("", ".")] + return "/".join(quote(s) for s in segs) + + +def _is_plain_name(name: str) -> bool: + return all(c not in name for c in ("/", "\\")) and name not in (".", "..") + + +def build_group_list(root_dir: str) -> dict: + groups = [] + try: + for gname in sorted(os.listdir(root_dir)): + gpath = os.path.join(root_dir, gname) + if not os.path.isdir(gpath): + continue + has_scene = False + try: + for sname in os.listdir(gpath): + spath = os.path.join(gpath, sname) + if not os.path.isdir(spath): + continue + if os.path.exists(os.path.join(spath, "scene.glb")) and os.path.exists( + os.path.join(spath, "scene.jpg") + ): + has_scene = True + break + except Exception: + pass + if has_scene: + groups.append({"id": gname, "title": gname}) + except Exception as e: + print(f"[warn] build_group_list failed: {e}", file=sys.stderr) + return {"groups": groups} + + +def build_group_manifest(root_dir: str, group: str) -> dict: + items = [] + gpath = os.path.join(root_dir, group) + try: + if not os.path.isdir(gpath): + return {"group": group, "items": []} + for sname in sorted(os.listdir(gpath)): + spath = os.path.join(gpath, sname) + if not os.path.isdir(spath): + continue + glb_fs = os.path.join(spath, "scene.glb") + jpg_fs = os.path.join(spath, "scene.jpg") + if not (os.path.exists(glb_fs) and os.path.exists(jpg_fs)): + continue + depth_images = [] + dpath = os.path.join(spath, "depth_vis") + if os.path.isdir(dpath): + files = [ + f for f in os.listdir(dpath) if os.path.splitext(f)[1].lower() in IMAGE_EXTS + ] + for fn in sorted(files): + depth_images.append("/" + _url_join(group, sname, "depth_vis", fn)) + items.append( + { + "id": sname, + "title": sname, + "model": "/" + _url_join(group, sname, "scene.glb"), + "thumbnail": "/" + _url_join(group, sname, "scene.jpg"), + "depth_images": depth_images, + } + ) + except Exception as e: + print(f"[warn] build_group_manifest failed for {group}: {e}", file=sys.stderr) + return {"group": group, "items": items} + + +class GalleryHandler(SimpleHTTPRequestHandler): + def __init__(self, *args, directory=None, **kwargs): + super().__init__(*args, directory=directory, **kwargs) + + def do_GET(self): + if self.path in ("/", "/index.html") or self.path.startswith("/?"): + content = HTML_PAGE.encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(content) + return + if self.path == "/manifest.json": + data = json.dumps( + build_group_list(self.directory), ensure_ascii=False, indent=2 + ).encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(data) + return + if self.path.startswith("/manifest/") and self.path.endswith(".json"): + group_enc = self.path[len("/manifest/") : -len(".json")] + try: + group = unquote(group_enc) + except Exception: + group = group_enc + if not _is_plain_name(group): + self.send_error(HTTPStatus.BAD_REQUEST, "Invalid group name") + return + data = json.dumps( + build_group_manifest(self.directory, group), ensure_ascii=False, indent=2 + ).encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(data) + return + if self.path == "/favicon.ico": + self.send_response(HTTPStatus.NO_CONTENT) + self.end_headers() + return + return super().do_GET() + + def list_directory(self, path): + self.send_error(HTTPStatus.NOT_FOUND, "Directory listing disabled") + return None + + +def gallery(): + parser = argparse.ArgumentParser( + description="Depth Anything 3 Gallery Server (two-level, with pagination)" + ) + parser.add_argument( + "-d", "--dir", required=True, help="Gallery root directory (two-level: group/scene)" + ) + parser.add_argument("-p", "--port", type=int, default=8000, help="Port (default 8000)") + parser.add_argument("--host", default="127.0.0.1", help="Host address (default 127.0.0.1)") + parser.add_argument("--open", action="store_true", help="Open browser after launch") + args = parser.parse_args() + + root_dir = os.path.abspath(args.dir) + if not os.path.isdir(root_dir): + print(f"[error] Directory not found: {root_dir}", file=sys.stderr) + sys.exit(1) + + Handler = partial(GalleryHandler, directory=root_dir) + server = ThreadingHTTPServer((args.host, args.port), Handler) + + addr = f"http://{args.host}:{args.port}/" + print(f"[info] Serving gallery from: {root_dir}") + print(f"[info] Open: {addr}") + + if args.open: + try: + import webbrowser + + webbrowser.open(addr) + except Exception as e: + print(f"[warn] Failed to open browser: {e}", file=sys.stderr) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\n[info] Shutting down...") + finally: + server.server_close() + + +def main(): + """Main entry point for gallery server.""" + mimetypes.add_type("model/gltf-binary", ".glb") + gallery() + + +if __name__ == "__main__": + main() diff --git a/src/depth_anything_3/services/inference_service.py b/src/depth_anything_3/services/inference_service.py new file mode 100644 index 0000000000000000000000000000000000000000..f110bb348af03a3dcf29c812d8f3e24436f75c02 --- /dev/null +++ b/src/depth_anything_3/services/inference_service.py @@ -0,0 +1,346 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unified Inference Service +Provides unified interface for local and remote inference +""" + +from typing import Any, Dict, List, Optional, Union + +import numpy as np +import requests +import typer + +from ..api import DepthAnything3 + + +class InferenceService: + """Unified inference service class""" + + def __init__(self, model_dir: str, device: str = "cuda"): + self.model_dir = model_dir + self.device = device + self.model = None + + def load_model(self): + """Load model""" + if self.model is None: + typer.echo(f"Loading model from {self.model_dir}...") + self.model = DepthAnything3.from_pretrained(self.model_dir).to(self.device) + return self.model + + def run_local_inference( + self, + image_paths: List[str], + export_dir: str, + export_format: str = "mini_npz-glb", + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + export_feat_layers: List[int] = None, + extrinsics: Optional[np.ndarray] = None, + intrinsics: Optional[np.ndarray] = None, + align_to_input_ext_scale: bool = True, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + conf_thresh_percentile: float = 40.0, + num_max_points: int = 1_000_000, + show_cameras: bool = True, + feat_vis_fps: int = 15, + batch_size: Union[int, str] = "all", + max_batch_size: int = 64, + target_memory_utilization: float = 0.85, + ) -> Any: + """Run local inference. + + Args: + batch_size: Batch size for processing: + - "all": Process all images in one batch (original behavior) + - "auto": Adaptive batching based on available GPU memory + - int: Fixed batch size (e.g., 4, 8, 16) + max_batch_size: Maximum batch size when using adaptive batching + target_memory_utilization: Target GPU memory usage (0.0-1.0) for adaptive batching + """ + if export_feat_layers is None: + export_feat_layers = [] + + model = self.load_model() + + typer.echo(f"Running inference on {len(image_paths)} images...") + + # Use batch_inference for adaptive or fixed batching + if batch_size != "all": + typer.echo(f"Batch mode: {batch_size}" + (f" (max: {max_batch_size})" if batch_size == "auto" else "")) + + def progress_callback(processed: int, total: int): + typer.echo(f" Progress: {processed}/{total} images") + + predictions = model.batch_inference( + images=image_paths, + process_res=process_res, + batch_size=batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + progress_callback=progress_callback, + ) + + # For batch inference, we need to merge results and export + # The first prediction contains all depth maps if we need to export + if export_dir and predictions: + # Merge all predictions into one for export + merged = self._merge_predictions(predictions) + from depth_anything_3.utils.export import export + export( + merged, + export_dir, + export_format, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + ) + typer.echo(f"Results saved to {export_dir}") + typer.echo(f"Export format: {export_format}") + + return predictions + + # Original behavior: process all images in one batch + inference_kwargs = { + "image": image_paths, + "export_dir": export_dir, + "export_format": export_format, + "process_res": process_res, + "process_res_method": process_res_method, + "export_feat_layers": export_feat_layers, + "align_to_input_ext_scale": align_to_input_ext_scale, + "use_ray_pose": use_ray_pose, + "ref_view_strategy": ref_view_strategy, + "conf_thresh_percentile": conf_thresh_percentile, + "num_max_points": num_max_points, + "show_cameras": show_cameras, + "feat_vis_fps": feat_vis_fps, + } + + # Add pose data (if exists) + if extrinsics is not None: + inference_kwargs["extrinsics"] = extrinsics + if intrinsics is not None: + inference_kwargs["intrinsics"] = intrinsics + + prediction = model.inference(**inference_kwargs) + + typer.echo(f"Results saved to {export_dir}") + typer.echo(f"Export format: {export_format}") + + return prediction + + def _merge_predictions(self, predictions: List[Any]) -> Any: + """Merge multiple batch predictions into one.""" + if not predictions: + return None + if len(predictions) == 1: + return predictions[0] + + # Merge depth maps and other fields + merged_depths = [] + merged_confs = [] + merged_image_paths = [] + + for pred in predictions: + if hasattr(pred, 'depth') and pred.depth is not None: + # Handle batched depth maps + if pred.depth.dim() == 4: # (B, 1, H, W) + for i in range(pred.depth.shape[0]): + merged_depths.append(pred.depth[i]) + else: + merged_depths.append(pred.depth) + + if hasattr(pred, 'confidence') and pred.confidence is not None: + if pred.confidence.dim() == 4: + for i in range(pred.confidence.shape[0]): + merged_confs.append(pred.confidence[i]) + else: + merged_confs.append(pred.confidence) + + if hasattr(pred, 'image_paths') and pred.image_paths: + merged_image_paths.extend(pred.image_paths) + + # Use first prediction as base and update fields + import torch + merged = predictions[0] + if merged_depths: + merged.depth = torch.stack(merged_depths) + if merged_confs: + merged.confidence = torch.stack(merged_confs) + if merged_image_paths: + merged.image_paths = merged_image_paths + + return merged + + def run_backend_inference( + self, + image_paths: List[str], + export_dir: str, + backend_url: str, + export_format: str = "mini_npz-glb", + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + export_feat_layers: List[int] = None, + extrinsics: Optional[np.ndarray] = None, + intrinsics: Optional[np.ndarray] = None, + align_to_input_ext_scale: bool = True, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + conf_thresh_percentile: float = 40.0, + num_max_points: int = 1_000_000, + show_cameras: bool = True, + feat_vis_fps: int = 15, + ) -> Dict[str, Any]: + """Run backend inference""" + if export_feat_layers is None: + export_feat_layers = [] + + # Check backend status + if not self._check_backend_status(backend_url): + raise typer.BadParameter(f"Backend service is not running at {backend_url}") + + # Prepare payload + payload = { + "image_paths": image_paths, + "export_dir": export_dir, + "export_format": export_format, + "process_res": process_res, + "process_res_method": process_res_method, + "export_feat_layers": export_feat_layers, + "align_to_input_ext_scale": align_to_input_ext_scale, + "use_ray_pose": use_ray_pose, + "ref_view_strategy": ref_view_strategy, + "conf_thresh_percentile": conf_thresh_percentile, + "num_max_points": num_max_points, + "show_cameras": show_cameras, + "feat_vis_fps": feat_vis_fps, + } + + # Add pose data (if exists) + if extrinsics is not None: + payload["extrinsics"] = [ext.astype(np.float64).tolist() for ext in extrinsics] + if intrinsics is not None: + payload["intrinsics"] = [intr.astype(np.float64).tolist() for intr in intrinsics] + + # Submit task + typer.echo("Submitting inference task to backend...") + try: + response = requests.post(f"{backend_url}/inference", json=payload, timeout=30) + response.raise_for_status() + result = response.json() + + if result["success"]: + task_id = result["task_id"] + typer.echo("Task submitted successfully!") + typer.echo(f"Task ID: {task_id}") + typer.echo(f"Results will be saved to: {export_dir}") + typer.echo(f"Check backend logs for progress updates with task ID: {task_id}") + return result + else: + raise typer.BadParameter( + f"Backend inference submission failed: {result['message']}" + ) + except requests.exceptions.RequestException as e: + raise typer.BadParameter(f"Backend inference submission failed: {e}") + + def _check_backend_status(self, backend_url: str) -> bool: + """Check backend status""" + try: + response = requests.get(f"{backend_url}/status", timeout=5) + return response.status_code == 200 + except Exception: + return False + + +def run_inference( + image_paths: List[str], + export_dir: str, + model_dir: str, + device: str = "cuda", + backend_url: Optional[str] = None, + export_format: str = "mini_npz-glb", + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + export_feat_layers: List[int] = None, + extrinsics: Optional[np.ndarray] = None, + intrinsics: Optional[np.ndarray] = None, + align_to_input_ext_scale: bool = True, + use_ray_pose: bool = False, + ref_view_strategy: str = "saddle_balanced", + conf_thresh_percentile: float = 40.0, + num_max_points: int = 1_000_000, + show_cameras: bool = True, + feat_vis_fps: int = 15, + batch_size: Union[int, str] = "all", + max_batch_size: int = 64, + target_memory_utilization: float = 0.85, +) -> Union[Any, Dict[str, Any]]: + """Unified inference interface. + + Args: + batch_size: Batch size for processing: + - "all": Process all images in one batch (default, original behavior) + - "auto": Adaptive batching based on available GPU memory + - int: Fixed batch size (e.g., 4, 8, 16) + max_batch_size: Maximum batch size when using adaptive batching (default: 64) + target_memory_utilization: Target GPU memory usage (0.0-1.0) for adaptive batching + """ + + service = InferenceService(model_dir, device) + + if backend_url: + return service.run_backend_inference( + image_paths=image_paths, + export_dir=export_dir, + backend_url=backend_url, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + extrinsics=extrinsics, + intrinsics=intrinsics, + align_to_input_ext_scale=align_to_input_ext_scale, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + ) + else: + return service.run_local_inference( + image_paths=image_paths, + export_dir=export_dir, + export_format=export_format, + process_res=process_res, + process_res_method=process_res_method, + export_feat_layers=export_feat_layers, + extrinsics=extrinsics, + intrinsics=intrinsics, + align_to_input_ext_scale=align_to_input_ext_scale, + use_ray_pose=use_ray_pose, + ref_view_strategy=ref_view_strategy, + conf_thresh_percentile=conf_thresh_percentile, + num_max_points=num_max_points, + show_cameras=show_cameras, + feat_vis_fps=feat_vis_fps, + batch_size=batch_size, + max_batch_size=max_batch_size, + target_memory_utilization=target_memory_utilization, + ) diff --git a/src/depth_anything_3/services/input_handlers.py b/src/depth_anything_3/services/input_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..99dc551cac213e4b5d0341158f8b2a9b598e20db --- /dev/null +++ b/src/depth_anything_3/services/input_handlers.py @@ -0,0 +1,267 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Input Processing Service +Handles different types of inputs (image, images, colmap, video) +""" + +import glob +import os +from typing import List, Tuple + +import cv2 +import numpy as np +import typer + +from ..utils.read_write_model import read_model + + +class InputHandler: + """Base input handler class""" + + @staticmethod + def validate_path(path: str, path_type: str = "file") -> str: + """Validate path""" + if not os.path.exists(path): + raise typer.BadParameter(f"{path_type} not found: {path}") + return path + + @staticmethod + def handle_export_dir(export_dir: str, auto_cleanup: bool = False) -> str: + """Handle export directory""" + if os.path.exists(export_dir): + if auto_cleanup: + typer.echo(f"Auto-cleaning existing export directory: {export_dir}") + import shutil + + shutil.rmtree(export_dir) + os.makedirs(export_dir, exist_ok=True) + else: + typer.echo(f"Export directory '{export_dir}' already exists.") + if typer.confirm("Do you want to clean it and continue?"): + import shutil + + shutil.rmtree(export_dir) + os.makedirs(export_dir, exist_ok=True) + typer.echo(f"Cleaned export directory: {export_dir}") + else: + typer.echo("Operation cancelled.") + raise typer.Exit(0) + else: + os.makedirs(export_dir, exist_ok=True) + return export_dir + + +class ImageHandler(InputHandler): + """Single image handler""" + + @staticmethod + def process(image_path: str) -> List[str]: + """Process single image""" + InputHandler.validate_path(image_path, "Image file") + return [image_path] + + +class ImagesHandler(InputHandler): + """Image directory handler""" + + @staticmethod + def process(images_dir: str, image_extensions: str = "png,jpg,jpeg") -> List[str]: + """Process image directory""" + InputHandler.validate_path(images_dir, "Images directory") + + # Parse extensions + extensions = [ext.strip().lower() for ext in image_extensions.split(",")] + extensions = [ext if ext.startswith(".") else f".{ext}" for ext in extensions] + + # Find image files + image_files = [] + for ext in extensions: + pattern = f"*{ext}" + image_files.extend(glob.glob(os.path.join(images_dir, pattern))) + image_files.extend(glob.glob(os.path.join(images_dir, pattern.upper()))) + + image_files = sorted(list(set(image_files))) # Remove duplicates and sort + + if not image_files: + raise typer.BadParameter( + f"No image files found in {images_dir} with extensions: {extensions}" + ) + + typer.echo(f"Found {len(image_files)} images to process") + return image_files + + +class ColmapHandler(InputHandler): + """COLMAP data handler""" + + @staticmethod + def process( + colmap_dir: str, sparse_subdir: str = "" + ) -> Tuple[List[str], np.ndarray, np.ndarray]: + """Process COLMAP data""" + InputHandler.validate_path(colmap_dir, "COLMAP directory") + + # Build paths + images_dir = os.path.join(colmap_dir, "images") + if sparse_subdir: + sparse_dir = os.path.join(colmap_dir, "sparse", sparse_subdir) + else: + sparse_dir = os.path.join(colmap_dir, "sparse") + + InputHandler.validate_path(images_dir, "Images directory") + InputHandler.validate_path(sparse_dir, "Sparse reconstruction directory") + + # Load COLMAP data + typer.echo("Loading COLMAP reconstruction data...") + try: + cameras, images, points3D = read_model(sparse_dir) + + typer.echo( + f"Loaded COLMAP data: {len(cameras)} cameras, {len(images)} images, " + f"{len(points3D)} 3D points." + ) + + # Get image files and pose data + image_files = [] + extrinsics = [] + intrinsics = [] + + for image_id, image_data in images.items(): + image_name = image_data.name + image_path = os.path.join(images_dir, image_name) + + if os.path.exists(image_path): + image_files.append(image_path) + + # Get camera parameters + camera = cameras[image_data.camera_id] + + # Convert quaternion to rotation matrix + R = image_data.qvec2rotmat() + t = image_data.tvec + + # Create extrinsic matrix (world to camera) + extrinsic = np.eye(4) + extrinsic[:3, :3] = R + extrinsic[:3, 3] = t + extrinsics.append(extrinsic) + + # Create intrinsic matrix + if camera.model == "PINHOLE": + fx, fy, cx, cy = camera.params + elif camera.model == "SIMPLE_PINHOLE": + f, cx, cy = camera.params + fx = fy = f + else: + # For other models, use basic pinhole approximation + fx = fy = camera.params[0] if len(camera.params) > 0 else 1000 + cx = camera.width / 2 + cy = camera.height / 2 + + intrinsic = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) + intrinsics.append(intrinsic) + + if not image_files: + raise typer.BadParameter("No valid images found in COLMAP data") + + typer.echo(f"Found {len(image_files)} valid images with pose data") + + return image_files, np.array(extrinsics), np.array(intrinsics) + + except Exception as e: + raise typer.BadParameter(f"Failed to load COLMAP data: {e}") + + +class VideoHandler(InputHandler): + """Video handler""" + + @staticmethod + def process(video_path: str, output_dir: str, fps: float = 1.0) -> List[str]: + """Process video, extract frames""" + InputHandler.validate_path(video_path, "Video file") + + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + raise typer.BadParameter(f"Cannot open video: {video_path}") + + # Get video properties + video_fps = cap.get(cv2.CAP_PROP_FPS) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + duration = total_frames / video_fps + + # Calculate frame interval (ensure at least 1) + frame_interval = max(1, int(video_fps / fps)) + actual_fps = video_fps / frame_interval + + typer.echo(f"Video FPS: {video_fps:.2f}, Duration: {duration:.2f}s") + + # Warn if requested FPS is higher than video FPS + if fps > video_fps: + typer.echo( + f"⚠️ Warning: Requested sampling FPS ({fps:.2f}) exceeds video FPS ({video_fps:.2f})", # noqa: E501 + err=True, + ) + typer.echo( + f"⚠️ Using maximum available FPS: {actual_fps:.2f} (extracting every frame)", + err=True, + ) + + typer.echo(f"Extracting frames at {actual_fps:.2f} FPS (every {frame_interval} frame(s))") + + # Create output directory + frames_dir = os.path.join(output_dir, "input_images") + os.makedirs(frames_dir, exist_ok=True) + + frame_count = 0 + saved_count = 0 + + while True: + ret, frame = cap.read() + if not ret: + break + + if frame_count % frame_interval == 0: + frame_path = os.path.join(frames_dir, f"{saved_count:06d}.png") + cv2.imwrite(frame_path, frame) + saved_count += 1 + + frame_count += 1 + + cap.release() + typer.echo(f"Extracted {saved_count} frames to {frames_dir}") + + # Get frame file list + frame_files = sorted( + [f for f in os.listdir(frames_dir) if f.endswith((".png", ".jpg", ".jpeg"))] + ) + if not frame_files: + raise typer.BadParameter("No frames extracted from video") + + return [os.path.join(frames_dir, f) for f in frame_files] + + +def parse_export_feat(export_feat_str: str) -> List[int]: + """Parse export_feat parameter""" + if not export_feat_str: + return [] + + try: + return [int(x.strip()) for x in export_feat_str.split(",") if x.strip()] + except ValueError: + raise typer.BadParameter( + f"Invalid export_feat format: {export_feat_str}. " + "Use comma-separated integers like '0,1,2'" + ) diff --git a/src/depth_anything_3/specs.py b/src/depth_anything_3/specs.py new file mode 100644 index 0000000000000000000000000000000000000000..9f999fff3d26ec79ea6a8302e234a4238fb50cbb --- /dev/null +++ b/src/depth_anything_3/specs.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional + +import numpy as np +import torch + + +@dataclass +class Gaussians: + """3DGS parameters, all in world space""" + + means: torch.Tensor # world points, "batch gaussian dim" + scales: torch.Tensor # scales_std, "batch gaussian 3" + rotations: torch.Tensor # world_quat_wxyz, "batch gaussian 4" + harmonics: torch.Tensor # world SH, "batch gaussian 3 d_sh" + opacities: torch.Tensor # opacity | opacity SH, "batch gaussian" | "batch gaussian 1 d_sh" + + +@dataclass +class Prediction: + depth: np.ndarray # N, H, W + is_metric: int + sky: np.ndarray | None = None # N, H, W + conf: np.ndarray | None = None # N, H, W + extrinsics: np.ndarray | None = None # N, 4, 4 + intrinsics: np.ndarray | None = None # N, 3, 3 + processed_images: np.ndarray | None = None # N, H, W, 3 - processed images for visualization + gaussians: Gaussians | None = None # 3D gaussians + aux: dict[str, Any] = None # + scale_factor: Optional[float] = None # metric scale diff --git a/src/depth_anything_3/utils/adaptive_batching.py b/src/depth_anything_3/utils/adaptive_batching.py new file mode 100644 index 0000000000000000000000000000000000000000..1d86473839d39de986824d26c68d84beb2170f27 --- /dev/null +++ b/src/depth_anything_3/utils/adaptive_batching.py @@ -0,0 +1,599 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Adaptive Batching Module for Depth Anything 3. + +This module provides intelligent batch size selection based on available GPU memory, +model parameters, and input resolution. It maximizes throughput by dynamically +adjusting batch sizes to utilize as much GPU memory as safely possible. + +Key features: +- Memory profiling for accurate estimation +- Model-specific memory coefficients +- Resolution-aware scaling +- Safety margins to prevent OOM +- Support for CUDA and MPS devices +""" +from __future__ import annotations + +import gc +import math +from dataclasses import dataclass, field +from typing import Callable, Iterator, Sequence, TypeVar + +import torch + +from depth_anything_3.utils.logger import logger + +T = TypeVar("T") + + +# ============================================================================= +# Model Memory Profiles +# ============================================================================= + +@dataclass +class ModelMemoryProfile: + """Memory profile for a specific model variant. + + Attributes: + base_memory_mb: Fixed memory overhead (model weights, buffers) + per_image_mb_at_504: Memory per image at 504px resolution + activation_scale: Scaling factor for activations (quadratic with resolution) + safety_margin: Safety margin to prevent OOM (0.0 to 1.0) + """ + base_memory_mb: float + per_image_mb_at_504: float + activation_scale: float = 1.0 + safety_margin: float = 0.15 # 15% safety margin by default + + +# Empirically measured memory profiles for each model variant +# Values calibrated on various GPU configurations +MODEL_MEMORY_PROFILES: dict[str, ModelMemoryProfile] = { + # Small models (ViT-S backbone) + "da3-small": ModelMemoryProfile( + base_memory_mb=350, + per_image_mb_at_504=180, + activation_scale=0.8, + ), + # Base models (ViT-B backbone) + "da3-base": ModelMemoryProfile( + base_memory_mb=800, + per_image_mb_at_504=350, + activation_scale=1.0, + ), + # Large models (ViT-L backbone) + "da3-large": ModelMemoryProfile( + base_memory_mb=1600, + per_image_mb_at_504=600, + activation_scale=1.2, + ), + "da3metric-large": ModelMemoryProfile( + base_memory_mb=1700, + per_image_mb_at_504=650, + activation_scale=1.2, + ), + "da3mono-large": ModelMemoryProfile( + base_memory_mb=1700, + per_image_mb_at_504=650, + activation_scale=1.2, + ), + # Giant models (ViT-G backbone) + "da3-giant": ModelMemoryProfile( + base_memory_mb=4500, + per_image_mb_at_504=1200, + activation_scale=1.5, + ), + "da3nested-giant-large": ModelMemoryProfile( + base_memory_mb=6000, + per_image_mb_at_504=1500, + activation_scale=1.8, + ), +} + + +# ============================================================================= +# Memory Utilities +# ============================================================================= + +def get_available_memory_mb(device: torch.device) -> float: + """Get available GPU memory in MB. + + Args: + device: Target device + + Returns: + Available memory in MB, or float('inf') for CPU + """ + if device.type == "cuda": + torch.cuda.synchronize(device) + total = torch.cuda.get_device_properties(device).total_memory + reserved = torch.cuda.memory_reserved(device) + return (total - reserved) / (1024 * 1024) + + elif device.type == "mps": + # MPS doesn't expose free memory directly + # Use system memory as a rough proxy (conservative estimate) + try: + allocated = torch.mps.current_allocated_memory() + # Assume 8GB available for MPS (conservative for most Apple Silicon) + # This can be overridden via environment variable + import os + max_mps_memory_gb = float(os.environ.get("DA3_MPS_MAX_MEMORY_GB", "8")) + max_mps_memory_mb = max_mps_memory_gb * 1024 + return max(0, max_mps_memory_mb - (allocated / (1024 * 1024))) + except Exception: + return 6000 # Conservative fallback: 6GB + + else: + return float("inf") # CPU has no practical limit for batching + + +def get_total_memory_mb(device: torch.device) -> float: + """Get total GPU memory in MB. + + Args: + device: Target device + + Returns: + Total memory in MB + """ + if device.type == "cuda": + return torch.cuda.get_device_properties(device).total_memory / (1024 * 1024) + + elif device.type == "mps": + import os + return float(os.environ.get("DA3_MPS_MAX_MEMORY_GB", "8")) * 1024 + + else: + return float("inf") + + +# ============================================================================= +# Adaptive Batch Size Calculator +# ============================================================================= + +@dataclass +class AdaptiveBatchConfig: + """Configuration for adaptive batching. + + Attributes: + min_batch_size: Minimum batch size (default: 1) + max_batch_size: Maximum batch size cap (default: 64) + target_memory_utilization: Target GPU memory usage (0.0 to 1.0) + enable_profiling: Enable runtime memory profiling for calibration + profile_warmup_batches: Number of warmup batches before profiling + """ + min_batch_size: int = 1 + max_batch_size: int = 64 + target_memory_utilization: float = 0.85 # Use 85% of available memory + enable_profiling: bool = True + profile_warmup_batches: int = 2 + + +class AdaptiveBatchSizeCalculator: + """ + Calculates optimal batch sizes based on available GPU memory. + + This class provides intelligent batch size selection that: + 1. Estimates memory requirements based on model and resolution + 2. Measures actual memory usage during runtime (optional profiling) + 3. Adjusts batch sizes dynamically to maximize throughput + + Example: + >>> from depth_anything_3.utils.adaptive_batching import AdaptiveBatchSizeCalculator + >>> calc = AdaptiveBatchSizeCalculator( + ... model_name="da3-large", + ... device=torch.device("cuda"), + ... ) + >>> # Get optimal batch size for 100 images at 518px + >>> batch_size = calc.compute_optimal_batch_size( + ... num_images=100, + ... process_res=518, + ... ) + >>> print(f"Optimal batch size: {batch_size}") + """ + + def __init__( + self, + model_name: str, + device: torch.device, + config: AdaptiveBatchConfig | None = None, + ): + """Initialize the adaptive batch size calculator. + + Args: + model_name: Name of the DA3 model variant + device: Target device for inference + config: Optional configuration overrides + """ + self.model_name = model_name + self.device = device + self.config = config or AdaptiveBatchConfig() + + # Get memory profile for this model + self.profile = MODEL_MEMORY_PROFILES.get( + model_name, + # Fallback to large model profile for unknown models + MODEL_MEMORY_PROFILES["da3-large"] + ) + + # Runtime calibration data + self._measured_per_image_mb: float | None = None + self._profiling_complete: bool = False + self._batch_count: int = 0 + + def compute_optimal_batch_size( + self, + num_images: int, + process_res: int = 504, + reserved_memory_mb: float = 0, + ) -> int: + """Compute optimal batch size for given workload. + + Args: + num_images: Total number of images to process + process_res: Processing resolution + reserved_memory_mb: Additional memory to reserve (e.g., for other operations) + + Returns: + Optimal batch size + """ + # Get available memory + available_mb = get_available_memory_mb(self.device) + + if available_mb == float("inf"): + # CPU: return reasonable batch size based on image count + return min(num_images, self.config.max_batch_size) + + # Apply target utilization and reserve + usable_mb = (available_mb * self.config.target_memory_utilization) - reserved_memory_mb + + # Subtract base model memory + usable_mb -= self.profile.base_memory_mb + + if usable_mb <= 0: + logger.warn( + f"Insufficient memory for model. " + f"Available: {available_mb:.0f} MB, " + f"Model base: {self.profile.base_memory_mb:.0f} MB" + ) + return self.config.min_batch_size + + # Calculate per-image memory requirement + per_image_mb = self._estimate_per_image_memory(process_res) + + # Apply safety margin + per_image_mb *= (1 + self.profile.safety_margin) + + # Calculate optimal batch size + optimal_batch = int(usable_mb / per_image_mb) + + # Clamp to configured bounds + optimal_batch = max(self.config.min_batch_size, optimal_batch) + optimal_batch = min(self.config.max_batch_size, optimal_batch) + optimal_batch = min(num_images, optimal_batch) + + logger.debug( + f"Adaptive batch: {optimal_batch} " + f"(available: {available_mb:.0f} MB, " + f"per_image: {per_image_mb:.0f} MB @ {process_res}px)" + ) + + return optimal_batch + + def _estimate_per_image_memory(self, process_res: int) -> float: + """Estimate memory per image at given resolution. + + Memory scales approximately quadratically with resolution. + + Args: + process_res: Processing resolution + + Returns: + Estimated memory per image in MB + """ + # Use measured value if available from profiling + if self._measured_per_image_mb is not None and self._profiling_complete: + base_per_image = self._measured_per_image_mb + else: + base_per_image = self.profile.per_image_mb_at_504 + + # Scale quadratically with resolution + resolution_scale = (process_res / 504) ** 2 + + # Apply model-specific activation scale + return base_per_image * resolution_scale * self.profile.activation_scale + + def update_from_profiling(self, batch_size: int, memory_used_mb: float, process_res: int) -> None: + """Update memory estimates from actual profiling data. + + Called after inference to calibrate memory estimates. + + Args: + batch_size: Batch size used + memory_used_mb: Actual memory consumed + process_res: Resolution used + """ + if not self.config.enable_profiling: + return + + self._batch_count += 1 + + if self._batch_count <= self.config.profile_warmup_batches: + # Skip warmup batches (memory not stable) + return + + # Calculate per-image memory at reference resolution (504) + resolution_scale = (process_res / 504) ** 2 + memory_per_image = (memory_used_mb - self.profile.base_memory_mb) / batch_size + memory_at_504 = memory_per_image / resolution_scale / self.profile.activation_scale + + # Exponential moving average for stability + alpha = 0.3 + if self._measured_per_image_mb is None: + self._measured_per_image_mb = memory_at_504 + else: + self._measured_per_image_mb = ( + alpha * memory_at_504 + + (1 - alpha) * self._measured_per_image_mb + ) + + self._profiling_complete = True + + logger.debug( + f"Profiling update: measured {memory_at_504:.0f} MB/img @ 504px " + f"(running avg: {self._measured_per_image_mb:.0f} MB)" + ) + + def get_memory_estimate(self, batch_size: int, process_res: int) -> float: + """Get estimated total memory for a batch. + + Args: + batch_size: Batch size + process_res: Processing resolution + + Returns: + Estimated memory in MB + """ + per_image = self._estimate_per_image_memory(process_res) + return self.profile.base_memory_mb + (batch_size * per_image) + + +# ============================================================================= +# Batch Iterator +# ============================================================================= + +@dataclass +class BatchInfo: + """Information about a batch for processing. + + Attributes: + batch_idx: Index of this batch (0-indexed) + start_idx: Start index in original sequence + end_idx: End index in original sequence (exclusive) + items: Items in this batch + batch_size: Size of this batch + is_last: Whether this is the last batch + """ + batch_idx: int + start_idx: int + end_idx: int + items: list + batch_size: int = field(init=False) + is_last: bool = False + + def __post_init__(self): + self.batch_size = len(self.items) + + +def adaptive_batch_iterator( + items: Sequence[T], + calculator: AdaptiveBatchSizeCalculator, + process_res: int = 504, + reserved_memory_mb: float = 0, +) -> Iterator[BatchInfo]: + """ + Iterate over items with adaptive batch sizes. + + This iterator dynamically adjusts batch sizes based on available memory, + potentially increasing throughput compared to fixed batch sizes. + + Args: + items: Sequence of items to batch + calculator: Adaptive batch size calculator + process_res: Processing resolution + reserved_memory_mb: Additional memory to reserve + + Yields: + BatchInfo objects containing batch data and metadata + + Example: + >>> calc = AdaptiveBatchSizeCalculator("da3-large", device) + >>> for batch_info in adaptive_batch_iterator(images, calc, process_res=518): + ... result = model.inference(batch_info.items, process_res=518) + ... # Process result... + """ + total = len(items) + idx = 0 + batch_idx = 0 + + while idx < total: + remaining = total - idx + + # Compute optimal batch size for remaining items + batch_size = calculator.compute_optimal_batch_size( + num_images=remaining, + process_res=process_res, + reserved_memory_mb=reserved_memory_mb, + ) + + end_idx = min(idx + batch_size, total) + batch_items = list(items[idx:end_idx]) + + yield BatchInfo( + batch_idx=batch_idx, + start_idx=idx, + end_idx=end_idx, + items=batch_items, + is_last=(end_idx >= total), + ) + + idx = end_idx + batch_idx += 1 + + +# ============================================================================= +# High-Level API +# ============================================================================= + +def process_with_adaptive_batching( + items: Sequence[T], + process_fn: Callable[[list[T]], list], + model_name: str, + device: torch.device, + process_res: int = 504, + config: AdaptiveBatchConfig | None = None, + progress_callback: Callable[[int, int], None] | None = None, +) -> list: + """ + Process items with adaptive batching for optimal GPU utilization. + + This function handles the complete workflow of: + 1. Computing optimal batch sizes + 2. Processing batches + 3. Collecting and returning results + 4. Memory cleanup between batches + + Args: + items: Sequence of items to process + process_fn: Function to process a batch of items + model_name: Name of the DA3 model + device: Target device + process_res: Processing resolution + config: Optional batching configuration + progress_callback: Optional callback(processed, total) for progress updates + + Returns: + List of all results concatenated + + Example: + >>> def inference_fn(batch): + ... return model.inference(batch, process_res=518) + >>> + >>> results = process_with_adaptive_batching( + ... items=image_paths, + ... process_fn=inference_fn, + ... model_name="da3-large", + ... device=torch.device("cuda"), + ... process_res=518, + ... ) + """ + calculator = AdaptiveBatchSizeCalculator( + model_name=model_name, + device=device, + config=config, + ) + + all_results = [] + total = len(items) + + for batch_info in adaptive_batch_iterator(items, calculator, process_res): + # Process batch + results = process_fn(batch_info.items) + all_results.extend(results if isinstance(results, list) else [results]) + + # Progress callback + if progress_callback: + progress_callback(batch_info.end_idx, total) + + # Memory cleanup between batches (except last) + if not batch_info.is_last: + gc.collect() + if device.type == "cuda": + torch.cuda.empty_cache() + elif device.type == "mps": + torch.mps.empty_cache() + + # Optional: profile memory usage for calibration + if calculator.config.enable_profiling and device.type == "cuda": + memory_used = torch.cuda.max_memory_allocated(device) / (1024 * 1024) + calculator.update_from_profiling( + batch_size=batch_info.batch_size, + memory_used_mb=memory_used, + process_res=process_res, + ) + torch.cuda.reset_peak_memory_stats(device) + + return all_results + + +# ============================================================================= +# Utility Functions +# ============================================================================= + +def estimate_max_batch_size( + model_name: str, + device: torch.device, + process_res: int = 504, + target_utilization: float = 0.85, +) -> int: + """ + Estimate maximum batch size for a given model and resolution. + + Quick utility function for one-off batch size estimation. + + Args: + model_name: Name of the DA3 model + device: Target device + process_res: Processing resolution + target_utilization: Target memory utilization (0.0 to 1.0) + + Returns: + Estimated maximum batch size + + Example: + >>> max_batch = estimate_max_batch_size("da3-large", torch.device("cuda"), 518) + >>> print(f"Max batch size at 518px: {max_batch}") + """ + config = AdaptiveBatchConfig(target_memory_utilization=target_utilization) + calculator = AdaptiveBatchSizeCalculator(model_name, device, config) + + # Return estimate for a large number of images + return calculator.compute_optimal_batch_size(num_images=1000, process_res=process_res) + + +def log_batch_plan( + num_images: int, + model_name: str, + device: torch.device, + process_res: int = 504, +) -> None: + """ + Log the planned batching strategy for a workload. + + Useful for debugging and understanding how images will be batched. + + Args: + num_images: Number of images to process + model_name: Name of the DA3 model + device: Target device + process_res: Processing resolution + """ + calculator = AdaptiveBatchSizeCalculator(model_name, device) + + total_memory = get_total_memory_mb(device) + available_memory = get_available_memory_mb(device) + batch_size = calculator.compute_optimal_batch_size(num_images, process_res) + num_batches = math.ceil(num_images / batch_size) + memory_per_batch = calculator.get_memory_estimate(batch_size, process_res) + + logger.info( + f"Batch Plan for {model_name}:\n" + f" Images: {num_images} @ {process_res}px\n" + f" Device: {device} ({total_memory:.0f} MB total, {available_memory:.0f} MB available)\n" + f" Batch Size: {batch_size}\n" + f" Num Batches: {num_batches}\n" + f" Est. Memory/Batch: {memory_per_batch:.0f} MB" + ) diff --git a/src/depth_anything_3/utils/alignment.py b/src/depth_anything_3/utils/alignment.py new file mode 100644 index 0000000000000000000000000000000000000000..6f6a8243a4e6217618865b62745b1db4bf53bbef --- /dev/null +++ b/src/depth_anything_3/utils/alignment.py @@ -0,0 +1,164 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Alignment utilities for depth estimation and metric scaling. +""" + +from typing import Tuple + +import torch + + +def least_squares_scale_scalar( + a: torch.Tensor, b: torch.Tensor, eps: float = 1e-12 +) -> torch.Tensor: + """ + Compute least squares scale factor s such that a ≈ s * b. + + Args: + a: First tensor + b: Second tensor + eps: Small epsilon for numerical stability + + Returns: + Scalar tensor containing the scale factor + + Raises: + ValueError: If tensors have mismatched shapes or devices + TypeError: If tensors are not floating point + """ + if a.shape != b.shape: + raise ValueError(f"Shape mismatch: {a.shape} vs {b.shape}") + if a.device != b.device: + raise ValueError(f"Device mismatch: {a.device} vs {b.device}") + if not a.is_floating_point() or not b.is_floating_point(): + raise TypeError("Tensors must be floating point type") + + # Compute dot products for least squares solution + num = torch.dot(a.reshape(-1), b.reshape(-1)) + den = torch.dot(b.reshape(-1), b.reshape(-1)).clamp_min(eps) + return num / den + + +def compute_sky_mask(sky_prediction: torch.Tensor, threshold: float = 0.3) -> torch.Tensor: + """ + Compute non-sky mask from sky prediction. + + Args: + sky_prediction: Sky prediction tensor + threshold: Threshold for sky classification + + Returns: + Boolean mask where True indicates non-sky regions + """ + return sky_prediction < threshold + + +def compute_alignment_mask( + depth_conf: torch.Tensor, + non_sky_mask: torch.Tensor, + depth: torch.Tensor, + metric_depth: torch.Tensor, + median_conf: torch.Tensor, + min_depth_threshold: float = 1e-3, + min_metric_depth_threshold: float = 1e-2, +) -> torch.Tensor: + """ + Compute mask for depth alignment based on confidence and depth thresholds. + + Args: + depth_conf: Depth confidence tensor + non_sky_mask: Non-sky region mask + depth: Predicted depth tensor + metric_depth: Metric depth tensor + median_conf: Median confidence threshold + min_depth_threshold: Minimum depth threshold + min_metric_depth_threshold: Minimum metric depth threshold + + Returns: + Boolean mask for valid alignment regions + """ + return ( + (depth_conf >= median_conf) + & non_sky_mask + & (metric_depth > min_metric_depth_threshold) + & (depth > min_depth_threshold) + ) + + +def sample_tensor_for_quantile(tensor: torch.Tensor, max_samples: int = 100000) -> torch.Tensor: + """ + Sample tensor elements for quantile computation to reduce memory usage. + + Args: + tensor: Input tensor to sample + max_samples: Maximum number of samples to take + + Returns: + Sampled tensor + """ + if tensor.numel() <= max_samples: + return tensor + + idx = torch.randperm(tensor.numel(), device=tensor.device)[:max_samples] + return tensor.flatten()[idx] + + +def apply_metric_scaling( + depth: torch.Tensor, intrinsics: torch.Tensor, scale_factor: float = 300.0 +) -> torch.Tensor: + """ + Apply metric scaling to depth based on camera intrinsics. + + Args: + depth: Input depth tensor + intrinsics: Camera intrinsics tensor + scale_factor: Scaling factor for metric conversion + + Returns: + Scaled depth tensor + """ + focal_length = (intrinsics[:, :, 0, 0] + intrinsics[:, :, 1, 1]) / 2 + return depth * (focal_length[:, :, None, None] / scale_factor) + + +def set_sky_regions_to_max_depth( + depth: torch.Tensor, + depth_conf: torch.Tensor, + non_sky_mask: torch.Tensor, + max_depth: float = 200.0, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Set sky regions to maximum depth and high confidence. + + Args: + depth: Depth tensor + depth_conf: Depth confidence tensor + non_sky_mask: Non-sky region mask + max_depth: Maximum depth value for sky regions + + Returns: + Tuple of (updated_depth, updated_depth_conf) + """ + depth = depth.clone() + + # Set sky regions to max depth and high confidence + depth[~non_sky_mask] = max_depth + if depth_conf is not None: + depth_conf = depth_conf.clone() + depth_conf[~non_sky_mask] = 1.0 + return depth, depth_conf + else: + return depth, None diff --git a/src/depth_anything_3/utils/api_helpers.py b/src/depth_anything_3/utils/api_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..5e984411a44bfd7dfa3d2b2ea3e922a569fa92b2 --- /dev/null +++ b/src/depth_anything_3/utils/api_helpers.py @@ -0,0 +1,58 @@ +import argparse + + +def parse_scalar(s): + if not isinstance(s, str): + return s + t = s.strip() + lower_val = t.lower() + if lower_val == "true": + return True + if lower_val == "false": + return False + if lower_val in ("none", "null"): + return None + try: + return int(t, 10) + except Exception: + pass + try: + return float(t) + except Exception: + return s + + +def fn_kv_csv(s: str) -> dict[str, dict[str, object]]: + """ + Parse a string of comma-separated triplets: fn:key:value + + Returns: + dict[fn_name] -> dict[key] = parsed_value + + Example: + "fn1:width:1920,fn1:height:1080,fn2:quality:0.8" + -> {"fn1": {"width": 1920, "height": 1080}, "fn2": {"quality": 0.8}} + """ + result: dict[str, dict[str, object]] = {} + if not s: + return result + + for item in s.split(","): + if not item: + continue + parts = item.split(":", 2) # allow value to contain ":" beyond first two separators + if len(parts) < 3: + raise argparse.ArgumentTypeError(f"Bad item '{item}', expected FN:KEY:VALUE") + fn, key, raw_val = parts[0], parts[1], parts[2] + # If you need to allow colons in values, join leftover parts: + # fn, key, raw_val = parts[0], parts[1], ":".join(parts[2:]) + + if not fn: + raise argparse.ArgumentTypeError(f"Bad item '{item}': empty function name") + if not key: + raise argparse.ArgumentTypeError(f"Bad item '{item}': empty key") + + val = parse_scalar(raw_val) + bucket = result.setdefault(fn, {}) + bucket[key] = val + return result diff --git a/src/depth_anything_3/utils/camera_trj_helpers.py b/src/depth_anything_3/utils/camera_trj_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..83624f359553908abd28af2d59f2066a7f7a7b15 --- /dev/null +++ b/src/depth_anything_3/utils/camera_trj_helpers.py @@ -0,0 +1,479 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from einops import einsum, rearrange, reduce + +try: + from scipy.spatial.transform import Rotation as R +except ImportError: + from depth_anything_3.utils.logger import logger + + logger.warn("Dependency 'scipy' not found. Required for interpolating camera trajectory.") + +from depth_anything_3.utils.geometry import as_homogeneous + + +@torch.no_grad() +def render_stabilization_path(poses, k_size=45): + """Rendering stabilized camera path. + poses: [batch, 4, 4] or [batch, 3, 4], + return: + smooth path: [batch 4 4]""" + num_frames = poses.shape[0] + device = poses.device + dtype = poses.dtype + + # Early exit for trivial cases + if num_frames <= 1: + return as_homogeneous(poses) + + # Make k_size safe: positive odd and not larger than num_frames + # 1) Ensure odd + if k_size < 1: + k_size = 1 + if k_size % 2 == 0: + k_size += 1 + # 2) Cap to num_frames (keep odd) + max_odd = num_frames if (num_frames % 2 == 1) else (num_frames - 1) + if max_odd < 1: + max_odd = 1 # covers num_frames == 0 theoretically + k_size = min(k_size, max_odd) + # 3) enforce a minimum of 3 when possible (for better smoothing) + if num_frames >= 3 and k_size < 3: + k_size = 3 + + input_poses = [] + for i in range(num_frames): + input_poses.append( + torch.cat([poses[i, :3, 0:1], poses[i, :3, 1:2], poses[i, :3, 3:4]], dim=-1) + ) + input_poses = torch.stack(input_poses) # (num_frames, 3, 3) + + # Prepare Gaussian kernel + gaussian_kernel = cv2.getGaussianKernel(ksize=k_size, sigma=-1).astype(np.float32).squeeze() + gaussian_kernel = torch.tensor(gaussian_kernel, dtype=dtype, device=device).view(1, 1, -1) + pad = k_size // 2 + + output_vectors = [] + for idx in range(3): # For r1, r2, t + vec = ( + input_poses[:, :, idx].T.unsqueeze(0).unsqueeze(0) + ) # (1, 1, 3, num_frames) -> (1, 1, 3, num_frames) + # But actually, we want (batch=3, channel=1, width=num_frames) + # So: + vec = input_poses[:, :, idx].T.unsqueeze(1) # (3, 1, num_frames) + vec_padded = F.pad(vec, (pad, pad), mode="reflect") + filtered = F.conv1d(vec_padded, gaussian_kernel) + output_vectors.append(filtered.squeeze(1).T) # (num_frames, 3) + + output_r1, output_r2, output_t = output_vectors # Each is (num_frames, 3) + + # Normalize r1 and r2 + output_r1 = output_r1 / output_r1.norm(dim=-1, keepdim=True) + output_r2 = output_r2 / output_r2.norm(dim=-1, keepdim=True) + + output_poses = [] + for i in range(num_frames): + output_r3 = torch.linalg.cross(output_r1[i], output_r2[i]) + render_pose = torch.cat( + [ + output_r1[i].unsqueeze(-1), + output_r2[i].unsqueeze(-1), + output_r3.unsqueeze(-1), + output_t[i].unsqueeze(-1), + ], + dim=-1, + ) + output_poses.append(render_pose[:3, :]) + output_poses = as_homogeneous(torch.stack(output_poses, dim=0)) + + return output_poses + + +@torch.no_grad() +def render_wander_path( + cam2world: torch.Tensor, + intrinsic: torch.Tensor, + h: int, + w: int, + num_frames: int = 120, + max_disp: float = 48.0, +): + device, dtype = cam2world.device, cam2world.dtype + fx = intrinsic[0, 0] * w + r = max_disp / fx + th = torch.linspace(0, 2.0 * torch.pi, steps=num_frames, device=device, dtype=dtype) + x = r * torch.sin(th) + yz = r * torch.cos(th) / 3.0 + T = torch.eye(4, device=device, dtype=dtype).unsqueeze(0).repeat(num_frames, 1, 1) + T[:, :3, 3] = torch.stack([x, yz, yz], dim=-1) * -1.0 + c2ws = cam2world.unsqueeze(0) @ T + # Start at reference pose and end back at reference pose + c2ws = torch.cat([cam2world.unsqueeze(0), c2ws, cam2world.unsqueeze(0)], dim=0) + Ks = intrinsic.unsqueeze(0).repeat(c2ws.shape[0], 1, 1) + return c2ws, Ks + + +@torch.no_grad() +def render_dolly_zoom_path( + cam2world: torch.Tensor, + intrinsic: torch.Tensor, + h: int, + w: int, + num_frames: int = 120, + max_disp: float = 0.1, + D_focus: float = 10.0, +): + device, dtype = cam2world.device, cam2world.dtype + fx0, fy0 = intrinsic[0, 0] * w, intrinsic[1, 1] * h + t = torch.linspace(0.0, 2.0, steps=num_frames, device=device, dtype=dtype) + z = 0.5 * (1.0 - torch.cos(torch.pi * t)) * max_disp + T = torch.eye(4, device=device, dtype=dtype).unsqueeze(0).repeat(num_frames, 1, 1) + T[:, 2, 3] = -z + c2ws = cam2world.unsqueeze(0) @ T + Df = torch.as_tensor(D_focus, device=device, dtype=dtype) + scale = (Df / (Df + z)).clamp(min=1e-6) + Ks = intrinsic.unsqueeze(0).repeat(num_frames, 1, 1) + Ks[:, 0, 0] = (fx0 * scale) / w + Ks[:, 1, 1] = (fy0 * scale) / h + return c2ws, Ks + + +@torch.no_grad() +def interpolate_intrinsics( + initial: torch.Tensor, # "*#batch 3 3" + final: torch.Tensor, # "*#batch 3 3" + t: torch.Tensor, # " time_step" +) -> torch.Tensor: # "*batch time_step 3 3" + initial = rearrange(initial, "... i j -> ... () i j") + final = rearrange(final, "... i j -> ... () i j") + t = rearrange(t, "t -> t () ()") + return initial + (final - initial) * t + + +def intersect_rays( + a_origins: torch.Tensor, # "*#batch dim" + a_directions: torch.Tensor, # "*#batch dim" + b_origins: torch.Tensor, # "*#batch dim" + b_directions: torch.Tensor, # "*#batch dim" +) -> torch.Tensor: # "*batch dim" + """Compute the least-squares intersection of rays. Uses the math from here: + https://math.stackexchange.com/a/1762491/286022 + """ + + # Broadcast and stack the tensors. + a_origins, a_directions, b_origins, b_directions = torch.broadcast_tensors( + a_origins, a_directions, b_origins, b_directions + ) + origins = torch.stack((a_origins, b_origins), dim=-2) + directions = torch.stack((a_directions, b_directions), dim=-2) + + # Compute n_i * n_i^T - eye(3) from the equation. + n = einsum(directions, directions, "... n i, ... n j -> ... n i j") + n = n - torch.eye(3, dtype=origins.dtype, device=origins.device) + + # Compute the left-hand side of the equation. + lhs = reduce(n, "... n i j -> ... i j", "sum") + + # Compute the right-hand side of the equation. + rhs = einsum(n, origins, "... n i j, ... n j -> ... n i") + rhs = reduce(rhs, "... n i -> ... i", "sum") + + # Left-matrix-multiply both sides by the inverse of lhs to find p. + return torch.linalg.lstsq(lhs, rhs).solution + + +def normalize(a: torch.Tensor) -> torch.Tensor: # "*#batch dim" -> "*#batch dim" + return a / a.norm(dim=-1, keepdim=True) + + +def generate_coordinate_frame( + y: torch.Tensor, # "*#batch 3" + z: torch.Tensor, # "*#batch 3" +) -> torch.Tensor: # "*batch 3 3" + """Generate a coordinate frame given perpendicular, unit-length Y and Z vectors.""" + y, z = torch.broadcast_tensors(y, z) + return torch.stack([y.cross(z, dim=-1), y, z], dim=-1) + + +def generate_rotation_coordinate_frame( + a: torch.Tensor, # "*#batch 3" + b: torch.Tensor, # "*#batch 3" + eps: float = 1e-4, +) -> torch.Tensor: # "*batch 3 3" + """Generate a coordinate frame where the Y direction is normal to the plane defined + by unit vectors a and b. The other axes are arbitrary.""" + device = a.device + + # Replace every entry in b that's parallel to the corresponding entry in a with an + # arbitrary vector. + b = b.detach().clone() + parallel = (einsum(a, b, "... i, ... i -> ...").abs() - 1).abs() < eps + b[parallel] = torch.tensor([0, 0, 1], dtype=b.dtype, device=device) + parallel = (einsum(a, b, "... i, ... i -> ...").abs() - 1).abs() < eps + b[parallel] = torch.tensor([0, 1, 0], dtype=b.dtype, device=device) + + # Generate the coordinate frame. The initial cross product defines the plane. + return generate_coordinate_frame(normalize(torch.linalg.cross(a, b)), a) + + +def matrix_to_euler( + rotations: torch.Tensor, # "*batch 3 3" + pattern: str, +) -> torch.Tensor: # "*batch 3" + *batch, _, _ = rotations.shape + rotations = rotations.reshape(-1, 3, 3) + angles_np = R.from_matrix(rotations.detach().cpu().numpy()).as_euler(pattern) + rotations = torch.tensor(angles_np, dtype=rotations.dtype, device=rotations.device) + return rotations.reshape(*batch, 3) + + +def euler_to_matrix( + rotations: torch.Tensor, # "*batch 3" + pattern: str, +) -> torch.Tensor: # "*batch 3 3" + *batch, _ = rotations.shape + rotations = rotations.reshape(-1, 3) + matrix_np = R.from_euler(pattern, rotations.detach().cpu().numpy()).as_matrix() + rotations = torch.tensor(matrix_np, dtype=rotations.dtype, device=rotations.device) + return rotations.reshape(*batch, 3, 3) + + +def extrinsics_to_pivot_parameters( + extrinsics: torch.Tensor, # "*#batch 4 4" + pivot_coordinate_frame: torch.Tensor, # "*#batch 3 3" + pivot_point: torch.Tensor, # "*#batch 3" +) -> torch.Tensor: # "*batch 5" + """Convert the extrinsics to a representation with 5 degrees of freedom: + 1. Distance from pivot point in the "X" (look cross pivot axis) direction. + 2. Distance from pivot point in the "Y" (pivot axis) direction. + 3. Distance from pivot point in the Z (look) direction + 4. Angle in plane + 5. Twist (rotation not in plane) + """ + + # The pivot coordinate frame's Z axis is normal to the plane. + pivot_axis = pivot_coordinate_frame[..., :, 1] + + # Compute the translation elements of the pivot parametrization. + translation_frame = generate_coordinate_frame(pivot_axis, extrinsics[..., :3, 2]) + origin = extrinsics[..., :3, 3] + delta = pivot_point - origin + translation = einsum(translation_frame, delta, "... i j, ... i -> ... j") + + # Add the rotation elements of the pivot parametrization. + inverted = pivot_coordinate_frame.inverse() @ extrinsics[..., :3, :3] + y, _, z = matrix_to_euler(inverted, "YXZ").unbind(dim=-1) + + return torch.cat([translation, y[..., None], z[..., None]], dim=-1) + + +def pivot_parameters_to_extrinsics( + parameters: torch.Tensor, # "*#batch 5" + pivot_coordinate_frame: torch.Tensor, # "*#batch 3 3" + pivot_point: torch.Tensor, # "*#batch 3" +) -> torch.Tensor: # "*batch 4 4" + translation, y, z = parameters.split((3, 1, 1), dim=-1) + + euler = torch.cat((y, torch.zeros_like(y), z), dim=-1) + rotation = pivot_coordinate_frame @ euler_to_matrix(euler, "YXZ") + + # The pivot coordinate frame's Z axis is normal to the plane. + pivot_axis = pivot_coordinate_frame[..., :, 1] + + translation_frame = generate_coordinate_frame(pivot_axis, rotation[..., :3, 2]) + delta = einsum(translation_frame, translation, "... i j, ... j -> ... i") + origin = pivot_point - delta + + *batch, _ = origin.shape + extrinsics = torch.eye(4, dtype=parameters.dtype, device=parameters.device) + extrinsics = extrinsics.broadcast_to((*batch, 4, 4)).clone() + extrinsics[..., 3, 3] = 1 + extrinsics[..., :3, :3] = rotation + extrinsics[..., :3, 3] = origin + return extrinsics + + +def interpolate_circular( + a: torch.Tensor, # "*#batch" + b: torch.Tensor, # "*#batch" + t: torch.Tensor, # "*#batch" +) -> torch.Tensor: # " *batch" + a, b, t = torch.broadcast_tensors(a, b, t) + + tau = 2 * torch.pi + a = a % tau + b = b % tau + + # Consider piecewise edge cases. + d = (b - a).abs() + a_left = a - tau + d_left = (b - a_left).abs() + a_right = a + tau + d_right = (b - a_right).abs() + use_d = (d < d_left) & (d < d_right) + use_d_left = (d_left < d_right) & (~use_d) + use_d_right = (~use_d) & (~use_d_left) + + result = a + (b - a) * t + result[use_d_left] = (a_left + (b - a_left) * t)[use_d_left] + result[use_d_right] = (a_right + (b - a_right) * t)[use_d_right] + + return result + + +def interpolate_pivot_parameters( + initial: torch.Tensor, # "*#batch 5" + final: torch.Tensor, # "*#batch 5" + t: torch.Tensor, # " time_step" +) -> torch.Tensor: # "*batch time_step 5" + initial = rearrange(initial, "... d -> ... () d") + final = rearrange(final, "... d -> ... () d") + t = rearrange(t, "t -> t ()") + ti, ri = initial.split((3, 2), dim=-1) + tf, rf = final.split((3, 2), dim=-1) + + t_lerp = ti + (tf - ti) * t + r_lerp = interpolate_circular(ri, rf, t) + + return torch.cat((t_lerp, r_lerp), dim=-1) + + +@torch.no_grad() +def interpolate_extrinsics( + initial: torch.Tensor, # "*#batch 4 4" + final: torch.Tensor, # "*#batch 4 4" + t: torch.Tensor, # " time_step" + eps: float = 1e-4, +) -> torch.Tensor: # "*batch time_step 4 4" + """Interpolate extrinsics by rotating around their "focus point," which is the + least-squares intersection between the look vectors of the initial and final + extrinsics. + """ + + initial = initial.type(torch.float64) + final = final.type(torch.float64) + t = t.type(torch.float64) + + # Based on the dot product between the look vectors, pick from one of two cases: + # 1. Look vectors are parallel: interpolate about their origins' midpoint. + # 3. Look vectors aren't parallel: interpolate about their focus point. + initial_look = initial[..., :3, 2] + final_look = final[..., :3, 2] + dot_products = einsum(initial_look, final_look, "... i, ... i -> ...") + parallel_mask = (dot_products.abs() - 1).abs() < eps + + # Pick focus points. + initial_origin = initial[..., :3, 3] + final_origin = final[..., :3, 3] + pivot_point = 0.5 * (initial_origin + final_origin) + pivot_point[~parallel_mask] = intersect_rays( + initial_origin[~parallel_mask], + initial_look[~parallel_mask], + final_origin[~parallel_mask], + final_look[~parallel_mask], + ) + + # Convert to pivot parameters. + pivot_frame = generate_rotation_coordinate_frame(initial_look, final_look, eps=eps) + initial_params = extrinsics_to_pivot_parameters(initial, pivot_frame, pivot_point) + final_params = extrinsics_to_pivot_parameters(final, pivot_frame, pivot_point) + + # Interpolate the pivot parameters. + interpolated_params = interpolate_pivot_parameters(initial_params, final_params, t) + + # Convert back. + return pivot_parameters_to_extrinsics( + interpolated_params.type(torch.float32), + rearrange(pivot_frame, "... i j -> ... () i j").type(torch.float32), + rearrange(pivot_point, "... xyz -> ... () xyz").type(torch.float32), + ) + + +@torch.no_grad() +def generate_wobble_transformation( + radius: torch.Tensor, # "*#batch" + t: torch.Tensor, # " time_step" + num_rotations: int = 1, + scale_radius_with_t: bool = True, +) -> torch.Tensor: # "*batch time_step 4 4"]: + # Generate a translation in the image plane. + tf = torch.eye(4, dtype=torch.float32, device=t.device) + tf = tf.broadcast_to((*radius.shape, t.shape[0], 4, 4)).clone() + radius = radius[..., None] + if scale_radius_with_t: + radius = radius * t + tf[..., 0, 3] = torch.sin(2 * torch.pi * num_rotations * t) * radius + tf[..., 1, 3] = -torch.cos(2 * torch.pi * num_rotations * t) * radius + return tf + + +@torch.no_grad() +def render_wobble_inter_path( + cam2world: torch.Tensor, intr_normed: torch.Tensor, inter_len: int, n_skip: int = 3 +): + """ + cam2world: [batch, 4, 4], + intr_normed: [batch, 3, 3] + """ + frame_per_round = n_skip * inter_len + num_rotations = 1 + + t = torch.linspace(0, 1, frame_per_round, dtype=torch.float32, device=cam2world.device) + # t = (torch.cos(torch.pi * (t + 1)) + 1) / 2 + tgt_c2w_b = [] + tgt_intr_b = [] + for b_idx in range(cam2world.shape[0]): + tgt_c2w = [] + tgt_intr = [] + for cur_idx in range(0, cam2world.shape[1] - n_skip, n_skip): + origin_a = cam2world[b_idx, cur_idx, :3, 3] + origin_b = cam2world[b_idx, cur_idx + n_skip, :3, 3] + delta = (origin_a - origin_b).norm(dim=-1) + if cur_idx == 0: + delta_prev = delta + else: + delta = (delta_prev + delta) / 2 + delta_prev = delta + tf = generate_wobble_transformation( + radius=delta * 0.5, + t=t, + num_rotations=num_rotations, + scale_radius_with_t=False, + ) + cur_extrs = ( + interpolate_extrinsics( + cam2world[b_idx, cur_idx], + cam2world[b_idx, cur_idx + n_skip], + t, + ) + @ tf + ) + tgt_c2w.append(cur_extrs[(0 if cur_idx == 0 else 1) :]) + tgt_intr.append( + interpolate_intrinsics( + intr_normed[b_idx, cur_idx], + intr_normed[b_idx, cur_idx + n_skip], + t, + )[(0 if cur_idx == 0 else 1) :] + ) + tgt_c2w_b.append(torch.cat(tgt_c2w)) + tgt_intr_b.append(torch.cat(tgt_intr)) + tgt_c2w = torch.stack(tgt_c2w_b) # b v 4 4 + tgt_intr = torch.stack(tgt_intr_b) # b v 3 3 + return tgt_c2w, tgt_intr diff --git a/src/depth_anything_3/utils/constants.py b/src/depth_anything_3/utils/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..79c7643b23a6fb86d3eea08dfce43c3c09655ecf --- /dev/null +++ b/src/depth_anything_3/utils/constants.py @@ -0,0 +1,19 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DEFAULT_MODEL = "depth-anything/DA3NESTED-GIANT-LARGE" +DEFAULT_EXPORT_DIR = "workspace/gallery/scene" +DEFAULT_GALLERY_DIR = "workspace/gallery" +DEFAULT_GRADIO_DIR = "workspace/gradio" +THRESH_FOR_REF_SELECTION = 3 diff --git a/src/depth_anything_3/utils/export/__init__.py b/src/depth_anything_3/utils/export/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e3e4c657983b19a75865ad7d3329f9f037f60cd6 --- /dev/null +++ b/src/depth_anything_3/utils/export/__init__.py @@ -0,0 +1,59 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.export.gs import export_to_gs_ply, export_to_gs_video + +from .colmap import export_to_colmap +from .depth_vis import export_to_depth_vis +from .feat_vis import export_to_feat_vis +from .glb import export_to_glb +from .npz import export_to_mini_npz, export_to_npz + + +def export( + prediction: Prediction, + export_format: str, + export_dir: str, + **kwargs, +): + if "-" in export_format: + export_formats = export_format.split("-") + for export_format in export_formats: + export(prediction, export_format, export_dir, **kwargs) + return # Prevent falling through to single-format handling + + if export_format == "glb": + export_to_glb(prediction, export_dir, **kwargs.get(export_format, {})) + elif export_format == "mini_npz": + export_to_mini_npz(prediction, export_dir) + elif export_format == "npz": + export_to_npz(prediction, export_dir) + elif export_format == "feat_vis": + export_to_feat_vis(prediction, export_dir, **kwargs.get(export_format, {})) + elif export_format == "depth_vis": + export_to_depth_vis(prediction, export_dir) + elif export_format == "gs_ply": + export_to_gs_ply(prediction, export_dir, **kwargs.get(export_format, {})) + elif export_format == "gs_video": + export_to_gs_video(prediction, export_dir, **kwargs.get(export_format, {})) + elif export_format == "colmap": + export_to_colmap(prediction, export_dir, **kwargs.get(export_format, {})) + else: + raise ValueError(f"Unsupported export format: {export_format}") + + +__all__ = [ + export, +] diff --git a/src/depth_anything_3/utils/export/colmap.py b/src/depth_anything_3/utils/export/colmap.py new file mode 100644 index 0000000000000000000000000000000000000000..cf125e8d456e832a0ebeb69c186cefd38d617d93 --- /dev/null +++ b/src/depth_anything_3/utils/export/colmap.py @@ -0,0 +1,149 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import numpy as np +import pycolmap +from PIL import Image + +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.logger import logger + +from .glb import _depths_to_world_points_with_colors + + +def export_to_colmap( + prediction: Prediction, + export_dir: str, + image_paths: list[str], + conf_thresh_percentile: float = 40.0, + process_res_method: str = "upper_bound_resize", +) -> None: + # 1. Data preparation + conf_thresh = np.percentile(prediction.conf, conf_thresh_percentile) + points, colors = _depths_to_world_points_with_colors( + prediction.depth, + prediction.intrinsics, + prediction.extrinsics, # w2c + prediction.processed_images, + prediction.conf, + conf_thresh, + ) + num_points = len(points) + logger.info(f"Exporting to COLMAP with {num_points} points") + num_frames = len(prediction.processed_images) + h, w = prediction.processed_images.shape[1:3] + points_xyf = _create_xyf(num_frames, h, w) + points_xyf = points_xyf[prediction.conf >= conf_thresh] + + # 2. Set Reconstruction + reconstruction = pycolmap.Reconstruction() + + point3d_ids = [] + for vidx in range(num_points): + point3d_id = reconstruction.add_point3D(points[vidx], pycolmap.Track(), colors[vidx]) + point3d_ids.append(point3d_id) + + for fidx in range(num_frames): + orig_w, orig_h = Image.open(image_paths[fidx]).size + + intrinsic = prediction.intrinsics[fidx] + if process_res_method.endswith("resize"): + intrinsic[:1] *= orig_w / w + intrinsic[1:2] *= orig_h / h + elif process_res_method == "crop": + raise NotImplementedError("COLMAP export for crop method is not implemented") + else: + raise ValueError(f"Unknown process_res_method: {process_res_method}") + + pycolmap_intri = np.array( + [intrinsic[0, 0], intrinsic[1, 1], intrinsic[0, 2], intrinsic[1, 2]] + ) + + extrinsic = prediction.extrinsics[fidx] + cam_from_world = pycolmap.Rigid3d(pycolmap.Rotation3d(extrinsic[:3, :3]), extrinsic[:3, 3]) + + # set and add camera + camera = pycolmap.Camera() + camera.camera_id = fidx + 1 + camera.model = pycolmap.CameraModelId.PINHOLE + camera.width = orig_w + camera.height = orig_h + camera.params = pycolmap_intri + reconstruction.add_camera(camera) + + # set and add rig (from camera) + rig = pycolmap.Rig() + rig.rig_id = camera.camera_id + rig.add_ref_sensor(camera.sensor_id) + reconstruction.add_rig(rig) + + # set image + image = pycolmap.Image() + image.image_id = fidx + 1 + image.camera_id = camera.camera_id + + # set and add frame (from image) + frame = pycolmap.Frame() + frame.frame_id = image.image_id + frame.rig_id = camera.camera_id + frame.add_data_id(image.data_id) + frame.rig_from_world = cam_from_world + reconstruction.add_frame(frame) + + # set point2d and update track + point2d_list = [] + points_in_frame = points_xyf[:, 2].astype(np.int32) == fidx + for vidx in np.where(points_in_frame)[0]: + point2d = points_xyf[vidx][:2] + point2d[0] *= orig_w / w + point2d[1] *= orig_h / h + point3d_id = point3d_ids[vidx] + point2d_list.append(pycolmap.Point2D(point2d, point3d_id)) + reconstruction.point3D(point3d_id).track.add_element( + image.image_id, len(point2d_list) - 1 + ) + + # set and add image + image.frame_id = image.image_id + image.name = os.path.basename(image_paths[fidx]) + image.points2D = pycolmap.Point2DList(point2d_list) + reconstruction.add_image(image) + + # 3. Export + reconstruction.write(export_dir) + + +def _create_xyf(num_frames, height, width): + """ + Creates a grid of pixel coordinates and frame indices (fidx) for all frames. + """ + # Create coordinate grids for a single frame + y_grid, x_grid = np.indices((height, width), dtype=np.int32) + x_grid = x_grid[np.newaxis, :, :] + y_grid = y_grid[np.newaxis, :, :] + + # Broadcast to all frames + x_coords = np.broadcast_to(x_grid, (num_frames, height, width)) + y_coords = np.broadcast_to(y_grid, (num_frames, height, width)) + + # Create frame indices and broadcast + f_idx = np.arange(num_frames, dtype=np.int32)[:, np.newaxis, np.newaxis] + f_coords = np.broadcast_to(f_idx, (num_frames, height, width)) + + # Stack coordinates and frame indices + points_xyf = np.stack((x_coords, y_coords, f_coords), axis=-1) + + return points_xyf diff --git a/src/depth_anything_3/utils/export/depth_vis.py b/src/depth_anything_3/utils/export/depth_vis.py new file mode 100644 index 0000000000000000000000000000000000000000..b10ff738602a9521ac51b3d5ec4462c44bdf852f --- /dev/null +++ b/src/depth_anything_3/utils/export/depth_vis.py @@ -0,0 +1,42 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import imageio +import numpy as np + +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.visualize import visualize_depth + + +def export_to_depth_vis( + prediction: Prediction, + export_dir: str, +): + # Use prediction.processed_images, which is already processed image data + if prediction.processed_images is None: + raise ValueError("prediction.processed_images is required but not available") + + images_u8 = prediction.processed_images # (N,H,W,3) uint8 + + os.makedirs(os.path.join(export_dir, "depth_vis"), exist_ok=True) + for idx in range(prediction.depth.shape[0]): + depth_vis = visualize_depth(prediction.depth[idx]) + image_vis = images_u8[idx] + depth_vis = depth_vis.astype(np.uint8) + image_vis = image_vis.astype(np.uint8) + vis_image = np.concatenate([image_vis, depth_vis], axis=1) + save_path = os.path.join(export_dir, f"depth_vis/{idx:04d}.jpg") + imageio.imwrite(save_path, vis_image, quality=95) diff --git a/src/depth_anything_3/utils/export/feat_vis.py b/src/depth_anything_3/utils/export/feat_vis.py new file mode 100644 index 0000000000000000000000000000000000000000..26a811073cb45a8f6b87143f80085fb1fa97f63a --- /dev/null +++ b/src/depth_anything_3/utils/export/feat_vis.py @@ -0,0 +1,66 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import cv2 +import imageio +import numpy as np +from tqdm.auto import tqdm + +from depth_anything_3.utils.parallel_utils import async_call +from depth_anything_3.utils.pca_utils import PCARGBVisualizer + + +@async_call +def export_to_feat_vis( + prediction, + export_dir, + fps=15, +): + """Export feature visualization with PCA. + + Args: + prediction: Model prediction containing feature maps + export_dir: Directory to export results + fps: Frame rate for output video (default: 15) + """ + out_dir = os.path.join(export_dir, "feat_vis") + os.makedirs(out_dir, exist_ok=True) + + images = prediction.processed_images + for k, v in prediction.aux.items(): + if not k.startswith("feat_layer_"): + continue + os.makedirs(os.path.join(out_dir, k), exist_ok=True) + viz = PCARGBVisualizer(basis_mode="fixed", percentile_mode="global", clip_percent=10.0) + viz.fit_reference(v) + feats_vis = viz.transform_video(v) + for idx in tqdm(range(len(feats_vis))): + img = images[idx] + feat_vis = (feats_vis[idx] * 255).astype(np.uint8) + feat_vis = cv2.resize( + feat_vis, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_NEAREST + ) + save_path = os.path.join(out_dir, f"{k}/{idx:06d}.jpg") + save = np.concatenate([img, feat_vis], axis=1) + imageio.imwrite(save_path, save, quality=95) + cmd = ( + "ffmpeg -loglevel error -hide_banner -y " + f"-framerate {fps} -start_number 0 " + f"-i {out_dir}/{k}/%06d.jpg " + f"-c:v libx264 -pix_fmt yuv420p " + f"{out_dir}/{k}.mp4" + ) + os.system(cmd) diff --git a/src/depth_anything_3/utils/export/glb.py b/src/depth_anything_3/utils/export/glb.py new file mode 100644 index 0000000000000000000000000000000000000000..2f153fa145e67d9687556f21997149beb8df0501 --- /dev/null +++ b/src/depth_anything_3/utils/export/glb.py @@ -0,0 +1,433 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os + +import numpy as np +import trimesh + +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.logger import logger + +from .depth_vis import export_to_depth_vis + + +def set_sky_depth(prediction: Prediction, sky_mask: np.ndarray, sky_depth_def: float = 98.0): + non_sky_mask = ~sky_mask + valid_depth = prediction.depth[non_sky_mask] + if valid_depth.size > 0: + max_depth = np.percentile(valid_depth, sky_depth_def) + prediction.depth[sky_mask] = max_depth + + +def get_conf_thresh( + prediction: Prediction, + sky_mask: np.ndarray, + conf_thresh: float, + conf_thresh_percentile: float = 10.0, + ensure_thresh_percentile: float = 90.0, +): + if sky_mask is not None and (~sky_mask).sum() > 10: + conf_pixels = prediction.conf[~sky_mask] + else: + conf_pixels = prediction.conf + lower = np.percentile(conf_pixels, conf_thresh_percentile) + upper = np.percentile(conf_pixels, ensure_thresh_percentile) + conf_thresh = min(max(conf_thresh, lower), upper) + return conf_thresh + + +def export_to_glb( + prediction: Prediction, + export_dir: str, + num_max_points: int = 1_000_000, + conf_thresh: float = 1.05, + filter_black_bg: bool = False, + filter_white_bg: bool = False, + conf_thresh_percentile: float = 40.0, + ensure_thresh_percentile: float = 90.0, + sky_depth_def: float = 98.0, + show_cameras: bool = True, + camera_size: float = 0.03, + export_depth_vis: bool = True, +) -> str: + """Generate a 3D point cloud and camera wireframes and export them as a ``.glb`` file. + + The function builds a point cloud from the predicted depth maps, aligns it to the + first camera in glTF coordinates (X-right, Y-up, Z-backward), optionally draws + camera wireframes, and writes the result to ``scene.glb``. Auxiliary assets such as + depth visualizations can also be generated alongside the main export. + + Args: + prediction: Model prediction containing depth, confidence, intrinsics, extrinsics, + and pre-processed images. + export_dir: Output directory where the glTF assets will be written. + num_max_points: Maximum number of points retained after downsampling. + conf_thresh: Base confidence threshold used before percentile adjustments. + filter_black_bg: Mark near-black background pixels for removal during confidence filtering. + filter_white_bg: Mark near-white background pixels for removal during confidence filtering. + conf_thresh_percentile: Lower percentile used when adapting the confidence threshold. + ensure_thresh_percentile: Upper percentile clamp for the adaptive threshold. + sky_depth_def: Percentile used to fill sky pixels with plausible depth values. + show_cameras: Whether to render camera wireframes in the exported scene. + camera_size: Relative camera wireframe scale as a fraction of the scene diagonal. + export_depth_vis: Whether to export raster depth visualisations alongside the glTF. + + Returns: + Path to the exported ``scene.glb`` file. + """ + # 1) Use prediction.processed_images, which is already processed image data + assert ( + prediction.processed_images is not None + ), "Export to GLB: prediction.processed_images is required but not available" + assert ( + prediction.depth is not None + ), "Export to GLB: prediction.depth is required but not available" + assert ( + prediction.intrinsics is not None + ), "Export to GLB: prediction.intrinsics is required but not available" + assert ( + prediction.extrinsics is not None + ), "Export to GLB: prediction.extrinsics is required but not available" + assert ( + prediction.conf is not None + ), "Export to GLB: prediction.conf is required but not available" + logger.info(f"conf_thresh_percentile: {conf_thresh_percentile}") + logger.info(f"num max points: {num_max_points}") + logger.info(f"Exporting to GLB with num_max_points: {num_max_points}") + if prediction.processed_images is None: + raise ValueError("prediction.processed_images is required but not available") + + images_u8 = prediction.processed_images # (N,H,W,3) uint8 + + # 2) Sky processing (if sky_mask is provided) + if getattr(prediction, "sky_mask", None) is not None: + set_sky_depth(prediction, prediction.sky_mask, sky_depth_def) + + # 3) Confidence threshold (if no conf, then no filtering) + if filter_black_bg: + prediction.conf[(prediction.processed_images < 16).all(axis=-1)] = 1.0 + if filter_white_bg: + prediction.conf[(prediction.processed_images >= 240).all(axis=-1)] = 1.0 + conf_thr = get_conf_thresh( + prediction, + getattr(prediction, "sky_mask", None), + conf_thresh, + conf_thresh_percentile, + ensure_thresh_percentile, + ) + + # 4) Back-project to world coordinates and get colors (world frame) + points, colors = _depths_to_world_points_with_colors( + prediction.depth, + prediction.intrinsics, + prediction.extrinsics, # w2c + images_u8, + prediction.conf, + conf_thr, + ) + + # 5) Based on first camera orientation + glTF axis system, center by point cloud, + # construct alignment transform, and apply to point cloud + A = _compute_alignment_transform_first_cam_glTF_center_by_points( + prediction.extrinsics[0], points + ) # (4,4) + + if points.shape[0] > 0: + points = trimesh.transform_points(points, A) + + # 6) Clean + downsample + points, colors = _filter_and_downsample(points, colors, num_max_points) + + # 7) Assemble scene (add point cloud first) + scene = trimesh.Scene() + if scene.metadata is None: + scene.metadata = {} + scene.metadata["hf_alignment"] = A # For camera wireframes and external reuse + + if points.shape[0] > 0: + pc = trimesh.points.PointCloud(vertices=points, colors=colors) + scene.add_geometry(pc) + + # 8) Draw cameras (wireframe pyramids), using the same transform A + if show_cameras and prediction.intrinsics is not None and prediction.extrinsics is not None: + scene_scale = _estimate_scene_scale(points, fallback=1.0) + H, W = prediction.depth.shape[1:] + _add_cameras_to_scene( + scene=scene, + K=prediction.intrinsics, + ext_w2c=prediction.extrinsics, + image_sizes=[(H, W)] * prediction.depth.shape[0], + scale=scene_scale * camera_size, + ) + + # 9) Export + os.makedirs(export_dir, exist_ok=True) + out_path = os.path.join(export_dir, "scene.glb") + scene.export(out_path) + + if export_depth_vis: + export_to_depth_vis(prediction, export_dir) + os.system(f"cp -r {export_dir}/depth_vis/0000.jpg {export_dir}/scene.jpg") + return out_path + + +# ========================= +# utilities +# ========================= + + +def _as_homogeneous44(ext: np.ndarray) -> np.ndarray: + """ + Accept (4,4) or (3,4) extrinsic parameters, return (4,4) homogeneous matrix. + """ + if ext.shape == (4, 4): + return ext + if ext.shape == (3, 4): + H = np.eye(4, dtype=ext.dtype) + H[:3, :4] = ext + return H + raise ValueError(f"extrinsic must be (4,4) or (3,4), got {ext.shape}") + + +def _depths_to_world_points_with_colors( + depth: np.ndarray, + K: np.ndarray, + ext_w2c: np.ndarray, + images_u8: np.ndarray, + conf: np.ndarray | None, + conf_thr: float, +) -> tuple[np.ndarray, np.ndarray]: + """ + For each frame, transform (u,v,1) through K^{-1} to get rays, + multiply by depth to camera frame, then use (w2c)^{-1} to transform to world frame. + Simultaneously extract colors. + """ + N, H, W = depth.shape + us, vs = np.meshgrid(np.arange(W), np.arange(H)) + ones = np.ones_like(us) + pix = np.stack([us, vs, ones], axis=-1).reshape(-1, 3) # (H*W,3) + + pts_all, col_all = [], [] + + for i in range(N): + d = depth[i] # (H,W) + valid = np.isfinite(d) & (d > 0) + if conf is not None: + valid &= conf[i] >= conf_thr + if not np.any(valid): + continue + + d_flat = d.reshape(-1) + vidx = np.flatnonzero(valid.reshape(-1)) + + K_inv = np.linalg.inv(K[i]) # (3,3) + c2w = np.linalg.inv(_as_homogeneous44(ext_w2c[i])) # (4,4) + + rays = K_inv @ pix[vidx].T # (3,M) + Xc = rays * d_flat[vidx][None, :] # (3,M) + Xc_h = np.vstack([Xc, np.ones((1, Xc.shape[1]))]) + Xw = (c2w @ Xc_h)[:3].T.astype(np.float32) # (M,3) + + cols = images_u8[i].reshape(-1, 3)[vidx].astype(np.uint8) # (M,3) + + pts_all.append(Xw) + col_all.append(cols) + + if len(pts_all) == 0: + return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.uint8) + + return np.concatenate(pts_all, 0), np.concatenate(col_all, 0) + + +def _filter_and_downsample(points: np.ndarray, colors: np.ndarray, num_max: int): + if points.shape[0] == 0: + return points, colors + finite = np.isfinite(points).all(axis=1) + points, colors = points[finite], colors[finite] + if points.shape[0] > num_max: + idx = np.random.choice(points.shape[0], num_max, replace=False) + points, colors = points[idx], colors[idx] + return points, colors + + +def _estimate_scene_scale(points: np.ndarray, fallback: float = 1.0) -> float: + if points.shape[0] < 2: + return fallback + lo = np.percentile(points, 5, axis=0) + hi = np.percentile(points, 95, axis=0) + diag = np.linalg.norm(hi - lo) + return float(diag if np.isfinite(diag) and diag > 0 else fallback) + + +def _compute_alignment_transform_first_cam_glTF_center_by_points( + ext_w2c0: np.ndarray, + points_world: np.ndarray, +) -> np.ndarray: + """Computes the transformation matrix to align the scene with glTF standards. + + This function calculates a 4x4 homogeneous matrix that centers the scene's + point cloud and transforms its coordinate system from the computer vision (CV) + standard to the glTF standard. + + The transformation process involves three main steps: + 1. **Initial Alignment**: Orients the world coordinate system to match the + first camera's view (x-right, y-down, z-forward). + 2. **Coordinate System Conversion**: Converts the CV camera frame to the + glTF frame (x-right, y-up, z-backward) by flipping the Y and Z axes. + 3. **Centering**: Translates the entire scene so that the median of the + point cloud becomes the new origin (0,0,0). + + Returns: + A 4x4 homogeneous transformation matrix (torch.Tensor or np.ndarray) + that applies these transformations. A: X' = A @ [X;1] + """ + + w2c0 = _as_homogeneous44(ext_w2c0).astype(np.float64) + + # CV -> glTF axis transformation + M = np.eye(4, dtype=np.float64) + M[1, 1] = -1.0 # flip Y + M[2, 2] = -1.0 # flip Z + + # Don't center first + A_no_center = M @ w2c0 + + # Calculate point cloud center in new coordinate system (use median to resist outliers) + if points_world.shape[0] > 0: + pts_tmp = trimesh.transform_points(points_world, A_no_center) + center = np.median(pts_tmp, axis=0) + else: + center = np.zeros(3, dtype=np.float64) + + T_center = np.eye(4, dtype=np.float64) + T_center[:3, 3] = -center + + A = T_center @ A_no_center + return A + + +def _add_cameras_to_scene( + scene: trimesh.Scene, + K: np.ndarray, + ext_w2c: np.ndarray, + image_sizes: list[tuple[int, int]], + scale: float, +) -> None: + """Draws camera frustums to visualize their position and orientation. + + This function renders each camera as a wireframe pyramid, originating from + the camera's center and extending to the corners of its imaging plane. + + It reads the 'hf_alignment' metadata from the scene to ensure the + wireframes are correctly aligned with the 3D point cloud. + """ + N = K.shape[0] + if N == 0: + return + + # Alignment matrix consistent with point cloud (use identity matrix if missing) + A = None + try: + A = scene.metadata.get("hf_alignment", None) if scene.metadata else None + except Exception: + A = None + if A is None: + A = np.eye(4, dtype=np.float64) + + for i in range(N): + H, W = image_sizes[i] + segs = _camera_frustum_lines(K[i], ext_w2c[i], W, H, scale) # (8,2,3) world frame + # Apply unified transformation + segs = trimesh.transform_points(segs.reshape(-1, 3), A).reshape(-1, 2, 3) + path = trimesh.load_path(segs) + color = _index_color_rgb(i, N) + if hasattr(path, "colors"): + path.colors = np.tile(color, (len(path.entities), 1)) + scene.add_geometry(path) + + +def _camera_frustum_lines( + K: np.ndarray, ext_w2c: np.ndarray, W: int, H: int, scale: float +) -> np.ndarray: + corners = np.array( + [ + [0, 0, 1.0], + [W - 1, 0, 1.0], + [W - 1, H - 1, 1.0], + [0, H - 1, 1.0], + ], + dtype=float, + ) # (4,3) + + K_inv = np.linalg.inv(K) + c2w = np.linalg.inv(_as_homogeneous44(ext_w2c)) + + # camera center in world + Cw = (c2w @ np.array([0, 0, 0, 1.0]))[:3] + + # rays -> z=1 plane points (camera frame) + rays = (K_inv @ corners.T).T + z = rays[:, 2:3] + z[z == 0] = 1.0 + plane_cam = (rays / z) * scale # (4,3) + + # to world + plane_w = [] + for p in plane_cam: + pw = (c2w @ np.array([p[0], p[1], p[2], 1.0]))[:3] + plane_w.append(pw) + plane_w = np.stack(plane_w, 0) # (4,3) + + segs = [] + # center to corners + for k in range(4): + segs.append(np.stack([Cw, plane_w[k]], 0)) + # rectangle edges + order = [0, 1, 2, 3, 0] + for a, b in zip(order[:-1], order[1:]): + segs.append(np.stack([plane_w[a], plane_w[b]], 0)) + + return np.stack(segs, 0) # (8,2,3) + + +def _index_color_rgb(i: int, n: int) -> np.ndarray: + h = (i + 0.5) / max(n, 1) + s, v = 0.85, 0.95 + r, g, b = _hsv_to_rgb(h, s, v) + return (np.array([r, g, b]) * 255).astype(np.uint8) + + +def _hsv_to_rgb(h: float, s: float, v: float) -> tuple[float, float, float]: + i = int(h * 6.0) + f = h * 6.0 - i + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + i = i % 6 + if i == 0: + r, g, b = v, t, p + elif i == 1: + r, g, b = q, v, p + elif i == 2: + r, g, b = p, v, t + elif i == 3: + r, g, b = p, q, v + elif i == 4: + r, g, b = t, p, v + else: + r, g, b = v, p, q + return r, g, b diff --git a/src/depth_anything_3/utils/export/gs.py b/src/depth_anything_3/utils/export/gs.py new file mode 100644 index 0000000000000000000000000000000000000000..337839a55fdc15d2d10ef22da4bb3185b7279c9c --- /dev/null +++ b/src/depth_anything_3/utils/export/gs.py @@ -0,0 +1,155 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Literal, Optional + +import moviepy.editor as mpy +import torch + +from depth_anything_3.model.utils.gs_renderer import run_renderer_in_chunk_w_trj_mode +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.gsply_helpers import save_gaussian_ply +from depth_anything_3.utils.layout_helpers import hcat, vcat +from depth_anything_3.utils.visualize import vis_depth_map_tensor + +VIDEO_QUALITY_MAP = { + "low": {"crf": "28", "preset": "veryfast"}, + "medium": {"crf": "23", "preset": "medium"}, + "high": {"crf": "18", "preset": "slow"}, +} + + +def export_to_gs_ply( + prediction: Prediction, + export_dir: str, + gs_views_interval: Optional[ + int + ] = 1, # export GS every N views, useful for extremely dense inputs +): + gs_world = prediction.gaussians + pred_depth = torch.from_numpy(prediction.depth).unsqueeze(-1).to(gs_world.means) # v h w 1 + idx = 0 + os.makedirs(os.path.join(export_dir, "gs_ply"), exist_ok=True) + save_path = os.path.join(export_dir, f"gs_ply/{idx:04d}.ply") + if gs_views_interval is None: # select around 12 views in total + gs_views_interval = max(pred_depth.shape[0] // 12, 1) + save_gaussian_ply( + gaussians=gs_world, + save_path=save_path, + ctx_depth=pred_depth, + shift_and_scale=False, + save_sh_dc_only=True, + gs_views_interval=gs_views_interval, + inv_opacity=True, + prune_by_depth_percent=0.9, + prune_border_gs=True, + match_3dgs_mcmc_dev=False, + ) + + +def export_to_gs_video( + prediction: Prediction, + export_dir: str, + extrinsics: Optional[torch.Tensor] = None, # render views' world2cam, "b v 4 4" + intrinsics: Optional[torch.Tensor] = None, # render views' unnormed intrinsics, "b v 3 3" + out_image_hw: Optional[tuple[int, int]] = None, # render views' resolution, (h, w) + chunk_size: Optional[int] = 4, + trj_mode: Literal[ + "original", + "smooth", + "interpolate", + "interpolate_smooth", + "wander", + "dolly_zoom", + "extend", + "wobble_inter", + ] = "extend", + color_mode: Literal["RGB+D", "RGB+ED"] = "RGB+ED", + vis_depth: Optional[Literal["hcat", "vcat"]] = "hcat", + enable_tqdm: Optional[bool] = True, + output_name: Optional[str] = None, + video_quality: Literal["low", "medium", "high"] = "high", +) -> None: + gs_world = prediction.gaussians + # if target poses are not provided, render the (smooth/interpolate) input poses + if extrinsics is not None: + tgt_extrs = extrinsics + else: + tgt_extrs = torch.from_numpy(prediction.extrinsics).unsqueeze(0).to(gs_world.means) + if prediction.is_metric: + scale_factor = prediction.scale_factor + if scale_factor is not None: + tgt_extrs[:, :, :3, 3] /= scale_factor + tgt_intrs = ( + intrinsics + if intrinsics is not None + else torch.from_numpy(prediction.intrinsics).unsqueeze(0).to(gs_world.means) + ) + # if render resolution is not provided, render the input ones + if out_image_hw is not None: + H, W = out_image_hw + else: + H, W = prediction.depth.shape[-2:] + # if single views, render wander trj + if tgt_extrs.shape[1] <= 1: + trj_mode = "wander" + # trj_mode = "dolly_zoom" + + color, depth = run_renderer_in_chunk_w_trj_mode( + gaussians=gs_world, + extrinsics=tgt_extrs, + intrinsics=tgt_intrs, + image_shape=(H, W), + chunk_size=chunk_size, + trj_mode=trj_mode, + use_sh=True, + color_mode=color_mode, + enable_tqdm=enable_tqdm, + ) + + # save as video + ffmpeg_params = [ + "-crf", + VIDEO_QUALITY_MAP[video_quality]["crf"], + "-preset", + VIDEO_QUALITY_MAP[video_quality]["preset"], + "-pix_fmt", + "yuv420p", + ] # best compatibility + + os.makedirs(os.path.join(export_dir, "gs_video"), exist_ok=True) + for idx in range(color.shape[0]): + video_i = color[idx] + if vis_depth is not None: + depth_i = vis_depth_map_tensor(depth[0]) + cat_fn = hcat if vis_depth == "hcat" else vcat + video_i = torch.stack([cat_fn(c, d) for c, d in zip(video_i, depth_i)]) + frames = list( + (video_i.clamp(0, 1) * 255).byte().permute(0, 2, 3, 1).cpu().numpy() + ) # T x H x W x C, uint8, numpy() + + fps = 24 + clip = mpy.ImageSequenceClip(frames, fps=fps) + output_name = f"{idx:04d}_{trj_mode}" if output_name is None else output_name + save_path = os.path.join(export_dir, f"gs_video/{output_name}.mp4") + # clip.write_videofile(save_path, codec="libx264", audio=False, bitrate="4000k") + clip.write_videofile( + save_path, + codec="libx264", + audio=False, + fps=fps, + ffmpeg_params=ffmpeg_params, + ) + return diff --git a/src/depth_anything_3/utils/export/npz.py b/src/depth_anything_3/utils/export/npz.py new file mode 100644 index 0000000000000000000000000000000000000000..f9ce04b0e68330a4ac05d1a9395731225810a7e0 --- /dev/null +++ b/src/depth_anything_3/utils/export/npz.py @@ -0,0 +1,74 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import numpy as np + +from depth_anything_3.specs import Prediction +from depth_anything_3.utils.parallel_utils import async_call + + +@async_call +def export_to_npz( + prediction: Prediction, + export_dir: str, +): + output_file = os.path.join(export_dir, "exports", "npz", "results.npz") + os.makedirs(os.path.dirname(output_file), exist_ok=True) + + # Use prediction.processed_images, which is already processed image data + if prediction.processed_images is None: + raise ValueError("prediction.processed_images is required but not available") + + image = prediction.processed_images # (N,H,W,3) uint8 + + # Build save dict with only non-None values + save_dict = { + "image": image, + "depth": np.round(prediction.depth, 6), + } + + if prediction.conf is not None: + save_dict["conf"] = np.round(prediction.conf, 2) + if prediction.extrinsics is not None: + save_dict["extrinsics"] = prediction.extrinsics + if prediction.intrinsics is not None: + save_dict["intrinsics"] = prediction.intrinsics + + # aux = {k: np.round(v, 4) for k, v in prediction.aux.items()} + np.savez_compressed(output_file, **save_dict) + + +@async_call +def export_to_mini_npz( + prediction: Prediction, + export_dir: str, +): + output_file = os.path.join(export_dir, "exports", "mini_npz", "results.npz") + os.makedirs(os.path.dirname(output_file), exist_ok=True) + + # Build save dict with only non-None values + save_dict = { + "depth": np.round(prediction.depth, 6), + } + + if prediction.conf is not None: + save_dict["conf"] = np.round(prediction.conf, 2) + if prediction.extrinsics is not None: + save_dict["extrinsics"] = prediction.extrinsics + if prediction.intrinsics is not None: + save_dict["intrinsics"] = prediction.intrinsics + + np.savez_compressed(output_file, **save_dict) diff --git a/src/depth_anything_3/utils/export/utils.py b/src/depth_anything_3/utils/export/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..81f45fb563ce595bf547bebe829c9b83eb175f1c --- /dev/null +++ b/src/depth_anything_3/utils/export/utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch + + +def _denorm_and_to_uint8(image_tensor: torch.Tensor) -> np.ndarray: + """Denormalize to [0,255] and output (N, H, W, 3) uint8.""" + resnet_mean = torch.tensor( + [0.485, 0.456, 0.406], dtype=image_tensor.dtype, device=image_tensor.device + ) + resnet_std = torch.tensor( + [0.229, 0.224, 0.225], dtype=image_tensor.dtype, device=image_tensor.device + ) + img = image_tensor * resnet_std[None, :, None, None] + resnet_mean[None, :, None, None] + img = torch.clamp(img, 0.0, 1.0) + img = (img.permute(0, 2, 3, 1).cpu().numpy() * 255.0).round().astype(np.uint8) # (N,H,W,3) + return img diff --git a/src/depth_anything_3/utils/geometry.py b/src/depth_anything_3/utils/geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..bef7aaf773fb0e5218e08a7832d3abd417ea9cb6 --- /dev/null +++ b/src/depth_anything_3/utils/geometry.py @@ -0,0 +1,499 @@ +# flake8: noqa: F722 +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from types import SimpleNamespace +from typing import Optional + +import numpy as np +import torch +import torch.nn.functional as F +from einops import einsum + + +def as_homogeneous(ext): + """ + Accept (..., 3,4) or (..., 4,4) extrinsics, return (...,4,4) homogeneous matrix. + Supports torch.Tensor or np.ndarray. + """ + if isinstance(ext, torch.Tensor): + # If already in homogeneous form + if ext.shape[-2:] == (4, 4): + return ext + elif ext.shape[-2:] == (3, 4): + # Create a new homogeneous matrix + ones = torch.zeros_like(ext[..., :1, :4]) + ones[..., 0, 3] = 1.0 + return torch.cat([ext, ones], dim=-2) + else: + raise ValueError(f"Invalid shape for torch.Tensor: {ext.shape}") + + elif isinstance(ext, np.ndarray): + if ext.shape[-2:] == (4, 4): + return ext + elif ext.shape[-2:] == (3, 4): + ones = np.zeros_like(ext[..., :1, :4]) + ones[..., 0, 3] = 1.0 + return np.concatenate([ext, ones], axis=-2) + else: + raise ValueError(f"Invalid shape for np.ndarray: {ext.shape}") + + else: + raise TypeError("Input must be a torch.Tensor or np.ndarray.") + + +@torch.jit.script +def affine_inverse(A: torch.Tensor): + R = A[..., :3, :3] # ..., 3, 3 + T = A[..., :3, 3:] # ..., 3, 1 + P = A[..., 3:, :] # ..., 1, 4 + return torch.cat([torch.cat([R.mT, -R.mT @ T], dim=-1), P], dim=-2) + + +def transpose_last_two_axes(arr): + """ + for np < 2 + """ + if arr.ndim < 2: + return arr + axes = list(range(arr.ndim)) + # swap the last two + axes[-2], axes[-1] = axes[-1], axes[-2] + return arr.transpose(axes) + + +def affine_inverse_np(A: np.ndarray): + R = A[..., :3, :3] + T = A[..., :3, 3:] + P = A[..., 3:, :] + return np.concatenate( + [ + np.concatenate([transpose_last_two_axes(R), -transpose_last_two_axes(R) @ T], axis=-1), + P, + ], + axis=-2, + ) + + +def quat_to_mat(quaternions: torch.Tensor) -> torch.Tensor: + """ + Quaternion Order: XYZW or say ijkr, scalar-last + + Convert rotations given as quaternions to rotation matrices. + Args: + quaternions: quaternions with real part last, + as tensor of shape (..., 4). + + Returns: + Rotation matrices as tensor of shape (..., 3, 3). + """ + i, j, k, r = torch.unbind(quaternions, -1) + # pyre-fixme[58]: `/` is not supported for operand types `float` and `Tensor`. + two_s = 2.0 / (quaternions * quaternions).sum(-1) + + o = torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + -1, + ) + return o.reshape(quaternions.shape[:-1] + (3, 3)) + + +def mat_to_quat(matrix: torch.Tensor) -> torch.Tensor: + """ + Convert rotations given as rotation matrices to quaternions. + + Args: + matrix: Rotation matrices as tensor of shape (..., 3, 3). + + Returns: + quaternions with real part last, as tensor of shape (..., 4). + Quaternion Order: XYZW or say ijkr, scalar-last + """ + if matrix.size(-1) != 3 or matrix.size(-2) != 3: + raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.") + + batch_dim = matrix.shape[:-2] + m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind( + matrix.reshape(batch_dim + (9,)), dim=-1 + ) + + q_abs = _sqrt_positive_part( + torch.stack( + [ + 1.0 + m00 + m11 + m22, + 1.0 + m00 - m11 - m22, + 1.0 - m00 + m11 - m22, + 1.0 - m00 - m11 + m22, + ], + dim=-1, + ) + ) + + # we produce the desired quaternion multiplied by each of r, i, j, k + quat_by_rijk = torch.stack( + [ + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and + # `int`. + torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1), + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and + # `int`. + torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1), + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and + # `int`. + torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1), + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and + # `int`. + torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1), + ], + dim=-2, + ) + + # We floor here at 0.1 but the exact level is not important; if q_abs is small, + # the candidate won't be picked. + flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device) + quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr)) + + # if not for numerical problems, quat_candidates[i] should be same (up to a sign), + # forall i; we pick the best-conditioned one (with the largest denominator) + out = quat_candidates[F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :].reshape( + batch_dim + (4,) + ) + + # Convert from rijk to ijkr + out = out[..., [1, 2, 3, 0]] + + out = standardize_quaternion(out) + + return out + + +def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor: + """ + Returns torch.sqrt(torch.max(0, x)) + but with a zero subgradient where x is 0. + """ + ret = torch.zeros_like(x) + positive_mask = x > 0 + if torch.is_grad_enabled(): + ret[positive_mask] = torch.sqrt(x[positive_mask]) + else: + ret = torch.where(positive_mask, torch.sqrt(x), ret) + return ret + + +def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor: + """ + Convert a unit quaternion to a standard form: one in which the real + part is non negative. + + Args: + quaternions: Quaternions with real part last, + as tensor of shape (..., 4). + + Returns: + Standardized quaternions as tensor of shape (..., 4). + """ + return torch.where(quaternions[..., 3:4] < 0, -quaternions, quaternions) + + +def sample_image_grid( + shape: tuple[int, ...], + device: torch.device = torch.device("cpu"), +) -> tuple[ + torch.Tensor, # float coordinates (xy indexing), "*shape dim" + torch.Tensor, # integer indices (ij indexing), "*shape dim" +]: + """Get normalized (range 0 to 1) coordinates and integer indices for an image.""" + + # Each entry is a pixel-wise integer coordinate. In the 2D case, each entry is a + # (row, col) coordinate. + indices = [torch.arange(length, device=device) for length in shape] + stacked_indices = torch.stack(torch.meshgrid(*indices, indexing="ij"), dim=-1) + + # Each entry is a floating-point coordinate in the range (0, 1). In the 2D case, + # each entry is an (x, y) coordinate. + coordinates = [(idx + 0.5) / length for idx, length in zip(indices, shape)] + coordinates = reversed(coordinates) + coordinates = torch.stack(torch.meshgrid(*coordinates, indexing="xy"), dim=-1) + + return coordinates, stacked_indices + + +def homogenize_points(points: torch.Tensor) -> torch.Tensor: # "*batch dim" # "*batch dim+1" + """Convert batched points (xyz) to (xyz1).""" + return torch.cat([points, torch.ones_like(points[..., :1])], dim=-1) + + +def homogenize_vectors(vectors: torch.Tensor) -> torch.Tensor: # "*batch dim" # "*batch dim+1" + """Convert batched vectors (xyz) to (xyz0).""" + return torch.cat([vectors, torch.zeros_like(vectors[..., :1])], dim=-1) + + +def transform_rigid( + homogeneous_coordinates: torch.Tensor, # "*#batch dim" + transformation: torch.Tensor, # "*#batch dim dim" +) -> torch.Tensor: # "*batch dim" + """Apply a rigid-body transformation to points or vectors.""" + return einsum( + transformation, + homogeneous_coordinates.to(transformation.dtype), + "... i j, ... j -> ... i", + ) + + +def transform_cam2world( + homogeneous_coordinates: torch.Tensor, # "*#batch dim" + extrinsics: torch.Tensor, # "*#batch dim dim" +) -> torch.Tensor: # "*batch dim" + """Transform points from 3D camera coordinates to 3D world coordinates.""" + return transform_rigid(homogeneous_coordinates, extrinsics) + + +def unproject( + coordinates: torch.Tensor, # "*#batch dim" + z: torch.Tensor, # "*#batch" + intrinsics: torch.Tensor, # "*#batch dim+1 dim+1" +) -> torch.Tensor: # "*batch dim+1" + """Unproject 2D camera coordinates with the given Z values.""" + + # Apply the inverse intrinsics to the coordinates. + coordinates = homogenize_points(coordinates) + ray_directions = einsum( + intrinsics.float().inverse().to(intrinsics), + coordinates.to(intrinsics.dtype), + "... i j, ... j -> ... i", + ) + + # Apply the supplied depth values. + return ray_directions * z[..., None] + + +def get_world_rays( + coordinates: torch.Tensor, # "*#batch dim" + extrinsics: torch.Tensor, # "*#batch dim+2 dim+2" + intrinsics: torch.Tensor, # "*#batch dim+1 dim+1" +) -> tuple[ + torch.Tensor, # origins, "*batch dim+1" + torch.Tensor, # directions, "*batch dim+1" +]: + # Get camera-space ray directions. + directions = unproject( + coordinates, + torch.ones_like(coordinates[..., 0]), + intrinsics, + ) + directions = directions / directions.norm(dim=-1, keepdim=True) + + # Transform ray directions to world coordinates. + directions = homogenize_vectors(directions) + directions = transform_cam2world(directions, extrinsics)[..., :-1] + + # Tile the ray origins to have the same shape as the ray directions. + origins = extrinsics[..., :-1, -1].broadcast_to(directions.shape) + + return origins, directions + + +def get_fov(intrinsics: torch.Tensor) -> torch.Tensor: # "batch 3 3" -> "batch 2" + intrinsics_inv = intrinsics.float().inverse().to(intrinsics) + + def process_vector(vector): + vector = torch.tensor(vector, dtype=intrinsics.dtype, device=intrinsics.device) + vector = einsum(intrinsics_inv, vector, "b i j, j -> b i") + return vector / vector.norm(dim=-1, keepdim=True) + + left = process_vector([0, 0.5, 1]) + right = process_vector([1, 0.5, 1]) + top = process_vector([0.5, 0, 1]) + bottom = process_vector([0.5, 1, 1]) + fov_x = (left * right).sum(dim=-1).acos() + fov_y = (top * bottom).sum(dim=-1).acos() + return torch.stack((fov_x, fov_y), dim=-1) + + +def map_pdf_to_opacity( + pdf: torch.Tensor, # " *batch" + global_step: int = 0, + opacity_mapping: Optional[dict] = None, +) -> torch.Tensor: # " *batch" + # https://www.desmos.com/calculator/opvwti3ba9 + + # Figure out the exponent. + if opacity_mapping is not None: + cfg = SimpleNamespace(**opacity_mapping) + x = cfg.initial + min(global_step / cfg.warm_up, 1) * (cfg.final - cfg.initial) + else: + x = 0.0 + exponent = 2**x + + # Map the probability density to an opacity. + return 0.5 * (1 - (1 - pdf) ** exponent + pdf ** (1 / exponent)) + +def normalize_homogenous_points(points): + """Normalize the point vectors""" + return points / points[..., -1:] + +def inverse_intrinsic_matrix(ixts): + """ """ + return torch.inverse(ixts) + +def pixel_space_to_camera_space(pixel_space_points, depth, intrinsics): + """ + Convert pixel space points to camera space points. + + Args: + pixel_space_points (torch.Tensor): Pixel space points with shape (h, w, 2) + depth (torch.Tensor): Depth map with shape (b, v, h, w, 1) + intrinsics (torch.Tensor): Camera intrinsics with shape (b, v, 3, 3) + + Returns: + torch.Tensor: Camera space points with shape (b, v, h, w, 3). + """ + pixel_space_points = homogenize_points(pixel_space_points) + # camera_space_points = torch.einsum( + # "b v i j , h w j -> b v h w i", intrinsics.inverse(), pixel_space_points + # ) + camera_space_points = torch.einsum( + "b v i j , h w j -> b v h w i", inverse_intrinsic_matrix(intrinsics), pixel_space_points + ) + camera_space_points = camera_space_points * depth + return camera_space_points + + +def camera_space_to_world_space(camera_space_points, c2w): + """ + Convert camera space points to world space points. + + Args: + camera_space_points (torch.Tensor): Camera space points with shape (b, v, h, w, 3) + c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v, 4, 4) + + Returns: + torch.Tensor: World space points with shape (b, v, h, w, 3). + """ + camera_space_points = homogenize_points(camera_space_points) + world_space_points = torch.einsum("b v i j , b v h w j -> b v h w i", c2w, camera_space_points) + return world_space_points[..., :3] + + +def camera_space_to_pixel_space(camera_space_points, intrinsics): + """ + Convert camera space points to pixel space points. + + Args: + camera_space_points (torch.Tensor): Camera space points with shape (b, v1, v2, h, w, 3) + c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v2, 3, 3) + + Returns: + torch.Tensor: World space points with shape (b, v1, v2, h, w, 2). + """ + camera_space_points = normalize_homogenous_points(camera_space_points) + pixel_space_points = torch.einsum( + "b u i j , b v u h w j -> b v u h w i", intrinsics, camera_space_points + ) + return pixel_space_points[..., :2] + + +def world_space_to_camera_space(world_space_points, c2w): + """ + Convert world space points to pixel space points. + + Args: + world_space_points (torch.Tensor): World space points with shape (b, v1, h, w, 3) + c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v2, 4, 4) + + Returns: + torch.Tensor: Camera space points with shape (b, v1, v2, h, w, 3). + """ + world_space_points = homogenize_points(world_space_points) + camera_space_points = torch.einsum( + "b u i j , b v h w j -> b v u h w i", c2w.inverse(), world_space_points + ) + return camera_space_points[..., :3] + + +def unproject_depth( + depth, intrinsics, c2w=None, ixt_normalized=False, num_patches_x=None, num_patches_y=None +): + """ + Turn the depth map into a 3D point cloud in world space + + Args: + depth: (b, v, h, w, 1) + intrinsics: (b, v, 3, 3) + c2w: (b, v, 4, 4) + + Returns: + torch.Tensor: World space points with shape (b, v, h, w, 3). + """ + if c2w is None: + c2w = torch.eye(4, device=depth.device, dtype=depth.dtype) + c2w = c2w[None, None].repeat(depth.shape[0], depth.shape[1], 1, 1) + + if not ixt_normalized: + # Compute indices of pixels + h, w = depth.shape[-3], depth.shape[-2] + x_grid, y_grid = torch.meshgrid( + torch.arange(w, device=depth.device, dtype=depth.dtype), + torch.arange(h, device=depth.device, dtype=depth.dtype), + indexing="xy", + ) # (h, w), (h, w) + else: + # ixt_normalized: h=w=2.0. cx, cy, fx, fy are normalized according to h=w=2.0 + assert num_patches_x is not None and num_patches_y is not None + dx = 1 / num_patches_x + dy = 1 / num_patches_y + max_y = 1 - dy + min_y = -max_y + max_x = 1 - dx + min_x = -max_x + + grid_shift = 1.0 + y_grid, x_grid = torch.meshgrid( + torch.linspace( + min_y + grid_shift, + max_y + grid_shift, + num_patches_y, + dtype=torch.float32, + device=depth.device, + ), + torch.linspace( + min_x + grid_shift, + max_x + grid_shift, + num_patches_x, + dtype=torch.float32, + device=depth.device, + ), + indexing="ij", + ) + + # Compute coordinates of pixels in camera space + pixel_space_points = torch.stack((x_grid, y_grid), dim=-1) # (..., h, w, 2) + camera_points = pixel_space_to_camera_space( + pixel_space_points, depth, intrinsics + ) # (..., h, w, 3) + + # Convert points to world space + world_points = camera_space_to_world_space(camera_points, c2w) # (..., h, w, 3) + + return world_points diff --git a/src/depth_anything_3/utils/gsply_helpers.py b/src/depth_anything_3/utils/gsply_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..bdd753fa26f73f0fa8eb5e03f3190c2470126aaf --- /dev/null +++ b/src/depth_anything_3/utils/gsply_helpers.py @@ -0,0 +1,174 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +from einops import rearrange, repeat +from plyfile import PlyData, PlyElement +from torch import Tensor + +from depth_anything_3.specs import Gaussians + + +def construct_list_of_attributes(num_rest: int) -> list[str]: + attributes = ["x", "y", "z", "nx", "ny", "nz"] + for i in range(3): + attributes.append(f"f_dc_{i}") + for i in range(num_rest): + attributes.append(f"f_rest_{i}") + attributes.append("opacity") + for i in range(3): + attributes.append(f"scale_{i}") + for i in range(4): + attributes.append(f"rot_{i}") + return attributes + + +def export_ply( + means: Tensor, # "gaussian 3" + scales: Tensor, # "gaussian 3" + rotations: Tensor, # "gaussian 4" + harmonics: Tensor, # "gaussian 3 d_sh" + opacities: Tensor, # "gaussian" + path: Path, + shift_and_scale: bool = False, + save_sh_dc_only: bool = True, + match_3dgs_mcmc_dev: Optional[bool] = False, +): + if shift_and_scale: + # Shift the scene so that the median Gaussian is at the origin. + means = means - means.median(dim=0).values + + # Rescale the scene so that most Gaussians are within range [-1, 1]. + scale_factor = means.abs().quantile(0.95, dim=0).max() + means = means / scale_factor + scales = scales / scale_factor + + rotations = rotations.detach().cpu().numpy() + + # Since current model use SH_degree = 4, + # which require large memory to store, we can only save the DC band to save memory. + f_dc = harmonics[..., 0] + f_rest = harmonics[..., 1:].flatten(start_dim=1) + + if match_3dgs_mcmc_dev: + sh_degree = 3 + n_rest = 3 * (sh_degree + 1) ** 2 - 3 + f_rest = repeat( + torch.zeros_like(harmonics[..., :1]), "... i -> ... (n i)", n=(n_rest // 3) + ).flatten(start_dim=1) + dtype_full = [ + (attribute, "f4") + for attribute in construct_list_of_attributes(num_rest=n_rest) + if attribute not in ("nx", "ny", "nz") + ] + else: + dtype_full = [ + (attribute, "f4") + for attribute in construct_list_of_attributes( + 0 if save_sh_dc_only else f_rest.shape[1] + ) + ] + elements = np.empty(means.shape[0], dtype=dtype_full) + attributes = [ + means.detach().cpu().numpy(), + torch.zeros_like(means).detach().cpu().numpy(), + f_dc.detach().cpu().contiguous().numpy(), + f_rest.detach().cpu().contiguous().numpy(), + opacities[..., None].detach().cpu().numpy(), + scales.log().detach().cpu().numpy(), + rotations, + ] + if match_3dgs_mcmc_dev: + attributes.pop(1) # dummy normal is not needed + elif save_sh_dc_only: + attributes.pop(3) # remove f_rest from attributes + + attributes = np.concatenate(attributes, axis=1) + elements[:] = list(map(tuple, attributes)) + path.parent.mkdir(exist_ok=True, parents=True) + PlyData([PlyElement.describe(elements, "vertex")]).write(path) + + +def inverse_sigmoid(x): + return torch.log(x / (1 - x)) + + +def save_gaussian_ply( + gaussians: Gaussians, + save_path: str, + ctx_depth: torch.Tensor, # depth of input views; for getting shape and filtering, "v h w 1" + shift_and_scale: bool = False, + save_sh_dc_only: bool = True, + gs_views_interval: int = 1, + inv_opacity: Optional[bool] = True, + prune_by_depth_percent: Optional[float] = 1.0, + prune_border_gs: Optional[bool] = True, + match_3dgs_mcmc_dev: Optional[bool] = False, +): + b = gaussians.means.shape[0] + assert b == 1, "must set batch_size=1 when exporting 3D gaussians" + src_v, out_h, out_w, _ = ctx_depth.shape + + # extract gs params + world_means = gaussians.means + world_shs = gaussians.harmonics + world_rotations = gaussians.rotations + gs_scales = gaussians.scales + gs_opacities = inverse_sigmoid(gaussians.opacities) if inv_opacity else gaussians.opacities + + # Create a mask to filter the Gaussians. + + # TODO: prune the sky region here + + # throw away Gaussians at the borders, since they're generally of lower quality. + if prune_border_gs: + mask = torch.zeros_like(ctx_depth, dtype=torch.bool) + gstrim_h = int(8 / 256 * out_h) + gstrim_w = int(8 / 256 * out_w) + mask[:, gstrim_h:-gstrim_h, gstrim_w:-gstrim_w, :] = 1 + else: + mask = torch.ones_like(ctx_depth, dtype=torch.bool) + + # trim the far away point based on depth; + if prune_by_depth_percent is not None and prune_by_depth_percent < 1: + in_depths = ctx_depth + d_percentile = torch.quantile( + in_depths.view(in_depths.shape[0], -1), q=prune_by_depth_percent, dim=1 + ).view(-1, 1, 1) + d_mask = (in_depths[..., 0] <= d_percentile).unsqueeze(-1) + mask = mask & d_mask + mask = mask.squeeze(-1) # v h w + + # helper fn, must place after mask + def trim_select_reshape(element): + selected_element = rearrange( + element[0], "(v h w) ... -> v h w ...", v=src_v, h=out_h, w=out_w + ) + selected_element = selected_element[::gs_views_interval][mask[::gs_views_interval]] + return selected_element + + export_ply( + means=trim_select_reshape(world_means), + scales=trim_select_reshape(gs_scales), + rotations=trim_select_reshape(world_rotations), + harmonics=trim_select_reshape(world_shs), + opacities=trim_select_reshape(gs_opacities), + path=Path(save_path), + shift_and_scale=shift_and_scale, + save_sh_dc_only=save_sh_dc_only, + match_3dgs_mcmc_dev=match_3dgs_mcmc_dev, + ) diff --git a/src/depth_anything_3/utils/io/gpu_input_processor.py b/src/depth_anything_3/utils/io/gpu_input_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..93eeff59520b49436d625958e8e590523afaf580 --- /dev/null +++ b/src/depth_anything_3/utils/io/gpu_input_processor.py @@ -0,0 +1,389 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +GPU-accelerated input processor using Kornia. + +This processor eliminates CPU→GPU transfers by performing all preprocessing +operations directly on GPU using Kornia ops (resize, crop, normalize). +Falls back to CPU-based InputProcessor if GPU is unavailable. +""" + +from __future__ import annotations + +import kornia.geometry.transform as K +import numpy as np +import torch +from PIL import Image + +from depth_anything_3.utils.io.input_processor import InputProcessor +from depth_anything_3.utils.logger import logger + + +class GPUInputProcessor(InputProcessor): + """GPU-accelerated preprocessing using Kornia. + + Performs all preprocessing operations on GPU to eliminate CPU→GPU transfer overhead. + Inherits from InputProcessor and overrides key methods with GPU implementations. + + Key differences: + - Loads images to GPU immediately after loading + - Uses Kornia for resize/crop/normalize on GPU + - Only transfers final batch to CPU if needed + + Fallback: + - Automatically falls back to CPU processing if GPU unavailable + - Detects device from first tensor in batch + """ + + def __init__(self, device: str | torch.device | None = None): + """Initialize GPU processor. + + Args: + device: Target device ('cuda', 'mps', 'cpu', or None for auto-detect). + If None, uses cuda if available, else mps if available, else cpu. + + Note: + On MPS (Apple Silicon), GPU preprocessing is automatically disabled because + CPU preprocessing is faster due to optimized OpenCV/Accelerate routines. + The GPU will still be used for model inference where it provides real gains. + """ + super().__init__() + self._device = self._resolve_device(device) + + # MPS: Force CPU preprocessing (benchmarks show CPU is faster on Apple Silicon) + # The overhead of MPS kernel launches + synchronization exceeds the benefit. + # GPU should be reserved for model inference where it provides 5-10x speedup. + if self._device.type == "mps": + self._use_gpu = False + logger.info( + "MPS detected: GPU preprocessing disabled (CPU is faster on Apple Silicon). " + "GPU will be used for model inference only." + ) + elif self._device.type == "cuda": + self._use_gpu = True + logger.info("GPUInputProcessor initialized with device=cuda (NVJPEG enabled)") + else: + self._use_gpu = False + logger.warn( + f"GPUInputProcessor initialized with device={self._device.type}. " + "GPU preprocessing disabled. Consider using InputProcessor instead." + ) + + # Pre-create Kornia normalize transform on GPU + if self._use_gpu: + mean = torch.tensor([0.485, 0.456, 0.406], device=self._device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=self._device).view(1, 3, 1, 1) + self._kornia_mean = mean + self._kornia_std = std + + # ----------------------------- + # Device management + # ----------------------------- + def _resolve_device(self, device: str | torch.device | None) -> torch.device: + """Resolve device string/object to torch.device.""" + if device is None: + if torch.cuda.is_available(): + return torch.device("cuda") + elif torch.backends.mps.is_available(): + return torch.device("mps") + else: + return torch.device("cpu") + if isinstance(device, str): + return torch.device(device) + return device + + @property + def device(self) -> torch.device: + """Current device.""" + return self._device + + @property + def use_gpu(self) -> bool: + """Whether GPU preprocessing is enabled.""" + return self._use_gpu + + # ----------------------------- + # Override: _process_one (GPU path) + # ----------------------------- + def _process_one( + self, + img: np.ndarray | Image.Image | str, + extrinsic: np.ndarray | None = None, + intrinsic: np.ndarray | None = None, + *, + process_res: int, + process_res_method: str, + perform_normalization: bool = True, + ) -> tuple[torch.Tensor, tuple[int, int], np.ndarray | None, np.ndarray | None]: + """Process single image with GPU acceleration. + + If GPU is enabled, performs all operations on GPU. + Otherwise falls back to CPU path from parent class. + """ + if not self._use_gpu: + # Fallback to CPU + return super()._process_one( + img, + extrinsic, + intrinsic, + process_res=process_res, + process_res_method=process_res_method, + perform_normalization=perform_normalization + ) + + orig_w, orig_h = 0, 0 + img_tensor = None + + # Try GPU/Accelerated decoding if input is a file path and device is CUDA or MPS + if isinstance(img, str) and self._device.type in ("cuda", "mps"): + import os + + import torchvision.io + + try: + # Read raw bytes from file + with open(img, "rb") as f: + # Read bytes -> numpy array (uint8) -> torch tensor + file_bytes = torch.from_numpy(np.frombuffer(f.read(), dtype=np.uint8)) + + ext = os.path.splitext(img)[1].lower() + + # 1. CUDA Optimized Path (NVJPEG) + if self._device.type == "cuda" and ext in (".jpg", ".jpeg"): + img_tensor = torchvision.io.decode_jpeg(file_bytes, device=self._device) + + # 2. Generic Path (MPS or non-JPG on CUDA) + # decode_image is generally faster than PIL for loading into tensors + else: + if ext in (".png", ".jpg", ".jpeg"): + # decode_image supports many formats + # We move to device immediately after decoding + img_tensor = torchvision.io.decode_image(img).to(self._device) + else: + # Fallback for exotic formats + img_tensor = None + + if img_tensor is not None: + # Ensure (1, 3, H, W) float32 [0, 1] + if img_tensor.dim() == 3: + img_tensor = img_tensor.unsqueeze(0) # Add batch dim + + _, c, h, w = img_tensor.shape + orig_h, orig_w = h, w + + # Handle RGBA or Grayscale + if c == 4: + img_tensor = img_tensor[:, :3, :, :] + elif c == 1: + img_tensor = img_tensor.repeat(1, 3, 1, 1) + + img_tensor = img_tensor.float() / 255.0 + + except Exception as e: + logger.warn(f"Accelerated decoding failed for {img} on {self._device}, falling back to PIL: {e}") + img_tensor = None + + # Fallback to PIL loading if GPU decoding failed or not applicable + if img_tensor is None: + pil_img = self._load_image(img) + orig_w, orig_h = pil_img.size + img_tensor = self._pil_to_tensor_gpu(pil_img) + + # Boundary resize (on GPU) + img_tensor = self._resize_image_gpu(img_tensor, process_res, process_res_method) + _, _, h_resized, w_resized = img_tensor.shape + intrinsic = self._resize_ixt(intrinsic, orig_w, orig_h, w_resized, h_resized) + + # Enforce divisibility by PATCH_SIZE (on GPU) + if process_res_method.endswith("resize"): + img_tensor = self._make_divisible_by_resize_gpu(img_tensor, self.PATCH_SIZE) + _, _, h_final, w_final = img_tensor.shape + intrinsic = self._resize_ixt(intrinsic, w_resized, h_resized, w_final, h_final) + elif process_res_method.endswith("crop"): + img_tensor = self._make_divisible_by_crop_gpu(img_tensor, self.PATCH_SIZE) + _, _, h_final, w_final = img_tensor.shape + intrinsic = self._crop_ixt(intrinsic, w_resized, h_resized, w_final, h_final) + else: + raise ValueError(f"Unsupported process_res_method: {process_res_method}") + + # Normalize (on GPU) if requested + if perform_normalization: + img_tensor = self._normalize_image_gpu(img_tensor) + + # Remove batch dimension: (1, 3, H, W) → (3, H, W) + img_tensor = img_tensor.squeeze(0) + + return img_tensor, (h_final, w_final), intrinsic, extrinsic + + # ----------------------------- + # GPU preprocessing ops (Kornia) + # ----------------------------- + def _pil_to_tensor_gpu(self, img: Image.Image) -> torch.Tensor: + """Convert PIL Image to GPU tensor (1, 3, H, W) float32 [0,1].""" + # PIL → numpy → torch → GPU + arr = np.array(img) # (H, W, 3) uint8 + tensor = torch.from_numpy(arr).permute(2, 0, 1).float() / 255.0 # (3, H, W) float32 + tensor = tensor.unsqueeze(0).to(self._device) # (1, 3, H, W) on GPU + return tensor + + def _normalize_image_gpu(self, img_tensor: torch.Tensor) -> torch.Tensor: + """Apply ImageNet normalization on GPU. + + Args: + img_tensor: (1, 3, H, W) tensor on GPU + + Returns: + Normalized tensor (1, 3, H, W) + """ + # Manual normalization: (x - mean) / std + return (img_tensor - self._kornia_mean) / self._kornia_std + + def _resize_image_gpu( + self, img_tensor: torch.Tensor, target_size: int, method: str + ) -> torch.Tensor: + """Resize image tensor on GPU. + + Args: + img_tensor: (1, 3, H, W) tensor on GPU + target_size: target size for longest/shortest side + method: resize method string + + Returns: + Resized tensor (1, 3, H', W') + """ + if method in ("upper_bound_resize", "upper_bound_crop"): + return self._resize_longest_side_gpu(img_tensor, target_size) + elif method in ("lower_bound_resize", "lower_bound_crop"): + return self._resize_shortest_side_gpu(img_tensor, target_size) + else: + raise ValueError(f"Unsupported resize method: {method}") + + def _resize_longest_side_gpu( + self, img_tensor: torch.Tensor, target_size: int + ) -> torch.Tensor: + """Resize so longest side = target_size (preserving aspect ratio).""" + _, _, h, w = img_tensor.shape + longest = max(w, h) + if longest == target_size: + return img_tensor + + scale = target_size / float(longest) + new_w = max(1, int(round(w * scale))) + new_h = max(1, int(round(h * scale))) + + # Kornia resize with antialiasing + # Note: MPS doesn't fully support 'area' mode, use 'bilinear' instead + if self._device.type == "mps": + mode = "bilinear" + else: + mode = "bilinear" if scale > 1.0 else "area" + return K.resize(img_tensor, (new_h, new_w), interpolation=mode, antialias=True) + + def _resize_shortest_side_gpu( + self, img_tensor: torch.Tensor, target_size: int + ) -> torch.Tensor: + """Resize so shortest side = target_size (preserving aspect ratio).""" + _, _, h, w = img_tensor.shape + shortest = min(w, h) + if shortest == target_size: + return img_tensor + + scale = target_size / float(shortest) + new_w = max(1, int(round(w * scale))) + new_h = max(1, int(round(h * scale))) + + # Note: MPS doesn't fully support 'area' mode, use 'bilinear' instead + if self._device.type == "mps": + mode = "bilinear" + else: + mode = "bilinear" if scale > 1.0 else "area" + return K.resize(img_tensor, (new_h, new_w), interpolation=mode, antialias=True) + + def _make_divisible_by_crop_gpu( + self, img_tensor: torch.Tensor, patch: int + ) -> torch.Tensor: + """Floor each dimension to nearest multiple of PATCH_SIZE via center crop (GPU).""" + _, _, h, w = img_tensor.shape + new_h = (h // patch) * patch + new_w = (w // patch) * patch + if new_h == h and new_w == w: + return img_tensor + + # Kornia center_crop + return K.center_crop(img_tensor, (new_h, new_w)) + + def _make_divisible_by_resize_gpu( + self, img_tensor: torch.Tensor, patch: int + ) -> torch.Tensor: + """Round each dimension to nearest multiple of PATCH_SIZE via resize (GPU).""" + _, _, h, w = img_tensor.shape + + def nearest_multiple(x: int, p: int) -> int: + down = (x // p) * p + up = down + p + return up if abs(up - x) <= abs(x - down) else down + + new_h = max(patch, nearest_multiple(h, patch)) + new_w = max(patch, nearest_multiple(w, patch)) + if new_h == h and new_w == w: + return img_tensor + + # Note: MPS doesn't fully support 'area' mode, use 'bilinear' instead + if self._device.type == "mps": + mode = "bilinear" + else: + upscale = (new_h > h) or (new_w > w) + mode = "bilinear" if upscale else "area" + return K.resize(img_tensor, (new_h, new_w), interpolation=mode, antialias=True) + + # ----------------------------- + # Override: _unify_batch_shapes (GPU version) + # ----------------------------- + def _unify_batch_shapes( + self, + processed_images: list[torch.Tensor], + out_sizes: list[tuple[int, int]], + out_intrinsics: list[np.ndarray | None], + ) -> tuple[list[torch.Tensor], list[tuple[int, int]], list[np.ndarray | None]]: + """Center-crop all tensors to smallest H, W on GPU.""" + if len(set(out_sizes)) <= 1: + return processed_images, out_sizes, out_intrinsics + + min_h = min(h for h, _ in out_sizes) + min_w = min(w for _, w in out_sizes) + logger.warn( + f"Images in batch have different sizes {out_sizes}; " + f"center-cropping all to smallest ({min_h},{min_w})" + ) + + new_imgs, new_sizes, new_ixts = [], [], [] + for img_t, (H, W), K_np in zip(processed_images, out_sizes, out_intrinsics): + if H == min_h and W == min_w: + new_imgs.append(img_t) + new_sizes.append((min_h, min_w)) + new_ixts.append(K_np) + continue + + # Crop on GPU using Kornia + # img_t is (3, H, W), need (1, 3, H, W) for Kornia + img_t_4d = img_t.unsqueeze(0) + cropped = K.center_crop(img_t_4d, (min_h, min_w)) + new_imgs.append(cropped.squeeze(0)) + new_sizes.append((min_h, min_w)) + + # Adjust intrinsics + if K_np is None: + new_ixts.append(None) + else: + crop_top = max(0, (H - min_h) // 2) + crop_left = max(0, (W - min_w) // 2) + K_adj = K_np.copy() + K_adj[0, 2] -= crop_left + K_adj[1, 2] -= crop_top + new_ixts.append(K_adj) + + return new_imgs, new_sizes, new_ixts + + +# Backward compatibility alias +GPUInputAdapter = GPUInputProcessor diff --git a/src/depth_anything_3/utils/io/input_processor.py b/src/depth_anything_3/utils/io/input_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..ff65567f979ecc6c39ef084b7b4bb4eb7004b386 --- /dev/null +++ b/src/depth_anything_3/utils/io/input_processor.py @@ -0,0 +1,420 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Input processor for Depth Anything 3 (parallelized). + +This version removes the square center-crop step for "*crop" methods (same as your note). +In addition, it parallelizes per-image preprocessing using the provided `parallel_execution`. +""" + +from __future__ import annotations + +import cv2 +import numpy as np +import torch +import torchvision.transforms as T +from PIL import Image + +from depth_anything_3.utils.logger import logger +from depth_anything_3.utils.parallel_utils import parallel_execution + + +class InputProcessor: + """Prepares a batch of images for model inference. + This processor converts a list of image file paths into a single, model-ready + tensor. The processing pipeline is executed in parallel across multiple workers + for efficiency. + + Pipeline: + 1) Load image and convert to RGB + 2) Boundary resize (upper/lower bound, preserving aspect ratio) + 3) Enforce divisibility by PATCH_SIZE: + - "*resize" methods: each dimension is rounded to nearest multiple + (may up/downscale a few px) + - "*crop" methods: each dimension is floored to nearest multiple via center crop + 4) Convert to tensor and apply ImageNet normalization + 5) Stack into (1, N, 3, H, W) + + Parallelization: + - Each image is processed independently in a worker. + - Order of outputs matches the input order. + """ + + NORMALIZE = T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + PATCH_SIZE = 14 + + def __init__(self): + pass + + # ----------------------------- + # Public API + # ----------------------------- + def __call__( + self, + image: list[np.ndarray | Image.Image | str], + extrinsics: np.ndarray | None = None, + intrinsics: np.ndarray | None = None, + process_res: int = 504, + process_res_method: str = "upper_bound_resize", + *, + num_workers: int = 8, + print_progress: bool = False, + sequential: bool | None = None, + desc: str | None = "Preprocess", + perform_normalization: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]: + """ + Returns: + (tensor, extrinsics_list, intrinsics_list) + tensor shape: (1, N, 3, H, W) + If perform_normalization is False, tensor is uint8 (0-255). + If perform_normalization is True, tensor is float32 normalized (ImageNet). + """ + sequential = self._resolve_sequential(sequential, num_workers) + exts_list, ixts_list = self._validate_and_pack_meta(image, extrinsics, intrinsics) + + results = self._run_parallel( + image=image, + exts_list=exts_list, + ixts_list=ixts_list, + process_res=process_res, + process_res_method=process_res_method, + num_workers=num_workers, + print_progress=print_progress, + sequential=sequential, + desc=desc, + perform_normalization=perform_normalization, + ) + + proc_imgs, out_sizes, out_ixts, out_exts = self._unpack_results(results) + proc_imgs, out_sizes, out_ixts = self._unify_batch_shapes(proc_imgs, out_sizes, out_ixts) + + batch_tensor = self._stack_batch(proc_imgs) + + # Zero-copy conversion: torch.from_numpy shares memory with numpy array + # Only works when array is C-contiguous (which np.asarray ensures) + out_exts = ( + torch.from_numpy(np.ascontiguousarray(np.asarray(out_exts))).float() + if out_exts is not None and out_exts[0] is not None + else None + ) + out_ixts = ( + torch.from_numpy(np.ascontiguousarray(np.asarray(out_ixts))).float() + if out_ixts is not None and out_ixts[0] is not None + else None + ) + return (batch_tensor, out_exts, out_ixts) + + @staticmethod + def normalize_tensor(tensor: torch.Tensor, mean: torch.Tensor | list, std: torch.Tensor | list) -> torch.Tensor: + """Normalize a tensor (C, H, W) or (B, C, H, W) with given mean and std. + Expects input tensor to be float32 in range [0, 1]. + """ + if isinstance(mean, list): + mean = torch.tensor(mean, device=tensor.device, dtype=tensor.dtype).view(-1, 1, 1) + if isinstance(std, list): + std = torch.tensor(std, device=tensor.device, dtype=tensor.dtype).view(-1, 1, 1) + + # Ensure dimensions match + if tensor.dim() == 4 and mean.dim() == 3: + mean = mean.unsqueeze(0) + std = std.unsqueeze(0) + + return (tensor - mean) / std + + # ----------------------------- + # __call__ helpers + # ----------------------------- + def _resolve_sequential(self, sequential: bool | None, num_workers: int) -> bool: + return (num_workers <= 1) if sequential is None else sequential + + def _validate_and_pack_meta( + self, + images: list[np.ndarray | Image.Image | str], + extrinsics: np.ndarray | None, + intrinsics: np.ndarray | None, + ) -> tuple[list[np.ndarray | None] | None, list[np.ndarray | None] | None]: + if extrinsics is not None and len(extrinsics) != len(images): + raise ValueError("Length of extrinsics must match images when provided.") + if intrinsics is not None and len(intrinsics) != len(images): + raise ValueError("Length of intrinsics must match images when provided.") + exts_list = [e for e in extrinsics] if extrinsics is not None else None + ixts_list = [k for k in intrinsics] if intrinsics is not None else None + return exts_list, ixts_list + + def _run_parallel( + self, + *, + image: list[np.ndarray | Image.Image | str], + exts_list: list[np.ndarray | None] | None, + ixts_list: list[np.ndarray | None] | None, + process_res: int, + process_res_method: str, + num_workers: int, + print_progress: bool, + sequential: bool, + desc: str | None, + perform_normalization: bool, + ): + results = parallel_execution( + image, + exts_list, + ixts_list, + action=self._process_one, # (img, extrinsic, intrinsic, ...) + num_processes=num_workers, + print_progress=print_progress, + sequential=sequential, + desc=desc, + process_res=process_res, + process_res_method=process_res_method, + perform_normalization=perform_normalization, + ) + if not results: + raise RuntimeError( + "No preprocessing results returned. Check inputs and parallel_execution." + ) + return results + + def _unpack_results(self, results): + """ + results: List[Tuple[torch.Tensor, Tuple[H, W], Optional[np.ndarray], Optional[np.ndarray]]] + -> processed_images, out_sizes, out_intrinsics, out_extrinsics + """ + try: + processed_images, out_sizes, out_intrinsics, out_extrinsics = zip(*results) + except Exception as e: + raise RuntimeError( + "Unexpected results structure from parallel_execution: " + f"{type(results)} / sample: {results[0]}" + ) from e + + return list(processed_images), list(out_sizes), list(out_intrinsics), list(out_extrinsics) + + def _unify_batch_shapes( + self, + processed_images: list[torch.Tensor], + out_sizes: list[tuple[int, int]], + out_intrinsics: list[np.ndarray | None], + ) -> tuple[list[torch.Tensor], list[tuple[int, int]], list[np.ndarray | None]]: + """Center-crop all tensors to the smallest H, W; adjust intrinsics' cx, cy accordingly.""" + if len(set(out_sizes)) <= 1: + return processed_images, out_sizes, out_intrinsics + + min_h = min(h for h, _ in out_sizes) + min_w = min(w for _, w in out_sizes) + logger.warn( + f"Images in batch have different sizes {out_sizes}; " + f"center-cropping all to smallest ({min_h},{min_w})" + ) + + center_crop = T.CenterCrop((min_h, min_w)) + new_imgs, new_sizes, new_ixts = [], [], [] + for img_t, (H, W), K in zip(processed_images, out_sizes, out_intrinsics): + crop_top = max(0, (H - min_h) // 2) + crop_left = max(0, (W - min_w) // 2) + new_imgs.append(center_crop(img_t)) + new_sizes.append((min_h, min_w)) + if K is None: + new_ixts.append(None) + else: + K_adj = K.copy() + K_adj[0, 2] -= crop_left + K_adj[1, 2] -= crop_top + new_ixts.append(K_adj) + return new_imgs, new_sizes, new_ixts + + def _stack_batch(self, processed_images: list[torch.Tensor]) -> torch.Tensor: + return torch.stack(processed_images) + + # ----------------------------- + # Per-item worker + # ----------------------------- + def _process_one( + self, + img: np.ndarray | Image.Image | str, + extrinsic: np.ndarray | None = None, + intrinsic: np.ndarray | None = None, + *, + process_res: int, + process_res_method: str, + perform_normalization: bool = True, + ) -> tuple[torch.Tensor, tuple[int, int], np.ndarray | None, np.ndarray | None]: + # Load & remember original size + pil_img = self._load_image(img) + orig_w, orig_h = pil_img.size + + # Boundary resize + pil_img = self._resize_image(pil_img, process_res, process_res_method) + w, h = pil_img.size + intrinsic = self._resize_ixt(intrinsic, orig_w, orig_h, w, h) + + # Enforce divisibility by PATCH_SIZE + if process_res_method.endswith("resize"): + pil_img = self._make_divisible_by_resize(pil_img, self.PATCH_SIZE) + new_w, new_h = pil_img.size + intrinsic = self._resize_ixt(intrinsic, w, h, new_w, new_h) + w, h = new_w, new_h + elif process_res_method.endswith("crop"): + pil_img = self._make_divisible_by_crop(pil_img, self.PATCH_SIZE) + new_w, new_h = pil_img.size + intrinsic = self._crop_ixt(intrinsic, w, h, new_w, new_h) + w, h = new_w, new_h + else: + raise ValueError(f"Unsupported process_res_method: {process_res_method}") + + if perform_normalization: + # Convert to tensor & normalize + img_tensor = self._normalize_image(pil_img) + else: + # Return uint8 tensor (C, H, W) + # numpy array is H, W, C + arr = np.array(pil_img) + img_tensor = torch.from_numpy(arr).permute(2, 0, 1) + + _, H, W = img_tensor.shape + assert (W, H) == (w, h), "Tensor size mismatch with PIL image size after processing." + + # Return: (img_tensor, (H, W), intrinsic, extrinsic) + return img_tensor, (H, W), intrinsic, extrinsic + + # ----------------------------- + # Intrinsics transforms + # ----------------------------- + def _resize_ixt( + self, + intrinsic: np.ndarray | None, + orig_w: int, + orig_h: int, + w: int, + h: int, + ) -> np.ndarray | None: + if intrinsic is None: + return None + K = intrinsic.copy() + # scale fx, cx by w ratio; fy, cy by h ratio + K[:1] *= w / float(orig_w) + K[1:2] *= h / float(orig_h) + return K + + def _crop_ixt( + self, + intrinsic: np.ndarray | None, + orig_w: int, + orig_h: int, + w: int, + h: int, + ) -> np.ndarray | None: + if intrinsic is None: + return None + K = intrinsic.copy() + crop_h = (orig_h - h) // 2 + crop_w = (orig_w - w) // 2 + K[0, 2] -= crop_w + K[1, 2] -= crop_h + return K + + # ----------------------------- + # I/O & normalization + # ----------------------------- + def _load_image(self, img: np.ndarray | Image.Image | str) -> Image.Image: + if isinstance(img, str): + return Image.open(img).convert("RGB") + elif isinstance(img, np.ndarray): + # Assume HxWxC uint8/RGB + return Image.fromarray(img).convert("RGB") + elif isinstance(img, Image.Image): + return img.convert("RGB") + else: + raise ValueError(f"Unsupported image type: {type(img)}") + + def _normalize_image(self, img: Image.Image) -> torch.Tensor: + img_tensor = T.ToTensor()(img) + return self.NORMALIZE(img_tensor) + + # ----------------------------- + # Boundary resizing + # ----------------------------- + def _resize_image(self, img: Image.Image, target_size: int, method: str) -> Image.Image: + if method in ("upper_bound_resize", "upper_bound_crop"): + return self._resize_longest_side(img, target_size) + elif method in ("lower_bound_resize", "lower_bound_crop"): + return self._resize_shortest_side(img, target_size) + else: + raise ValueError(f"Unsupported resize method: {method}") + + def _resize_longest_side(self, img: Image.Image, target_size: int) -> Image.Image: + w, h = img.size + longest = max(w, h) + if longest == target_size: + return img + scale = target_size / float(longest) + new_w = max(1, int(round(w * scale))) + new_h = max(1, int(round(h * scale))) + interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA + arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation) + return Image.fromarray(arr) + + def _resize_shortest_side(self, img: Image.Image, target_size: int) -> Image.Image: + w, h = img.size + shortest = min(w, h) + if shortest == target_size: + return img + scale = target_size / float(shortest) + new_w = max(1, int(round(w * scale))) + new_h = max(1, int(round(h * scale))) + interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA + arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation) + return Image.fromarray(arr) + + # ----------------------------- + # Make divisible by PATCH_SIZE + # ----------------------------- + def _make_divisible_by_crop(self, img: Image.Image, patch: int) -> Image.Image: + """ + Floor each dimension to the nearest multiple of PATCH_SIZE via center crop. + Example: 504x377 -> 504x364 + """ + w, h = img.size + new_w = (w // patch) * patch + new_h = (h // patch) * patch + if new_w == w and new_h == h: + return img + left = (w - new_w) // 2 + top = (h - new_h) // 2 + return img.crop((left, top, left + new_w, top + new_h)) + + def _make_divisible_by_resize(self, img: Image.Image, patch: int) -> Image.Image: + """ + Round each dimension to nearest multiple of PATCH_SIZE via small resize. + """ + w, h = img.size + + def nearest_multiple(x: int, p: int) -> int: + down = (x // p) * p + up = down + p + return up if abs(up - x) <= abs(x - down) else down + + new_w = max(1, nearest_multiple(w, patch)) + new_h = max(1, nearest_multiple(h, patch)) + if new_w == w and new_h == h: + return img + upscale = (new_w > w) or (new_h > h) + interpolation = cv2.INTER_CUBIC if upscale else cv2.INTER_AREA + arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation) + return Image.fromarray(arr) + + +# Backward compatibility alias +InputAdapter = InputProcessor diff --git a/src/depth_anything_3/utils/io/output_processor.py b/src/depth_anything_3/utils/io/output_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..c317eb9d596c1687b5281891a035993868cc5f8c --- /dev/null +++ b/src/depth_anything_3/utils/io/output_processor.py @@ -0,0 +1,172 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Output processor for Depth Anything 3. + +This module handles model output processing, including tensor-to-numpy conversion, +batch dimension removal, and Prediction object creation. +""" + +from __future__ import annotations + +import numpy as np +import torch +from addict import Dict as AddictDict + +from depth_anything_3.specs import Prediction + + +class OutputProcessor: + """ + Output processor for converting model outputs to Prediction objects. + + Handles tensor-to-numpy conversion, batch dimension removal, + and creates structured Prediction objects with proper data types. + """ + + def __init__(self) -> None: + """Initialize the output processor.""" + + def __call__(self, model_output: dict[str, torch.Tensor]) -> Prediction: + """ + Convert model output to Prediction object. + + Args: + model_output: Model output dictionary containing depth, conf, extrinsics, intrinsics + Expected shapes: depth (B, N, 1, H, W), conf (B, N, 1, H, W), + extrinsics (B, N, 4, 4), intrinsics (B, N, 3, 3) + + Returns: + Prediction: Object containing depth estimation results with shapes: + depth (N, H, W), conf (N, H, W), extrinsics (N, 4, 4), intrinsics (N, 3, 3) + """ + # Extract data from batch dimension (B=1, N=number of images) + depth = self._extract_depth(model_output) + conf = self._extract_conf(model_output) + extrinsics = self._extract_extrinsics(model_output) + intrinsics = self._extract_intrinsics(model_output) + sky = self._extract_sky(model_output) + aux = self._extract_aux(model_output) + gaussians = model_output.get("gaussians", None) + scale_factor = model_output.get("scale_factor", None) + + return Prediction( + depth=depth, + sky=sky, + conf=conf, + extrinsics=extrinsics, + intrinsics=intrinsics, + is_metric=getattr(model_output, "is_metric", 0), + gaussians=gaussians, + aux=aux, + scale_factor=scale_factor, + ) + + def _extract_depth(self, model_output: dict[str, torch.Tensor]) -> np.ndarray: + """ + Extract depth tensor from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Depth array with shape (N, H, W) + """ + depth = model_output["depth"].squeeze(0).squeeze(-1).cpu().numpy() # (N, H, W) + return depth + + def _extract_conf(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None: + """ + Extract confidence tensor from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Confidence array with shape (N, H, W) or None + """ + conf = model_output.get("depth_conf", None) + if conf is not None: + conf = conf.squeeze(0).cpu().numpy() # (N, H, W) + return conf + + def _extract_extrinsics(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None: + """ + Extract extrinsics tensor from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Extrinsics array with shape (N, 4, 4) or None + """ + extrinsics = model_output.get("extrinsics", None) + if extrinsics is not None: + extrinsics = extrinsics.squeeze(0).cpu().numpy() # (N, 4, 4) + return extrinsics + + def _extract_intrinsics(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None: + """ + Extract intrinsics tensor from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Intrinsics array with shape (N, 3, 3) or None + """ + intrinsics = model_output.get("intrinsics", None) + if intrinsics is not None: + intrinsics = intrinsics.squeeze(0).cpu().numpy() # (N, 3, 3) + return intrinsics + + def _extract_sky(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None: + """ + Extract sky tensor from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Sky mask array with shape (N, H, W) or None + """ + sky = model_output.get("sky", None) + if sky is not None: + sky = sky.squeeze(0).cpu().numpy() >= 0.5 # (N, H, W) + return sky + + def _extract_aux(self, model_output: dict[str, torch.Tensor]) -> AddictDict: + """ + Extract auxiliary data from model output and convert to numpy. + + Args: + model_output: Model output dictionary + + Returns: + Dictionary containing auxiliary data + """ + aux = model_output.get("aux", None) + ret = AddictDict() + if aux is not None: + for k in aux.keys(): + if isinstance(aux[k], torch.Tensor): + ret[k] = aux[k].squeeze(0).cpu().numpy() + else: + ret[k] = aux[k] + return ret + + +# Backward compatibility alias +OutputAdapter = OutputProcessor diff --git a/src/depth_anything_3/utils/layout_helpers.py b/src/depth_anything_3/utils/layout_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..da5c4109663125fb642dc3a9cfddf0f44189161d --- /dev/null +++ b/src/depth_anything_3/utils/layout_helpers.py @@ -0,0 +1,217 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This file contains useful layout utilities for images. They are: + +- add_border: Add a border to an image. +- cat/hcat/vcat: Join images by arranging them in a line. If the images have different + sizes, they are aligned as specified (start, end, center). Allows you to specify a gap + between images. + +Images are assumed to be float32 tensors with shape (channel, height, width). +""" + +from typing import Any, Generator, Iterable, Literal, Union + +import torch +from torch import Tensor + +Alignment = Literal["start", "center", "end"] +Axis = Literal["horizontal", "vertical"] +Color = Union[ + int, + float, + Iterable[int], + Iterable[float], + Tensor, + Tensor, +] + + +def _sanitize_color(color: Color) -> Tensor: # "#channel" + # Convert tensor to list (or individual item). + if isinstance(color, torch.Tensor): + color = color.tolist() + + # Turn iterators and individual items into lists. + if isinstance(color, Iterable): + color = list(color) + else: + color = [color] + + return torch.tensor(color, dtype=torch.float32) + + +def _intersperse(iterable: Iterable, delimiter: Any) -> Generator[Any, None, None]: + it = iter(iterable) + yield next(it) + for item in it: + yield delimiter + yield item + + +def _get_main_dim(main_axis: Axis) -> int: + return { + "horizontal": 2, + "vertical": 1, + }[main_axis] + + +def _get_cross_dim(main_axis: Axis) -> int: + return { + "horizontal": 1, + "vertical": 2, + }[main_axis] + + +def _compute_offset(base: int, overlay: int, align: Alignment) -> slice: + assert base >= overlay + offset = { + "start": 0, + "center": (base - overlay) // 2, + "end": base - overlay, + }[align] + return slice(offset, offset + overlay) + + +def overlay( + base: Tensor, # "channel base_height base_width" + overlay: Tensor, # "channel overlay_height overlay_width" + main_axis: Axis, + main_axis_alignment: Alignment, + cross_axis_alignment: Alignment, +) -> Tensor: # "channel base_height base_width" + # The overlay must be smaller than the base. + _, base_height, base_width = base.shape + _, overlay_height, overlay_width = overlay.shape + assert base_height >= overlay_height and base_width >= overlay_width + + # Compute spacing on the main dimension. + main_dim = _get_main_dim(main_axis) + main_slice = _compute_offset( + base.shape[main_dim], overlay.shape[main_dim], main_axis_alignment + ) + + # Compute spacing on the cross dimension. + cross_dim = _get_cross_dim(main_axis) + cross_slice = _compute_offset( + base.shape[cross_dim], overlay.shape[cross_dim], cross_axis_alignment + ) + + # Combine the slices and paste the overlay onto the base accordingly. + selector = [..., None, None] + selector[main_dim] = main_slice + selector[cross_dim] = cross_slice + result = base.clone() + result[selector] = overlay + return result + + +def cat( + main_axis: Axis, + *images: Iterable[Tensor], # "channel _ _" + align: Alignment = "center", + gap: int = 8, + gap_color: Color = 1, +) -> Tensor: # "channel height width" + """Arrange images in a line. The interface resembles a CSS div with flexbox.""" + device = images[0].device + gap_color = _sanitize_color(gap_color).to(device) + + # Find the maximum image side length in the cross axis dimension. + cross_dim = _get_cross_dim(main_axis) + cross_axis_length = max(image.shape[cross_dim] for image in images) + + # Pad the images. + padded_images = [] + for image in images: + # Create an empty image with the correct size. + padded_shape = list(image.shape) + padded_shape[cross_dim] = cross_axis_length + base = torch.ones(padded_shape, dtype=torch.float32, device=device) + base = base * gap_color[:, None, None] + padded_images.append(overlay(base, image, main_axis, "start", align)) + + # Intersperse separators if necessary. + if gap > 0: + # Generate a separator. + c, _, _ = images[0].shape + separator_size = [gap, gap] + separator_size[cross_dim - 1] = cross_axis_length + separator = torch.ones((c, *separator_size), dtype=torch.float32, device=device) + separator = separator * gap_color[:, None, None] + + # Intersperse the separator between the images. + padded_images = list(_intersperse(padded_images, separator)) + + return torch.cat(padded_images, dim=_get_main_dim(main_axis)) + + +def hcat( + *images: Iterable[Tensor], # "channel _ _" + align: Literal["start", "center", "end", "top", "bottom"] = "start", + gap: int = 8, + gap_color: Color = 1, +): + """Shorthand for a horizontal linear concatenation.""" + return cat( + "horizontal", + *images, + align={ + "start": "start", + "center": "center", + "end": "end", + "top": "start", + "bottom": "end", + }[align], + gap=gap, + gap_color=gap_color, + ) + + +def vcat( + *images: Iterable[Tensor], # "channel _ _" + align: Literal["start", "center", "end", "left", "right"] = "start", + gap: int = 8, + gap_color: Color = 1, +): + """Shorthand for a horizontal linear concatenation.""" + return cat( + "vertical", + *images, + align={ + "start": "start", + "center": "center", + "end": "end", + "left": "start", + "right": "end", + }[align], + gap=gap, + gap_color=gap_color, + ) + + +def add_border( + image: Tensor, # "channel height width" + border: int = 8, + color: Color = 1, +) -> Tensor: # "channel new_height new_width" + color = _sanitize_color(color).to(image) + c, h, w = image.shape + result = torch.empty( + (c, h + 2 * border, w + 2 * border), dtype=torch.float32, device=image.device + ) + result[:] = color[:, None, None] + result[:, border : h + border, border : w + border] = image + return result diff --git a/src/depth_anything_3/utils/logger.py b/src/depth_anything_3/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..0eb4f60696a085001cf4866ccfe1654170702a2d --- /dev/null +++ b/src/depth_anything_3/utils/logger.py @@ -0,0 +1,82 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + + +class Color: + RED = "\033[91m" + YELLOW = "\033[93m" + WHITE = "\033[97m" + GREEN = "\033[92m" + RESET = "\033[0m" + + +LOG_LEVELS = {"ERROR": 0, "WARN": 1, "INFO": 2, "DEBUG": 3} + +COLOR_MAP = {"ERROR": Color.RED, "WARN": Color.YELLOW, "INFO": Color.WHITE, "DEBUG": Color.GREEN} + + +def get_env_log_level(): + level = os.environ.get("DA3_LOG_LEVEL", "INFO").upper() + return LOG_LEVELS.get(level, LOG_LEVELS["INFO"]) + + +class Logger: + def __init__(self): + self.level = get_env_log_level() + + def log(self, level_str, *args, **kwargs): + level_key = level_str.split(":")[0].strip() + level_val = LOG_LEVELS.get(level_key) + if level_val is None: + raise ValueError(f"Unknown log level: {level_str}") + if self.level >= level_val: + color = COLOR_MAP[level_key] + msg = " ".join(str(arg) for arg in args) + + # Align log level output in square brackets + # ERROR and DEBUG are 5 characters, INFO and WARN have an extra space for alignment + tag = level_key + if tag in ("INFO", "WARN"): + tag += " " + print( + f"{color}[{tag}] {msg}{Color.RESET}", + file=sys.stderr if level_key == "ERROR" else sys.stdout, + **kwargs, + ) + + def error(self, *args, **kwargs): + self.log("ERROR:", *args, **kwargs) + + def warn(self, *args, **kwargs): + self.log("WARN:", *args, **kwargs) + + def info(self, *args, **kwargs): + self.log("INFO:", *args, **kwargs) + + def debug(self, *args, **kwargs): + self.log("DEBUG:", *args, **kwargs) + + +logger = Logger() + +__all__ = ["logger"] + +if __name__ == "__main__": + logger.info("This is an info message") + logger.warn("This is a warning message") + logger.error("This is an error message") + logger.debug("This is a debug message") diff --git a/src/depth_anything_3/utils/memory.py b/src/depth_anything_3/utils/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..ac793c5fbcd6c7701f87436cd8529dc8ecb58335 --- /dev/null +++ b/src/depth_anything_3/utils/memory.py @@ -0,0 +1,243 @@ +# Copyright (c) 2025 Delanoe Pirard and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +GPU memory utility helpers. + +Shared cleanup and memory checking logic used by both the backend API and +the Gradio UI to keep memory-management behavior consistent. +""" +from __future__ import annotations + +import gc +from typing import Any, Dict, Optional + +import torch + + +def get_gpu_memory_info() -> Optional[Dict[str, Any]]: + """Return a snapshot of current GPU memory usage or None if CUDA not available. + + Keys in returned dict: total_gb, allocated_gb, reserved_gb, free_gb, utilization + """ + if not torch.cuda.is_available(): + return None + + try: + device = torch.cuda.current_device() + total_memory = torch.cuda.get_device_properties(device).total_memory + allocated_memory = torch.cuda.memory_allocated(device) + reserved_memory = torch.cuda.memory_reserved(device) + free_memory = total_memory - reserved_memory + + return { + "total_gb": total_memory / 1024 ** 3, + "allocated_gb": allocated_memory / 1024 ** 3, + "reserved_gb": reserved_memory / 1024 ** 3, + "free_gb": free_memory / 1024 ** 3, + "utilization": (reserved_memory / total_memory) * 100, + } + except Exception: + return None + + +def cleanup_cuda_memory() -> None: + """Perform a robust GPU cleanup sequence. + + This includes synchronizing, emptying caches, collecting IPC handles and + running the Python garbage collector. Use this instead of a raw + ``torch.cuda.empty_cache()`` where you need reliable freeing of GPU memory + between model loads or in error handling paths. + """ + try: + if torch.cuda.is_available(): + mem_before = get_gpu_memory_info() + + torch.cuda.synchronize() + torch.cuda.empty_cache() + # Collect cross-process cuda resources + try: + torch.cuda.ipc_collect() + except Exception: + # Older PyTorch versions or non-cuda devices may not support + # ipc_collect (no-op if not available) + pass + gc.collect() + + mem_after = get_gpu_memory_info() + if mem_before and mem_after: + freed = mem_before["reserved_gb"] - mem_after["reserved_gb"] + print( + f"CUDA cleanup: freed {freed:.2f}GB, " + f"available: {mem_after['free_gb']:.2f}GB/{mem_after['total_gb']:.2f}GB" + ) + else: + print("CUDA memory cleanup completed") + except Exception as e: + print(f"Warning: CUDA cleanup failed: {e}") + + +def check_memory_availability(required_gb: float = 2.0) -> tuple[bool, str]: + """Return whether at least ``required_gb`` seems available on the current GPU. + + The returned tuple is (is_available, message) with a human-friendly message. + """ + try: + if not torch.cuda.is_available(): + return False, "CUDA is not available" + + mem_info = get_gpu_memory_info() + if mem_info is None: + return True, "Cannot check memory, proceeding anyway" + + if mem_info["free_gb"] < required_gb: + return ( + False, + ( + f"Insufficient GPU memory: {mem_info['free_gb']:.2f}GB available, " + f"{required_gb:.2f}GB required. Total: {mem_info['total_gb']:.2f}GB, " + f"Used: {mem_info['reserved_gb']:.2f}GB ({mem_info['utilization']:.1f}%)" + ), + ) + + return ( + True, + ( + f"Memory check passed: {mem_info['free_gb']:.2f}GB available, " + f"{required_gb:.2f}GB required" + ), + ) + except Exception as e: + return True, f"Memory check failed: {e}, proceeding anyway" +def estimate_memory_requirement(num_images: int, process_res: int) -> float: + """Heuristic estimate for memory usage (GB) based on image count and resolution. + + This mirrors the simple policy used by the backend service so other code + (e.g., Gradio UI) can make consistent decisions when checking available + memory before loading a model or running inference. + + Args: + num_images: Number of images to process. + process_res: Processing resolution. + + Returns: + Estimated memory requirement in GB. + """ + base_memory = 2.0 + per_image_memory = (process_res / 504) ** 2 * 0.5 + total_memory = base_memory + (num_images * per_image_memory * 0.1) + return total_memory + + +# =========================== +# Proactive Memory Management +# =========================== + + +def cleanup_mps_memory() -> None: + """ + Perform proactive MPS memory cleanup. + + MPS (Apple Silicon) has unified memory architecture where CPU and GPU + share the same memory pool. Proactive cleanup prevents fragmentation. + """ + try: + if hasattr(torch, "mps") and torch.backends.mps.is_available(): + torch.mps.empty_cache() + gc.collect() + print("MPS memory cache cleared") + except Exception as e: + print(f"Warning: MPS cleanup failed: {e}") + + +def cleanup_all_device_memory() -> None: + """ + Clean up memory for all available devices (CUDA, MPS, CPU). + + Call this between batch processing or after large allocations + to prevent memory fragmentation and OOM errors. + + Example: + >>> from depth_anything_3.utils.memory import cleanup_all_device_memory + >>> # Process batch 1 + >>> model.inference(images_batch1) + >>> cleanup_all_device_memory() # Clean between batches + >>> # Process batch 2 + >>> model.inference(images_batch2) + """ + cleanup_cuda_memory() + cleanup_mps_memory() + gc.collect() + + +def clear_cache_if_low_memory(threshold_gb: float = 2.0) -> bool: + """ + Conditionally clear cache if available memory is below threshold. + + Args: + threshold_gb: Memory threshold in GB (default: 2.0) + + Returns: + True if cache was cleared, False otherwise + + Example: + >>> from depth_anything_3.utils.memory import clear_cache_if_low_memory + >>> # Before large allocation + >>> if clear_cache_if_low_memory(threshold_gb=3.0): + ... print("Low memory detected, cache cleared") + """ + if torch.cuda.is_available(): + mem_info = get_gpu_memory_info() + if mem_info and mem_info["free_gb"] < threshold_gb: + print(f"Low memory detected ({mem_info['free_gb']:.2f} GB < {threshold_gb:.2f} GB)") + cleanup_cuda_memory() + return True + + elif hasattr(torch, "mps") and torch.backends.mps.is_available(): + # MPS doesn't expose free memory easily, always clear if requested + cleanup_mps_memory() + return True + + return False + + +def log_memory_summary() -> None: + """ + Log current memory usage summary for all devices. + + Useful for debugging memory issues or understanding memory patterns. + """ + if torch.cuda.is_available(): + mem_info = get_gpu_memory_info() + if mem_info: + print( + f"[CUDA Memory] Allocated: {mem_info['allocated_gb']:.2f} GB, " + f"Reserved: {mem_info['reserved_gb']:.2f} GB, " + f"Free: {mem_info['free_gb']:.2f} GB / {mem_info['total_gb']:.2f} GB " + f"({mem_info['utilization']:.1f}% used)" + ) + + elif hasattr(torch, "mps") and torch.backends.mps.is_available(): + try: + allocated = torch.mps.current_allocated_memory() / (1024**3) + driver_allocated = torch.mps.driver_allocated_memory() / (1024**3) + print( + f"[MPS Memory] Allocated: {allocated:.2f} GB, " + f"Driver Allocated: {driver_allocated:.2f} GB" + ) + except Exception as e: + print(f"[MPS Memory] Stats unavailable: {e}") + + else: + print("[CPU Memory] Stats not available via PyTorch") diff --git a/src/depth_anything_3/utils/model_loading.py b/src/depth_anything_3/utils/model_loading.py new file mode 100644 index 0000000000000000000000000000000000000000..f643d306c192e0c1e77704d6a3c9be7b9e364c9b --- /dev/null +++ b/src/depth_anything_3/utils/model_loading.py @@ -0,0 +1,150 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Model loading and state dict conversion utilities. +""" + +from typing import Dict, Tuple + +import torch + +from depth_anything_3.utils.logger import logger + + +def convert_general_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """ + Convert general model state dict to match current model architecture. + + Args: + state_dict: Original state dictionary + + Returns: + Converted state dictionary + """ + # Replace module prefixes + state_dict = {k.replace("module.", "model."): v for k, v in state_dict.items()} + state_dict = {k.replace(".net.", ".backbone."): v for k, v in state_dict.items()} + + # Remove camera token if present + if "model.backbone.pretrained.camera_token" in state_dict: + del state_dict["model.backbone.pretrained.camera_token"] + + # Replace camera token naming + state_dict = { + k.replace(".camera_token_extra", ".camera_token"): v for k, v in state_dict.items() + } + + # Replace head naming + state_dict = { + k.replace("model.all_heads.camera_cond_head", "model.cam_enc"): v + for k, v in state_dict.items() + } + state_dict = { + k.replace("model.all_heads.camera_head", "model.cam_dec"): v for k, v in state_dict.items() + } + state_dict = {k.replace(".more_mlps.", ".backbone."): v for k, v in state_dict.items()} + state_dict = {k.replace(".fc_rot.", ".fc_qvec."): v for k, v in state_dict.items()} + state_dict = { + k.replace("model.all_heads.head", "model.head"): v for k, v in state_dict.items() + } + + # Replace output naming + state_dict = { + k.replace("output_conv2_additional.sky_mask", "sky_output_conv2"): v + for k, v in state_dict.items() + } + state_dict = {k.replace("_ray.", "_aux."): v for k, v in state_dict.items()} + + # Update GS-DPT head naming and value + state_dict = {k.replace("gaussian_param_head.", "gs_head."): v for k, v in state_dict.items()} + + return state_dict + + +def convert_metric_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """ + Convert metric model state dict to match current model architecture. + + Args: + state_dict: Original metric state dictionary + + Returns: + Converted state dictionary + """ + # Add module prefix for metric models + state_dict = {"module." + k: v for k, v in state_dict.items()} + return convert_general_state_dict(state_dict) + + +def load_pretrained_weights(model, model_path: str, is_metric: bool = False) -> Tuple[list, list]: + """ + Load pretrained weights for a single model. + + Args: + model: Model instance to load weights into + model_path: Path to the pretrained weights + is_metric: Whether this is a metric model + + Returns: + Tuple of (missed_keys, unexpected_keys) + """ + state_dict = torch.load(model_path, map_location="cpu") + + if is_metric: + state_dict = convert_metric_state_dict(state_dict) + else: + state_dict = convert_general_state_dict(state_dict) + + missed, unexpected = model.load_state_dict(state_dict, strict=False) + logger.info("Missed keys:", missed) + logger.info("Unexpected keys:", unexpected) + + return missed, unexpected + + +def load_pretrained_nested_weights( + model, main_model_path: str, metric_model_path: str +) -> Tuple[list, list]: + """ + Load pretrained weights for a nested model with both main and metric branches. + + Args: + model: Nested model instance + main_model_path: Path to main model weights + metric_model_path: Path to metric model weights + + Returns: + Tuple of (missed_keys, unexpected_keys) + """ + # Load main model weights + state_dict0 = torch.load(main_model_path, map_location="cpu") + state_dict0 = convert_general_state_dict(state_dict0) + state_dict0 = {k.replace("model.", "model.da3."): v for k, v in state_dict0.items()} + + # Load metric model weights + state_dict1 = torch.load(metric_model_path, map_location="cpu") + state_dict1 = convert_metric_state_dict(state_dict1) + state_dict1 = {k.replace("model.", "model.da3_metric."): v for k, v in state_dict1.items()} + + # Combine state dictionaries + combined_state_dict = state_dict0.copy() + combined_state_dict.update(state_dict1) + + missed, unexpected = model.load_state_dict(combined_state_dict, strict=False) + + print("Missed keys:", missed) + print("Unexpected keys:", unexpected) + + return missed, unexpected diff --git a/src/depth_anything_3/utils/parallel_utils.py b/src/depth_anything_3/utils/parallel_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9f1004551a91c1629e5f4f9bd09504c3639c5e72 --- /dev/null +++ b/src/depth_anything_3/utils/parallel_utils.py @@ -0,0 +1,134 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import os +from functools import wraps +from multiprocessing.pool import ThreadPool +from threading import Thread +from typing import Callable, Dict, List + +import imageio +from tqdm import tqdm + + +def async_call_func(func): + @wraps(func) + async def wrapper(*args, **kwargs): + loop = asyncio.get_event_loop() + # Use run_in_executor to run the blocking function in a separate thread + return await loop.run_in_executor(None, func, *args, **kwargs) + + return wrapper + + +def slice_func(chunk_index, chunk_dim, chunk_size): + """Create a slice for accessing a chunk of data along a specific dimension.""" + return [slice(None)] * chunk_dim + [slice(chunk_index, chunk_index + chunk_size)] + + +def async_call(fn): + def wrapper(*args, **kwargs): + Thread(target=fn, args=args, kwargs=kwargs).start() + + return wrapper + + +def _save_image_impl(save_img, save_path): + """Common implementation for saving images synchronously or asynchronously""" + os.makedirs(os.path.dirname(save_path), exist_ok=True) + imageio.imwrite(save_path, save_img) + + +@async_call +def save_image_async(save_img, save_path): + """Save image asynchronously""" + _save_image_impl(save_img, save_path) + + +def save_image(save_img, save_path): + """Save image synchronously""" + _save_image_impl(save_img, save_path) + + +def parallel_execution( + *args, + action: Callable, + num_processes=32, + print_progress=False, + sequential=False, + async_return=False, + desc=None, + **kwargs, +): + # Partially copy from EasyVolumetricVideo (parallel_execution) + # NOTE: we expect first arg / or kwargs to be distributed + # NOTE: print_progress arg is reserved. + # `*args` packs all positional arguments passed to the function into a tuple + args = list(args) + + def get_length(args: List, kwargs: Dict): + for a in args: + if isinstance(a, list): + return len(a) + for v in kwargs.values(): + if isinstance(v, list): + return len(v) + raise NotImplementedError + + def get_action_args(length: int, args: List, kwargs: Dict, i: int): + action_args = [ + (arg[i] if isinstance(arg, list) and len(arg) == length else arg) for arg in args + ] + # TODO: Support all types of iterable + action_kwargs = { + key: ( + kwargs[key][i] + if isinstance(kwargs[key], list) and len(kwargs[key]) == length + else kwargs[key] + ) + for key in kwargs + } + return action_args, action_kwargs + + if not sequential: + # Create ThreadPool + pool = ThreadPool(processes=num_processes) + + # Spawn threads + results = [] + asyncs = [] + length = get_length(args, kwargs) + for i in range(length): + action_args, action_kwargs = get_action_args(length, args, kwargs, i) + async_result = pool.apply_async(action, action_args, action_kwargs) + asyncs.append(async_result) + + # Join threads and get return values + if not async_return: + for async_result in tqdm(asyncs, desc=desc, disable=not print_progress): + results.append(async_result.get()) # will sync the corresponding thread + pool.close() + pool.join() + return results + else: + return pool + else: + results = [] + length = get_length(args, kwargs) + for i in tqdm(range(length), desc=desc, disable=not print_progress): + action_args, action_kwargs = get_action_args(length, args, kwargs, i) + async_result = action(*action_args, **action_kwargs) + results.append(async_result) + return results diff --git a/src/depth_anything_3/utils/pca_utils.py b/src/depth_anything_3/utils/pca_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..2b9eee268cd8692d885bf093700a5752077b9d7d --- /dev/null +++ b/src/depth_anything_3/utils/pca_utils.py @@ -0,0 +1,284 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +PCA utilities for feature visualization and dimensionality reduction (video-friendly). +- Support frame-by-frame: transform_frame / transform_video +- Support one-time global PCA fitting and reuse (mean, V3) for stable colors +- Support Procrustes alignment (solving principal component order/sign/rotation jumps) +- Support global fixed or temporal EMA for percentiles (time dimension only, no spatial) +""" + +import numpy as np +import torch + + +def pca_to_rgb_4d_bf16_percentile( + x_np: np.ndarray, + device=None, + q_oversample: int = 6, + clip_percent: float = 10.0, # Percentage to clip from top and bottom (0~49.9) + return_uint8: bool = False, + enable_autocast_bf16: bool = True, +): + """ + Reduce numpy array of shape (49, 27, 36, 3072) to 3D via PCA and visualize as (49, 27, 36, 3). + - PCA uses torch.pca_lowrank (randomized SVD), defaults to GPU. + - Uses CUDA bf16 autocast in computation (if available), + then per-channel percentile clipping and normalization. + - Default removes 5% outliers from top and bottom (adjustable via clip_percent) to + improve visualization contrast. + + Parameters + ---------- + x_np : np.ndarray + Shape must be (49, 27, 36, 3072). dtype recommended float32/float64. + device : str | None + Specify 'cuda' or 'cpu'. Auto-select if None (prefer cuda). + q_oversample : int + Oversampling q for pca_lowrank, must be >= 3. + Slightly larger than target dim (3) is more stable, default 6. + clip_percent : float + Percentage to clip from top and bottom (0~49.9), + e.g. 5.0 means clip lowest 5% and highest 5% per channel. + return_uint8 : bool + True returns uint8(0~255), otherwise returns float32(0~1). + enable_autocast_bf16 : bool + Enable bf16 autocast on CUDA. + + Returns + ------- + np.ndarray + Array of shape (49, 27, 36, 3), float32[0,1] or uint8[0,255]. + """ + assert ( + x_np.ndim == 4 + ) # and x_np.shape[-1] == 3072, f"expect (49,27,36,3072), got {x_np.shape}" + B1, B2, B3, D = x_np.shape + N = B1 * B2 * B3 + + # Device selection + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Convert input to torch, unified float32 + X = torch.from_numpy(x_np.reshape(N, D)).to(device=device, dtype=torch.float32) + + # Parameter and safety checks + k = 3 + q = max(int(q_oversample), k) + clip_percent = float(clip_percent) + if not (0.0 <= clip_percent < 50.0): + raise ValueError( + "clip_percent must be in [0, 50), e.g. 5.0 means clip 5% from top and bottom" + ) + low = clip_percent / 100.0 + high = 1.0 - low + + with torch.no_grad(): + # Zero mean + mean = X.mean(dim=0, keepdim=True) + Xc = X - mean + + # Main computation: PCA + projection, try to use bf16 + # (auto-fallback if operator not supported) + device.startswith("cuda") and enable_autocast_bf16 + U, S, V = torch.pca_lowrank(Xc, q=q, center=False) # V: (D, q) + V3 = V[:, :k] # (3072, 3) + PCs = Xc @ V3 # (N, 3) + + # === Per-channel percentile clipping and normalization to [0,1] === + # Vectorized one-time calculation of low/high percentiles for each channel + qs = torch.tensor([low, high], device=PCs.device, dtype=PCs.dtype) + qvals = torch.quantile(PCs, q=qs, dim=0) # Shape (2, 3) + lo = qvals[0] # (3,) + hi = qvals[1] # (3,) + + # Avoid degenerate case where hi==lo + denom = torch.clamp(hi - lo, min=1e-8) + + # Broadcast clipping + normalization + PCs = torch.clamp(PCs, lo, hi) + PCs = (PCs - lo) / denom # (N, 3) in [0,1] + + # Restore 4D + PCs = PCs.reshape(B1, B2, B3, k) + + # Output + if return_uint8: + out = (PCs * 255.0).round().clamp(0, 255).to(torch.uint8).cpu().numpy() + else: + out = PCs.clamp(0, 1).to(torch.float32).cpu().numpy() + + return out + + +class PCARGBVisualizer: + """ + Stable PCA→RGB for video features shaped (T, H, W, D) or a single frame (H, W, D). + - Global mean/V3 reference for stable colors + - Per-frame PCA with Procrustes alignment to V3_ref (basis_mode='procrustes') + - Percentile normalization with global or EMA stats (time-only, no spatial smoothing) + """ + + def __init__( + self, + device=None, + q_oversample: int = 16, + clip_percent: float = 10.0, + return_uint8: bool = False, + enable_autocast_bf16: bool = True, + basis_mode: str = "procrustes", # 'fixed' | 'procrustes' + percentile_mode: str = "ema", # 'global' | 'ema' + ema_alpha: float = 0.1, + denom_eps: float = 1e-4, + ): + assert 0.0 <= clip_percent < 50.0 + assert basis_mode in ("fixed", "procrustes") + assert percentile_mode in ("global", "ema") + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + self.q = max(int(q_oversample), 6) + self.clip_percent = float(clip_percent) + self.return_uint8 = return_uint8 + self.enable_autocast_bf16 = enable_autocast_bf16 + self.basis_mode = basis_mode + self.percentile_mode = percentile_mode + self.ema_alpha = float(ema_alpha) + self.denom_eps = float(denom_eps) + + # reference state + self.mean_ref = None # (1, D) + self.V3_ref = None # (D, 3) + self.lo_ref = None # (3,) + self.hi_ref = None # (3,) + + @torch.no_grad() + def fit_reference(self, frames): + """ + Fit global mean/V3 and initialize percentiles from a reference set. + frames: ndarray (T,H,W,D) or list of (H,W,D) + """ + if isinstance(frames, np.ndarray): + if frames.ndim != 4: + raise ValueError("fit_reference expects (T,H,W,D) ndarray.") + T, H, W, D = frames.shape + X = torch.from_numpy(frames.reshape(T * H * W, D)) + else: # list of (H,W,D) + xs = [torch.from_numpy(x.reshape(-1, x.shape[-1])) for x in frames] + D = xs[0].shape[-1] + X = torch.cat(xs, dim=0) + + X = X.to(self.device, dtype=torch.float32) + X = torch.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6) + + mean = X.mean(0, keepdim=True) + Xc = X - mean + + U, S, V = torch.pca_lowrank(Xc, q=max(self.q, 8), center=False) + V3 = V[:, :3] # (D,3) + + PCs = Xc @ V3 + low = self.clip_percent / 100.0 + high = 1.0 - low + qs = torch.tensor([low, high], device=PCs.device, dtype=PCs.dtype) + qvals = torch.quantile(PCs, q=qs, dim=0) + lo, hi = qvals[0], qvals[1] + + self.mean_ref = mean + self.V3_ref = V3 + if self.percentile_mode == "global": + self.lo_ref, self.hi_ref = lo, hi + else: + self.lo_ref = lo.clone() + self.hi_ref = hi.clone() + + @torch.no_grad() + def _project_with_stable_colors(self, X: torch.Tensor) -> torch.Tensor: + """ + X: (N,D) where N = H*W + Returns PCs_raw: (N,3) using stable basis (fixed or Procrustes-aligned) + """ + assert self.mean_ref is not None and self.V3_ref is not None, "Call fit_reference() first." + X = torch.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6) + Xc = X - self.mean_ref + + if self.basis_mode == "fixed": + V3_used = self.V3_ref + else: + U, S, V = torch.pca_lowrank(Xc, q=max(self.q, 6), center=False) + V3 = V[:, :3] # (D,3) + M = V3.T @ self.V3_ref + Uo, So, Vh = torch.linalg.svd(M) + R = Uo @ Vh + V3_used = V3 @ R + # Optional polarity fix via anchor + a = self.V3_ref.mean(0, keepdim=True) + sign = torch.sign((V3_used * a).sum(0, keepdim=True)).clamp(min=-1) + V3_used = V3_used * sign + + return Xc @ V3_used + + @torch.no_grad() + def _normalize_rgb(self, PCs_raw: torch.Tensor) -> torch.Tensor: + assert self.lo_ref is not None and self.hi_ref is not None + if self.percentile_mode == "global": + lo, hi = self.lo_ref, self.hi_ref + else: + low = self.clip_percent / 100.0 + high = 1.0 - low + qs = torch.tensor([low, high], device=PCs_raw.device, dtype=PCs_raw.dtype) + qvals = torch.quantile(PCs_raw, q=qs, dim=0) + lo_now, hi_now = qvals[0], qvals[1] + a = self.ema_alpha + self.lo_ref = (1 - a) * self.lo_ref + a * lo_now + self.hi_ref = (1 - a) * self.hi_ref + a * hi_now + lo, hi = self.lo_ref, self.hi_ref + + denom = torch.clamp(hi - lo, min=self.denom_eps) + PCs = torch.clamp(PCs_raw, lo, hi) + PCs = (PCs - lo) / denom + return PCs.clamp_(0, 1) + + @torch.no_grad() + def transform_frame(self, frame: np.ndarray) -> np.ndarray: + """ + frame: (H,W,D) -> (H,W,3) + """ + if frame.ndim != 3: + raise ValueError("transform_frame expects (H,W,D).") + H, W, D = frame.shape + X = torch.from_numpy(frame.reshape(H * W, D)).to(self.device, dtype=torch.float32) + PCs_raw = self._project_with_stable_colors(X) + PCs = self._normalize_rgb(PCs_raw).reshape(H, W, 3) + if self.return_uint8: + return (PCs * 255.0).round().clamp(0, 255).to(torch.uint8).cpu().numpy() + return PCs.to(torch.float32).cpu().numpy() + + @torch.no_grad() + def transform_video(self, frames) -> np.ndarray: + """ + frames: (T,H,W,D) or list of (H,W,D) + returns: (T,H,W,3) + """ + outs = [] + if isinstance(frames, np.ndarray): + if frames.ndim != 4: + raise ValueError("transform_video expects (T,H,W,D).") + T, H, W, D = frames.shape + for t in range(T): + outs.append(self.transform_frame(frames[t])) + else: + for f in frames: + outs.append(self.transform_frame(f)) + return np.stack(outs, axis=0) diff --git a/src/depth_anything_3/utils/pose_align.py b/src/depth_anything_3/utils/pose_align.py new file mode 100644 index 0000000000000000000000000000000000000000..616ad8873ae9193cc5b29709bbefbebfd0591c22 --- /dev/null +++ b/src/depth_anything_3/utils/pose_align.py @@ -0,0 +1,348 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List + +import numpy as np +import torch +from evo.core.trajectory import PosePath3D + +from depth_anything_3.utils.geometry import affine_inverse, affine_inverse_np + + +def batch_apply_alignment_to_enc( + rots: torch.Tensor, trans: torch.Tensor, scales: torch.Tensor, enc_list: List[torch.Tensor] +): + pass + + +def batch_apply_alignment_to_ext( + rots: torch.Tensor, trans: torch.Tensor, scales: torch.Tensor, ext: torch.Tensor +): + device, _ = ext.device, ext.dtype + if ext.shape[-2:] == (3, 4): + pad = torch.zeros((*ext.shape[:-2], 4, 4), dtype=ext.dtype, device=device) + pad[..., :3, :4] = ext + pad[..., 3, 3] = 1.0 + ext = pad + pose_est = affine_inverse(ext) + pose_new_align_rot = rots[:, None] @ pose_est[..., :3, :3] + pose_new_align_trans = ( + scales[:, None, None] * (rots[:, None] @ pose_est[..., :3, 3:])[..., 0] + trans[:, None] + ) + pose_new_align = torch.zeros_like(ext) + pose_new_align[..., :3, :3] = pose_new_align_rot + pose_new_align[..., :3, 3] = pose_new_align_trans + pose_new_align[..., 3, 3] = 1.0 + return affine_inverse(pose_new_align)[:, :3] + + +def batch_align_poses_umeyama(ext_ref: torch.Tensor, ext_est: torch.Tensor): + device, dtype = ext_ref.device, ext_ref.dtype + assert ext_ref.dtype in [torch.float32, torch.float64] + assert ext_est.dtype in [torch.float32, torch.float64] + assert ext_ref.requires_grad is False + assert ext_est.requires_grad is False + rots, trans, scales = [], [], [] + for b in range(ext_ref.shape[0]): + r, t, s = align_poses_umeyama(ext_ref[b].cpu().numpy(), ext_est[b].cpu().numpy()) + rots.append(torch.from_numpy(r).to(device=device, dtype=dtype)) + trans.append(torch.from_numpy(t).to(device=device, dtype=dtype)) + scales.append(torch.tensor(s, device=device, dtype=dtype)) + return torch.stack(rots), torch.stack(trans), torch.stack(scales) + + +# Dependencies: affine_inverse_np, PosePath3D (maintain consistency with your existing project) + + +def _to44(ext): + if ext.shape[1] == 3: + out = np.eye(4)[None].repeat(len(ext), 0) + out[:, :3, :4] = ext + return out + return ext + + +def _poses_from_ext(ext_ref, ext_est): + ext_ref = _to44(ext_ref) + ext_est = _to44(ext_est) + pose_ref = affine_inverse_np(ext_ref) + pose_est = affine_inverse_np(ext_est) + return pose_ref, pose_est + + +def _umeyama_sim3_from_paths(pose_ref, pose_est): + path_ref = PosePath3D(poses_se3=pose_ref.copy()) + path_est = PosePath3D(poses_se3=pose_est.copy()) + r, t, s = path_est.align(path_ref, correct_scale=True) + pose_est_aligned = np.stack(path_est.poses_se3) + return r, t, s, pose_est_aligned + + +def _apply_sim3_to_poses(poses, r, t, s): + out = poses.copy() + Ri = poses[:, :3, :3] + ti = poses[:, :3, 3] + out[:, :3, :3] = r @ Ri + out[:, :3, 3] = (r @ (s * ti.T)).T + t + return out + + +def _median_nn_thresh(pose_ref, pose_est_aligned): + P_ref = pose_ref[:, :3, 3] + P_est = pose_est_aligned[:, :3, 3] + dists = [] + for p in P_est: + dd = np.linalg.norm(P_ref - p[None, :], axis=1) + dists.append(dd.min()) + return float(np.median(dists)) if dists else 0.0 + + +def _ransac_align_sim3( + pose_ref, pose_est, sub_n=None, inlier_thresh=None, max_iters=10, random_state=None +): + rng = np.random.default_rng(random_state) + N = pose_ref.shape[0] + idx_all = np.arange(N) + if sub_n is None: + sub_n = max(3, (N + 1) // 2) + else: + sub_n = max(3, min(sub_n, N)) + + # Pre-alignment + default threshold + r0, t0, s0, pose_est0 = _umeyama_sim3_from_paths(pose_ref, pose_est) + if inlier_thresh is None: + inlier_thresh = _median_nn_thresh(pose_ref, pose_est0) + + P_ref_all = pose_ref[:, :3, 3] + + best_model = (r0, t0, s0) + best_inliers = None + best_score = (-1, np.inf) # (num_inliers, mean_err) + + for _ in range(max_iters): + sample = rng.choice(idx_all, size=sub_n, replace=False) + try: + r, t, s, _ = _umeyama_sim3_from_paths(pose_ref[sample], pose_est[sample]) + except Exception: + continue + pose_h = _apply_sim3_to_poses(pose_est, r, t, s) + P_h = pose_h[:, :3, 3] + errs = np.linalg.norm(P_h - P_ref_all, axis=1) # Match by same index + inliers = errs <= inlier_thresh + k = int(inliers.sum()) + mean_err = float(errs[inliers].mean()) if k > 0 else np.inf + if (k > best_score[0]) or (k == best_score[0] and mean_err < best_score[1]): + best_score = (k, mean_err) + best_model = (r, t, s) + best_inliers = inliers + + # Fit again with best inliers + if best_inliers is not None and best_inliers.sum() >= 3: + r, t, s, _ = _umeyama_sim3_from_paths(pose_ref[best_inliers], pose_est[best_inliers]) + else: + r, t, s = best_model + return r, t, s + + +def align_poses_umeyama( + ext_ref: np.ndarray, + ext_est: np.ndarray, + return_aligned=False, + ransac=False, + sub_n=None, + inlier_thresh=None, + ransac_max_iters=10, + random_state=None, +): + """ + Align estimated trajectory to reference using Umeyama Sim(3). + Default no RANSAC; if ransac=True, use RANSAC (max iterations default 10). + - sub_n defaults to half the number of frames (rounded up, at least 3) + - inlier_thresh defaults to median of "distance from each estimated pose to + nearest reference pose after pre-alignment" + Returns rotation (3x3), translation (3,), scale; optionally returns aligned extrinsics (4x4). + """ + pose_ref, pose_est = _poses_from_ext(ext_ref, ext_est) + + if not ransac: + r, t, s, pose_est_aligned = _umeyama_sim3_from_paths(pose_ref, pose_est) + else: + r, t, s = _ransac_align_sim3( + pose_ref, + pose_est, + sub_n=sub_n, + inlier_thresh=inlier_thresh, + max_iters=ransac_max_iters, + random_state=random_state, + ) + pose_est_aligned = _apply_sim3_to_poses(pose_est, r, t, s) + + if return_aligned: + ext_est_aligned = affine_inverse_np(pose_est_aligned) + return r, t, s, ext_est_aligned + return r, t, s + + +# def align_poses_umeyama(ext_ref: np.ndarray, ext_est: np.ndarray, return_aligned=False): +# """ +# Align estimated trajectory to reference trajectory using Umeyama Sim(3) +# alignment (via evo PosePath3D). # noqa +# Returns rotation, translation, and scale. +# """ +# # If input extrinsics are 3x4, convert to 4x4 by padding +# if ext_ref.shape[1] == 3: +# ext_ref_ = np.eye(4)[None].repeat(len(ext_ref), 0) +# ext_ref_[:, :3] = ext_ref +# ext_ref = ext_ref_ +# if ext_est.shape[1] == 3: +# ext_est_ = np.eye(4)[None].repeat(len(ext_est), 0) +# ext_est_[:, :3] = ext_est +# ext_est = ext_est_ + +# # Convert to camera poses (inverse extrinsics) +# pose_ref = affine_inverse_np(ext_ref) +# pose_est = affine_inverse_np(ext_est) + +# # Create evo PosePath3D objects +# path_ref = PosePath3D(poses_se3=pose_ref) +# path_est = PosePath3D(poses_se3=pose_est) +# r, t, s = path_est.align(path_ref, correct_scale=True) +# if return_aligned: +# return r, t, s, affine_inverse_np(np.stack(path_est.poses_se3)) +# else: +# return r, t, s + + +def apply_umeyama_alignment_to_ext( + rot: np.ndarray, # (3,3) + trans: np.ndarray, # (3,) or (1,3) + scale: float, + ext_est: np.ndarray, # (...,4,4) or (...,3,4) +) -> np.ndarray: + """ + Apply Sim(3) (R, t, s) to a batch of world-to-camera extrinsics ext_est. + Returns the aligned extrinsics, with the same shape as input. + """ + + # Allow 3x4 extrinsics: pad to 4x4 + if ext_est.shape[-2:] == (3, 4): + pad = np.zeros((*ext_est.shape[:-2], 4, 4), dtype=ext_est.dtype) + pad[..., :3, :4] = ext_est + pad[..., 3, 3] = 1.0 + ext_est = pad + + # Convert world-to-camera to camera-to-world + pose_est = affine_inverse_np(ext_est) # (...,4,4) + R_e = pose_est[..., :3, :3] # (...,3,3) + t_e = pose_est[..., :3, 3] # (...,3) + + # Apply Sim(3) transformation + R_a = np.einsum("ij,...jk->...ik", rot, R_e) # (...,3,3) + t_a = scale * np.einsum("ij,...j->...i", rot, t_e) + trans # (...,3) + + # Assemble the transformed pose + pose_a = np.zeros_like(pose_est) + pose_a[..., :3, :3] = R_a + pose_a[..., :3, 3] = t_a + pose_a[..., 3, 3] = 1.0 + + # Convert back to world-to-camera + return affine_inverse_np(pose_a) + + +def transform_points_sim3(points, rot, trans, scale, inverse=False): + """ + Sim(3) transform point cloud + points: (N, 3) + rot: (3, 3) + trans: (3,) or (1, 3) + scale: float + inverse: Whether to do inverse transform (ref->est) + Returns: (N, 3) + """ + if not inverse: + # Forward: est -> ref + return scale * (points @ rot.T) + trans + else: + # Inverse: ref -> est + return ((points - trans) @ rot) / scale + + +def _rand_rot(): + u1, u2, u3 = np.random.rand(3) + q = np.array( + [ + np.sqrt(1 - u1) * np.sin(2 * np.math.pi * u2), + np.sqrt(1 - u1) * np.cos(2 * np.math.pi * u2), + np.sqrt(u1) * np.sin(2 * np.math.pi * u3), + np.sqrt(u1) * np.cos(2 * np.math.pi * u3), + ] + ) + w, x, y, z = q + return np.array( + [ + [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], + [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], + [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], + ] + ) + + +def _rand_pose(): + R, t = _rand_rot(), np.random.randn(3) + P = np.eye(4) + P[:3, :3] = R + P[:3, 3] = t + return P + + +if __name__ == "__main__": + np.random.seed(42) + # 1. Randomly generate reference trajectory and Sim(3) + N = 8 + pose_ref = np.stack([_rand_pose() for _ in range(N)]) # (N,4,4) cam→world + rot_gt = _rand_rot() + scale_gt = 2.3 + trans_gt = np.random.randn(3) + # 2. Generate estimated trajectory (apply Sim(3)) + pose_est = np.zeros_like(pose_ref) + for i in range(N): + R = pose_ref[i][:3, :3] + t = pose_ref[i][:3, 3] + pose_est[i][:3, :3] = rot_gt @ R + pose_est[i][:3, 3] = scale_gt * (rot_gt @ t) + trans_gt + pose_est[i][3, 3] = 1.0 + # 3. Get extrinsics (world->cam) + ext_ref = affine_inverse_np(pose_ref) + ext_est = affine_inverse_np(pose_est) + # 4. Use umeyama alignment, estimate Sim(3) + r_est, t_est, s_est = align_poses_umeyama(ext_ref, ext_est) + print("GT scale:", scale_gt, "Estimated:", s_est) + print("GT trans:", trans_gt, "Estimated:", t_est) + print("GT rot:\n", rot_gt, "\nEstimated:\n", r_est) + # 5. Random point cloud, in ref frame + num_points = 100 + points_ref = np.random.randn(num_points, 3) + # 6. Use GT Sim(3) inverse transform to est frame + points_est = transform_points_sim3(points_ref, rot_gt, trans_gt, scale_gt, inverse=True) + # 7. Use estimated Sim(3) forward transform back to ref frame + points_ref_recovered = transform_points_sim3(points_est, r_est, t_est, s_est, inverse=False) + # 8. Check error + err = np.abs(points_ref_recovered - points_ref) + print("Point cloud sim3 transform error (mean abs):", err.mean()) + print("Point cloud sim3 transform error (max abs):", err.max()) + assert err.mean() < 1e-6, "Mean sim3 transform error too large!" + assert err.max() < 1e-5, "Max sim3 transform error too large!" + print("Sim(3) point cloud transform & alignment test passed!") diff --git a/src/depth_anything_3/utils/ray_utils.py b/src/depth_anything_3/utils/ray_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5a90042113cd15f04e2fee03015edfd741ddadb2 --- /dev/null +++ b/src/depth_anything_3/utils/ray_utils.py @@ -0,0 +1,524 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from einops import repeat + +from .geometry import unproject_depth + + +def compute_optimal_rotation_intrinsics_batch( + rays_origin, rays_target, z_threshold=1e-4, reproj_threshold=0.2, weights=None, + n_sample = None, + n_iter=100, + num_sample_for_ransac=8, + rand_sample_iters_idx=None, +): + """ + Args: + rays_origin (torch.Tensor): (B, N, 3) + rays_target (torch.Tensor): (B, N, 3) + z_threshold (float): Threshold for z value to be considered valid. + + Returns: + R (torch.tensor): (3, 3) + focal_length (torch.tensor): (2,) + principal_point (torch.tensor): (2,) + """ + device = rays_origin.device + B, N, _ = rays_origin.shape + z_mask = torch.logical_and( + torch.abs(rays_target[:, :, 2]) > z_threshold, torch.abs(rays_origin[:, :, 2]) > z_threshold + ) # (B, N, 1) + rays_origin = rays_origin.clone() + rays_target = rays_target.clone() + rays_origin[:, :, 0][z_mask] /= rays_origin[:, :, 2][z_mask] + rays_origin[:, :, 1][z_mask] /= rays_origin[:, :, 2][z_mask] + rays_target[:, :, 0][z_mask] /= rays_target[:, :, 2][z_mask] + rays_target[:, :, 1][z_mask] /= rays_target[:, :, 2][z_mask] + + rays_origin = rays_origin[:, :, :2] + rays_target = rays_target[:, :, :2] + assert weights is not None, "weights must be provided" + weights[~z_mask] = 0 + + A_list = [] + max_chunk_size = 2 + for i in range(0, rays_origin.shape[0], max_chunk_size): + A = ransac_find_homography_weighted_fast_batch( + rays_origin[i:i+max_chunk_size], + rays_target[i:i+max_chunk_size], + weights[i:i+max_chunk_size], + n_iter=n_iter, + n_sample = n_sample, + num_sample_for_ransac=num_sample_for_ransac, + reproj_threshold=reproj_threshold, + rand_sample_iters_idx=rand_sample_iters_idx, + max_inlier_num=8000, + ) + A = A.to(device) + A_need_inv_mask = torch.linalg.det(A) < 0 + A[A_need_inv_mask] = -A[A_need_inv_mask] + A_list.append(A) + + A = torch.cat(A_list, dim=0) + + R_list = [] + f_list = [] + pp_list = [] + for i in range(A.shape[0]): + R, L = ql_decomposition(A[i]) + L = L / L[2][2] + + f = torch.stack((L[0][0], L[1][1])) + pp = torch.stack((L[2][0], L[2][1])) + R_list.append(R) + f_list.append(f) + pp_list.append(pp) + + R = torch.stack(R_list) + f = torch.stack(f_list) + pp = torch.stack(pp_list) + + return R, f, pp + + +# https://www.reddit.com/r/learnmath/comments/v1crd7/linear_algebra_qr_to_ql_decomposition/ +def ql_decomposition(A): + P = torch.tensor([[0, 0, 1], [0, 1, 0], [1, 0, 0]], device=A.device).float() + A_tilde = torch.matmul(A, P) + Q_tilde, R_tilde = torch.linalg.qr(A_tilde) + Q = torch.matmul(Q_tilde, P) + L = torch.matmul(torch.matmul(P, R_tilde), P) + d = torch.diag(L) + Q[:, 0] *= torch.sign(d[0]) + Q[:, 1] *= torch.sign(d[1]) + Q[:, 2] *= torch.sign(d[2]) + L[0] *= torch.sign(d[0]) + L[1] *= torch.sign(d[1]) + L[2] *= torch.sign(d[2]) + return Q, L + +def find_homography_least_squares_weighted_torch(src_pts, dst_pts, confident_weight): + """ + src_pts: (N,2) source points (torch.Tensor, float32/float64) + dst_pts: (N,2) target points (torch.Tensor, float32/float64) + confident_weight: (N,) weights (torch.Tensor) + Returns: (3,3) homography matrix H (torch.Tensor) + """ + assert src_pts.shape == dst_pts.shape + N = src_pts.shape[0] + if N < 4: + raise ValueError("At least 4 points are required to compute homography.") + assert confident_weight.shape == (N,) + + w = confident_weight.sqrt().unsqueeze(1) # (N,1) + + x = src_pts[:, 0:1] # (N,1) + y = src_pts[:, 1:2] # (N,1) + u = dst_pts[:, 0:1] + v = dst_pts[:, 1:2] + + zeros = torch.zeros_like(x) + + # Construct A matrix (2N, 9) + A1 = torch.cat([-x * w, -y * w, -w, zeros, zeros, zeros, x * u * w, y * u * w, u * w], dim=1) + A2 = torch.cat([zeros, zeros, zeros, -x * w, -y * w, -w, x * v * w, y * v * w, v * w], dim=1) + A = torch.cat([A1, A2], dim=0) # (2N, 9) + + # SVD + # Note: torch.linalg.svd returns U, S, Vh, where Vh is the transpose of V + _, _, Vh = torch.linalg.svd(A) + H = Vh[-1].reshape(3, 3) + H = H / H[-1, -1] + return H + + +def ransac_find_homography_weighted( + src_pts, + dst_pts, + confident_weight, + n_iter=100, + sample_ratio=0.2, + reproj_threshold=3.0, + num_sample_for_ransac=16, + random_seed=None, +): + """ + RANSAC version of weighted Homography estimation. + Sample 4 points from the top 50% weighted points each time. + reproj_threshold: points with reprojection error less than this value are inliers + Returns: best_H + """ + if random_seed is not None: + torch.manual_seed(random_seed) + N = src_pts.shape[0] + assert N >= 4 + # 1. Select top 50% weighted points + sorted_idx = torch.argsort(confident_weight, descending=True) + n_sample = max(num_sample_for_ransac, int(N * sample_ratio)) + candidate_idx = sorted_idx[:n_sample] + best_inlier_mask = None + best_score = 0 + for _ in range(n_iter): + # 2. Randomly sample 4 points + idx = candidate_idx[torch.randperm(n_sample)[:num_sample_for_ransac]] + # 3. Compute Homography + try: + H = find_homography_least_squares_weighted_torch( + src_pts[idx], dst_pts[idx], confident_weight[idx] + ) + except Exception: + H = torch.eye(3, dtype=src_pts.dtype, device=src_pts.device) + # 4. Compute reprojection error for all points + src_homo = torch.cat( + [src_pts, torch.ones(N, 1, dtype=src_pts.dtype, device=src_pts.device)], dim=1 + ) + proj = (H @ src_homo.T).T + proj = proj[:, :2] / proj[:, 2:3] + error = ((proj - dst_pts) ** 2).sum(dim=1).sqrt() # Euclidean distance + inlier_mask = error < reproj_threshold + total_score = (inlier_mask * confident_weight).sum().item() + n_inlier = inlier_mask.sum().item() + if n_inlier < 4: + continue # At least 4 inliers required for fitting + + if total_score > best_score: + best_score = total_score + best_inlier_mask = inlier_mask + + # 5. Refit Homography using inliers + H_inlier = find_homography_least_squares_weighted_torch( + src_pts[best_inlier_mask], dst_pts[best_inlier_mask], confident_weight[best_inlier_mask] + ) + + return H_inlier + + +def find_homography_least_squares_weighted_torch_batch( + src_pts_batch, dst_pts_batch, confident_weight_batch +): + """ + Batch version of weighted least squares Homography + src_pts_batch: (B, K, 2) + dst_pts_batch: (B, K, 2) + confident_weight_batch: (B, K) + Returns: (B, 3, 3) + """ + B, K, _ = src_pts_batch.shape + w = confident_weight_batch.sqrt().unsqueeze(2) # (B,K,1) + x = src_pts_batch[:, :, 0:1] + y = src_pts_batch[:, :, 1:2] + u = dst_pts_batch[:, :, 0:1] + v = dst_pts_batch[:, :, 1:2] + zeros = torch.zeros_like(x) + A1 = torch.cat([-x * w, -y * w, -w, zeros, zeros, zeros, x * u * w, y * u * w, u * w], dim=2) + A2 = torch.cat([zeros, zeros, zeros, -x * w, -y * w, -w, x * v * w, y * v * w, v * w], dim=2) + A = torch.cat([A1, A2], dim=1) # (B, 2K, 9) + # SVD: torch.linalg.svd supports batch + _, _, Vh = torch.linalg.svd(A) + H = Vh[:, -1].reshape(B, 3, 3) + H = H / H[:, 2:3, 2:3] + return H + + +def ransac_find_homography_weighted_fast( + src_pts, + dst_pts, + confident_weight, + n_sample, + n_iter=100, + reproj_threshold=3.0, + num_sample_for_ransac=8, + random_seed=None, + rand_sample_iters_idx=None, +): + """ + Batch version of RANSAC weighted Homography estimation. + Returns: H_inlier + """ + if random_seed is not None: + torch.manual_seed(random_seed) + N = src_pts.shape[0] + device = src_pts.device + assert N >= 4 + # 1. Select top weighted points by sample_ratio + sorted_idx = torch.argsort(confident_weight, descending=True) + candidate_idx = sorted_idx[:n_sample] # (n_sample,) + if rand_sample_iters_idx is None: + rand_sample_iters_idx = torch.stack( + [torch.randperm(n_sample, device=device)[:num_sample_for_ransac] for _ in range(n_iter)], + dim=0, + ) # (n_iter, num_sample_for_ransac) + # 2. Generate all sampling groups at once + # shape: (n_iter, num_sample_for_ransac) + rand_idx = candidate_idx[rand_sample_iters_idx] # (n_iter, num_sample_for_ransac) + # 3. Construct batch input + src_pts_batch = src_pts[rand_idx] # (n_iter, num_sample_for_ransac, 2) + dst_pts_batch = dst_pts[rand_idx] # (n_iter, num_sample_for_ransac, 2) + confident_weight_batch = confident_weight[rand_idx] # (n_iter, num_sample_for_ransac) + # 4. Batch fit Homography + H_batch = find_homography_least_squares_weighted_torch_batch( + src_pts_batch, dst_pts_batch, confident_weight_batch + ) # (n_iter, 3, 3) + # 5. Batch evaluate inliers for all H + src_homo = torch.cat( + [src_pts, torch.ones(N, 1, dtype=src_pts.dtype, device=src_pts.device)], dim=1 + ) # (N,3) + src_homo_expand = src_homo.unsqueeze(0).expand(n_iter, N, 3) # (n_iter, N, 3) + dst_pts_expand = dst_pts.unsqueeze(0).expand(n_iter, N, 2) # (n_iter, N, 2) + confident_weight_expand = confident_weight.unsqueeze(0).expand(n_iter, N) # (n_iter, N) + # H_batch: (n_iter, 3, 3) + proj = torch.bmm(src_homo_expand, H_batch.transpose(1, 2)) # (n_iter, N, 3) + proj_xy = proj[:, :, :2] / proj[:, :, 2:3] # (n_iter, N, 2) + error = ((proj_xy - dst_pts_expand) ** 2).sum(dim=2).sqrt() # (n_iter, N) + inlier_mask = error < reproj_threshold # (n_iter, N) + total_score = (inlier_mask * confident_weight_expand).sum(dim=1) # (n_iter,) + # 6. Select the sampling group with the highest score + best_idx = torch.argmax(total_score) + best_inlier_mask = inlier_mask[best_idx] # (N,) + inlier_src_pts = src_pts[best_inlier_mask] + inlier_dst_pts = dst_pts[best_inlier_mask] + inlier_confident_weight = confident_weight[best_inlier_mask] + + max_inlier_num = 10000 + sorted_idx = torch.argsort(inlier_confident_weight, descending=True) + + # method 1: sort according to confident_weight, and only keep max_inlier_num pts + # sorted_idx = sorted_idx[:max_inlier_num] + + # method 2: random choose max_inlier_num pts + sorted_idx = sorted_idx[torch.randperm(len(sorted_idx))[:max_inlier_num]] + + inlier_src_pts = inlier_src_pts[sorted_idx] + inlier_dst_pts = inlier_dst_pts[sorted_idx] + inlier_confident_weight = inlier_confident_weight[sorted_idx] + # 7. Refit Homography using inliers + H_inlier = find_homography_least_squares_weighted_torch( + inlier_src_pts, inlier_dst_pts, inlier_confident_weight + ) + return H_inlier + + +def ransac_find_homography_weighted_fast_batch( + src_pts, # (B, N, 3) + dst_pts, # (B, N, 2) + confident_weight, # (B, N) + n_sample, + n_iter=100, + reproj_threshold=3.0, + num_sample_for_ransac=8, + max_inlier_num=10000, + random_seed=None, + rand_sample_iters_idx=None, +): + """ + Batch version of RANSAC weighted Homography estimation (supports batch). + Input: + src_pts: (B, N, 2) + dst_pts: (B, N, 2) + confident_weight: (B, N) + Returns: + H_inlier: (B, 3, 3) + """ + if random_seed is not None: + torch.manual_seed(random_seed) + B, N, _ = src_pts.shape + assert N >= 4 + + device = src_pts.device + + # 1. Select top weighted points by sample_ratio + sorted_idx = torch.argsort(confident_weight, descending=True, dim=1) # (B, N) + candidate_idx = sorted_idx[:, :n_sample] # (B, n_sample) + + # 2. Generate all sampling groups at once + # rand_idx: (B, n_iter, num_sample_for_ransac) + if rand_sample_iters_idx is None: + rand_sample_iters_idx = torch.stack( + [torch.randperm(n_sample, device=device)[:num_sample_for_ransac] for _ in range(n_iter)], + dim=0, + ) # (n_iter, num_sample_for_ransac) + + rand_idx = candidate_idx[:, rand_sample_iters_idx] # (B, n_iter, num_sample_for_ransac) + + # 3. Construct batch input + # Indexing method below: (B, n_iter, num_sample_for_ransac, ...) + b_idx = torch.arange(B, device=device).view(B, 1, 1).expand(B, n_iter, num_sample_for_ransac) + src_pts_batch = src_pts[b_idx, rand_idx] # (B, n_iter, num_sample_for_ransac, 2) + dst_pts_batch = dst_pts[b_idx, rand_idx] # (B, n_iter, num_sample_for_ransac, 2) + confident_weight_batch = confident_weight[b_idx, rand_idx] # (B, n_iter, num_sample_for_ransac) + + # 4. Batch fit Homography + # Need to implement batch version that supports (B, n_iter, num_sample_for_ransac, ...) input + # Output H_batch: (B, n_iter, 3, 3) + cB, cN = src_pts_batch.shape[:2] + H_batch = find_homography_least_squares_weighted_torch_batch( + src_pts_batch.flatten(0, 1), dst_pts_batch.flatten(0, 1), confident_weight_batch.flatten(0, 1) + ) # (B, n_iter, 3, 3) + H_batch = H_batch.unflatten(0, (cB, cN)) + + # 5. Batch evaluate inliers for all H + src_homo = torch.cat( + [src_pts, torch.ones(B, N, 1, dtype=src_pts.dtype, device=src_pts.device)], dim=2 + ) # (B, N, 3) + src_homo_expand = src_homo.unsqueeze(1).expand(B, n_iter, N, 3) # (B, n_iter, N, 3) + dst_pts_expand = dst_pts.unsqueeze(1).expand(B, n_iter, N, 2) # (B, n_iter, N, 2) + confident_weight_expand = confident_weight.unsqueeze(1).expand(B, n_iter, N) # (B, n_iter, N) + + # H_batch: (B, n_iter, 3, 3) + # Need to reshape H_batch to (B*n_iter, 3, 3), src_homo_expand to (B*n_iter, N, 3) + H_batch_flat = H_batch.reshape(-1, 3, 3) + src_homo_expand_flat = src_homo_expand.reshape(-1, N, 3) + proj = torch.bmm(src_homo_expand_flat, H_batch_flat.transpose(1, 2)) # (B*n_iter, N, 3) + proj_xy = proj[:, :, :2] / proj[:, :, 2:3] # (B*n_iter, N, 2) + proj_xy = proj_xy.reshape(B, n_iter, N, 2) + error = ((proj_xy - dst_pts_expand) ** 2).sum(dim=3).sqrt() # (B, n_iter, N) + inlier_mask = error < reproj_threshold # (B, n_iter, N) + total_score = (inlier_mask * confident_weight_expand).sum(dim=2) # (B, n_iter) + + # 6. Select the sampling group with the highest score + best_idx = torch.argmax(total_score, dim=1) # (B,) + best_inlier_mask = inlier_mask[torch.arange(B, device=device), best_idx] # (B, N) + + # 7. Refit Homography using inliers + H_inlier_list = [] + for b in range(B): + mask = best_inlier_mask[b] + inlier_src_pts = src_pts[b][mask] # (?, 3) + inlier_dst_pts = dst_pts[b][mask] # (?, 2) + inlier_confident_weight = confident_weight[b][mask] # (?) + + sorted_idx = torch.argsort(inlier_confident_weight, descending=True) + # # method 1: sort according to confident_weight, and only keep max_inlier_num pts + # sorted_idx = sorted_idx[:max_inlier_num] + # method 2: random choose max_inlier_num pts + if len(sorted_idx) > max_inlier_num: + # random choose from first 95% confident pts + keep_len = max(int(len(sorted_idx) * 0.95), max_inlier_num) + sorted_idx = sorted_idx[:keep_len] + perm = torch.randperm(len(sorted_idx), device=device)[:max_inlier_num] + sorted_idx = sorted_idx[perm] + inlier_src_pts = inlier_src_pts[sorted_idx] + inlier_dst_pts = inlier_dst_pts[sorted_idx] + inlier_confident_weight = inlier_confident_weight[sorted_idx] + + H_inlier = find_homography_least_squares_weighted_torch( + inlier_src_pts, inlier_dst_pts, inlier_confident_weight + ) # (3, 3) + H_inlier_list.append(H_inlier) + H_inlier = torch.stack(H_inlier_list, dim=0) # (B, 3, 3) + return H_inlier + +def get_params_for_ransac(N, device): + n_iter=100 + sample_ratio=0.3 + num_sample_for_ransac=8 + n_sample = max(num_sample_for_ransac, int(N * sample_ratio)) + rand_sample_iters_idx = torch.stack( + [torch.randperm(n_sample, device=device)[:num_sample_for_ransac] for _ in range(n_iter)], + dim=0, + ) # (n_iter, num_sample_for_ransac) + return n_iter, num_sample_for_ransac, n_sample, rand_sample_iters_idx + + +def camray_to_caminfo(camray, confidence=None, reproj_threshold=0.2, training=False): + """ + Args: + camray: (B, S, num_patches_y, num_patches_x, 6) + confidence: (B, S, num_patches_y, num_patches_x) + Returns: + R: (B, S, 3, 3) + T: (B, S, 3) + focal_lengths: (B, S, 2) + principal_points: (B, S, 2) + """ + if confidence is None: + confidence = torch.ones_like(camray[:, :, :, :, 0]) + B, S, num_patches_y, num_patches_x, _ = camray.shape + # identity K, assume imw=imh=2.0 + I_K = torch.eye(3, dtype=camray.dtype, device=camray.device) + I_K[0, 2] = 1.0 + I_K[1, 2] = 1.0 + # repeat I_K to match camray + I_K = I_K.unsqueeze(0).unsqueeze(0).expand(B, S, -1, -1) + + cam_plane_depth = torch.ones( + B, S, num_patches_y, num_patches_x, 1, dtype=camray.dtype, device=camray.device + ) + I_cam_plane_unproj = unproject_depth( + cam_plane_depth, + I_K, + c2w=None, + ixt_normalized=True, + num_patches_x=num_patches_x, + num_patches_y=num_patches_y, + ) # (B, S, num_patches_y, num_patches_x, 3) + + camray = camray.flatten(0, 1).flatten(1, 2) # (B*S, num_patches_y*num_patches_x, 6) + I_cam_plane_unproj = I_cam_plane_unproj.flatten(0, 1).flatten( + 1, 2 + ) # (B*S, num_patches_y*num_patches_x, 3) + confidence = confidence.flatten(0, 1).flatten(1, 2) # (B*S, num_patches_y*num_patches_x) + + # Compute optimal rotation to align rays + N = camray.shape[-2] + device = camray.device + n_iter, num_sample_for_ransac, n_sample, rand_sample_iters_idx = get_params_for_ransac(N, device) + + # Use batch processing (confidence is guaranteed to be not None at this point) + if training: + camray = camray.clone().detach() + I_cam_plane_unproj = I_cam_plane_unproj.clone().detach() + confidence = confidence.clone().detach() + R, focal_lengths, principal_points = compute_optimal_rotation_intrinsics_batch( + I_cam_plane_unproj, + camray[:, :, :3], + reproj_threshold=reproj_threshold, + weights=confidence, + n_sample = n_sample, + n_iter=n_iter, + num_sample_for_ransac=num_sample_for_ransac, + rand_sample_iters_idx=rand_sample_iters_idx, + ) + + T = torch.sum(camray[:, :, 3:] * confidence.unsqueeze(-1), dim=1) / torch.sum( + confidence, dim=-1, keepdim=True + ) + + R = R.reshape(B, S, 3, 3) + T = T.reshape(B, S, 3) + focal_lengths = focal_lengths.reshape(B, S, 2) + principal_points = principal_points.reshape(B, S, 2) + + return R, T, 1.0 / focal_lengths, principal_points + 1.0 + +def get_extrinsic_from_camray(camray, conf, patch_size_y, patch_size_x, training=False): + pred_R, pred_T, pred_focal_lengths, pred_principal_points = camray_to_caminfo( + camray, confidence=conf.squeeze(-1), training=training + ) + + pred_extrinsic = torch.cat( + [ + torch.cat([pred_R, pred_T.unsqueeze(-1)], dim=-1), + repeat( + torch.tensor([0, 0, 0, 1], dtype=pred_R.dtype, device=pred_R.device), + "c -> b s 1 c", + b=pred_R.shape[0], + s=pred_R.shape[1], + ), + ], + dim=-2, + ) # B, S, 4, 4 + return pred_extrinsic, pred_focal_lengths, pred_principal_points diff --git a/src/depth_anything_3/utils/read_write_model.py b/src/depth_anything_3/utils/read_write_model.py new file mode 100644 index 0000000000000000000000000000000000000000..e3a869e649974769831e0bb34d8750fb5af5cf00 --- /dev/null +++ b/src/depth_anything_3/utils/read_write_model.py @@ -0,0 +1,586 @@ +# Copyright (c), ETH Zurich and UNC Chapel Hill. +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# All rights reserved. +# +# This file has been modified by ByteDance Ltd. and/or its affiliates. on 11/05/2025 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +import argparse +import collections +import os +import struct + +import numpy as np + +CameraModel = collections.namedtuple("CameraModel", ["model_id", "model_name", "num_params"]) +Camera = collections.namedtuple("Camera", ["id", "model", "width", "height", "params"]) +BaseImage = collections.namedtuple( + "Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"] +) +Point3D = collections.namedtuple( + "Point3D", ["id", "xyz", "rgb", "error", "image_ids", "point2D_idxs"] +) + + +class Image(BaseImage): + def qvec2rotmat(self): + return qvec2rotmat(self.qvec) + + +CAMERA_MODELS = { + CameraModel(model_id=0, model_name="SIMPLE_PINHOLE", num_params=3), + CameraModel(model_id=1, model_name="PINHOLE", num_params=4), + CameraModel(model_id=2, model_name="SIMPLE_RADIAL", num_params=4), + CameraModel(model_id=3, model_name="RADIAL", num_params=5), + CameraModel(model_id=4, model_name="OPENCV", num_params=8), + CameraModel(model_id=5, model_name="OPENCV_FISHEYE", num_params=8), + CameraModel(model_id=6, model_name="FULL_OPENCV", num_params=12), + CameraModel(model_id=7, model_name="FOV", num_params=5), + CameraModel(model_id=8, model_name="SIMPLE_RADIAL_FISHEYE", num_params=4), + CameraModel(model_id=9, model_name="RADIAL_FISHEYE", num_params=5), + CameraModel(model_id=10, model_name="THIN_PRISM_FISHEYE", num_params=12), +} +CAMERA_MODEL_IDS = {camera_model.model_id: camera_model for camera_model in CAMERA_MODELS} +CAMERA_MODEL_NAMES = {camera_model.model_name: camera_model for camera_model in CAMERA_MODELS} + + +def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"): + """Read and unpack the next bytes from a binary file. + :param fid: + :param num_bytes: Sum of combination of {2, 4, 8}, e.g. 2, 6, 16, 30, etc. + :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. + :param endian_character: Any of {@, =, <, >, !} + :return: Tuple of read and unpacked values. + """ + data = fid.read(num_bytes) + return struct.unpack(endian_character + format_char_sequence, data) + + +def write_next_bytes(fid, data, format_char_sequence, endian_character="<"): + """pack and write to a binary file. + :param fid: + :param data: data to send, if multiple elements are sent at the same time, + they should be encapsuled either in a list or a tuple + :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. + should be the same length as the data list or tuple + :param endian_character: Any of {@, =, <, >, !} + """ + if isinstance(data, (list, tuple)): + bytes = struct.pack(endian_character + format_char_sequence, *data) + else: + bytes = struct.pack(endian_character + format_char_sequence, data) + fid.write(bytes) + + +def read_cameras_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasText(const std::string& path) + void Reconstruction::ReadCamerasText(const std::string& path) + """ + cameras = {} + with open(path) as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + camera_id = int(elems[0]) + model = elems[1] + width = int(elems[2]) + height = int(elems[3]) + params = np.array(tuple(map(float, elems[4:]))) + cameras[camera_id] = Camera( + id=camera_id, + model=model, + width=width, + height=height, + params=params, + ) + return cameras + + +def read_cameras_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasBinary(const std::string& path) + void Reconstruction::ReadCamerasBinary(const std::string& path) + """ + cameras = {} + with open(path_to_model_file, "rb") as fid: + num_cameras = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_cameras): + camera_properties = read_next_bytes(fid, num_bytes=24, format_char_sequence="iiQQ") + camera_id = camera_properties[0] + model_id = camera_properties[1] + model_name = CAMERA_MODEL_IDS[camera_properties[1]].model_name + width = camera_properties[2] + height = camera_properties[3] + num_params = CAMERA_MODEL_IDS[model_id].num_params + params = read_next_bytes( + fid, + num_bytes=8 * num_params, + format_char_sequence="d" * num_params, + ) + cameras[camera_id] = Camera( + id=camera_id, + model=model_name, + width=width, + height=height, + params=np.array(params), + ) + assert len(cameras) == num_cameras + return cameras + + +def write_cameras_text(cameras, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasText(const std::string& path) + void Reconstruction::ReadCamerasText(const std::string& path) + """ + HEADER = ( + "# Camera list with one line of data per camera:\n" + + "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n" + + f"# Number of cameras: {len(cameras)}\n" + ) + with open(path, "w") as fid: + fid.write(HEADER) + for _, cam in cameras.items(): + to_write = [cam.id, cam.model, cam.width, cam.height, *cam.params] + line = " ".join([str(elem) for elem in to_write]) + fid.write(line + "\n") + + +def write_cameras_binary(cameras, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::WriteCamerasBinary(const std::string& path) + void Reconstruction::ReadCamerasBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(cameras), "Q") + for _, cam in cameras.items(): + model_id = CAMERA_MODEL_NAMES[cam.model].model_id + camera_properties = [cam.id, model_id, cam.width, cam.height] + write_next_bytes(fid, camera_properties, "iiQQ") + for p in cam.params: + write_next_bytes(fid, float(p), "d") + return cameras + + +def read_images_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesText(const std::string& path) + void Reconstruction::WriteImagesText(const std::string& path) + """ + images = {} + with open(path) as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + image_id = int(elems[0]) + qvec = np.array(tuple(map(float, elems[1:5]))) + tvec = np.array(tuple(map(float, elems[5:8]))) + camera_id = int(elems[8]) + image_name = elems[9] + elems = fid.readline().split() + xys = np.column_stack( + [ + tuple(map(float, elems[0::3])), + tuple(map(float, elems[1::3])), + ] + ) + point3D_ids = np.array(tuple(map(int, elems[2::3]))) + images[image_id] = Image( + id=image_id, + qvec=qvec, + tvec=tvec, + camera_id=camera_id, + name=image_name, + xys=xys, + point3D_ids=point3D_ids, + ) + return images + + +def read_images_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesBinary(const std::string& path) + void Reconstruction::WriteImagesBinary(const std::string& path) + """ + images = {} + with open(path_to_model_file, "rb") as fid: + num_reg_images = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_reg_images): + binary_image_properties = read_next_bytes( + fid, num_bytes=64, format_char_sequence="idddddddi" + ) + image_id = binary_image_properties[0] + qvec = np.array(binary_image_properties[1:5]) + tvec = np.array(binary_image_properties[5:8]) + camera_id = binary_image_properties[8] + binary_image_name = b"" + current_char = read_next_bytes(fid, 1, "c")[0] + while current_char != b"\x00": # look for the ASCII 0 entry + binary_image_name += current_char + current_char = read_next_bytes(fid, 1, "c")[0] + image_name = binary_image_name.decode("utf-8") + num_points2D = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[0] + x_y_id_s = read_next_bytes( + fid, + num_bytes=24 * num_points2D, + format_char_sequence="ddq" * num_points2D, + ) + xys = np.column_stack( + [ + tuple(map(float, x_y_id_s[0::3])), + tuple(map(float, x_y_id_s[1::3])), + ] + ) + point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3]))) + images[image_id] = Image( + id=image_id, + qvec=qvec, + tvec=tvec, + camera_id=camera_id, + name=image_name, + xys=xys, + point3D_ids=point3D_ids, + ) + return images + + +def write_images_text(images, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesText(const std::string& path) + void Reconstruction::WriteImagesText(const std::string& path) + """ + if len(images) == 0: + mean_observations = 0 + else: + mean_observations = sum((len(img.point3D_ids) for _, img in images.items())) / len(images) + HEADER = ( + "# Image list with two lines of data per image:\n" + + "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n" + + "# POINTS2D[] as (X, Y, POINT3D_ID)\n" + + "# Number of images: {}, mean observations per image: {}\n".format( + len(images), mean_observations + ) + ) + + with open(path, "w") as fid: + fid.write(HEADER) + for _, img in images.items(): + image_header = [ + img.id, + *img.qvec, + *img.tvec, + img.camera_id, + img.name, + ] + first_line = " ".join(map(str, image_header)) + fid.write(first_line + "\n") + + points_strings = [] + for xy, point3D_id in zip(img.xys, img.point3D_ids): + points_strings.append(" ".join(map(str, [*xy, point3D_id]))) + fid.write(" ".join(points_strings) + "\n") + + +def write_images_binary(images, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadImagesBinary(const std::string& path) + void Reconstruction::WriteImagesBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(images), "Q") + for _, img in images.items(): + write_next_bytes(fid, img.id, "i") + write_next_bytes(fid, img.qvec.tolist(), "dddd") + write_next_bytes(fid, img.tvec.tolist(), "ddd") + write_next_bytes(fid, img.camera_id, "i") + for char in img.name: + write_next_bytes(fid, char.encode("utf-8"), "c") + write_next_bytes(fid, b"\x00", "c") + write_next_bytes(fid, len(img.point3D_ids), "Q") + for xy, p3d_id in zip(img.xys, img.point3D_ids): + write_next_bytes(fid, [*xy, p3d_id], "ddq") + + +def read_points3D_text(path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DText(const std::string& path) + void Reconstruction::WritePoints3DText(const std::string& path) + """ + points3D = {} + with open(path) as fid: + while True: + line = fid.readline() + if not line: + break + line = line.strip() + if len(line) > 0 and line[0] != "#": + elems = line.split() + point3D_id = int(elems[0]) + xyz = np.array(tuple(map(float, elems[1:4]))) + rgb = np.array(tuple(map(int, elems[4:7]))) + error = float(elems[7]) + image_ids = np.array(tuple(map(int, elems[8::2]))) + point2D_idxs = np.array(tuple(map(int, elems[9::2]))) + points3D[point3D_id] = Point3D( + id=point3D_id, + xyz=xyz, + rgb=rgb, + error=error, + image_ids=image_ids, + point2D_idxs=point2D_idxs, + ) + return points3D + + +def read_points3D_binary(path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DBinary(const std::string& path) + void Reconstruction::WritePoints3DBinary(const std::string& path) + """ + points3D = {} + with open(path_to_model_file, "rb") as fid: + num_points = read_next_bytes(fid, 8, "Q")[0] + for _ in range(num_points): + binary_point_line_properties = read_next_bytes( + fid, num_bytes=43, format_char_sequence="QdddBBBd" + ) + point3D_id = binary_point_line_properties[0] + xyz = np.array(binary_point_line_properties[1:4]) + rgb = np.array(binary_point_line_properties[4:7]) + error = np.array(binary_point_line_properties[7]) + track_length = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[0] + track_elems = read_next_bytes( + fid, + num_bytes=8 * track_length, + format_char_sequence="ii" * track_length, + ) + image_ids = np.array(tuple(map(int, track_elems[0::2]))) + point2D_idxs = np.array(tuple(map(int, track_elems[1::2]))) + points3D[point3D_id] = Point3D( + id=point3D_id, + xyz=xyz, + rgb=rgb, + error=error, + image_ids=image_ids, + point2D_idxs=point2D_idxs, + ) + return points3D + + +def write_points3D_text(points3D, path): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DText(const std::string& path) + void Reconstruction::WritePoints3DText(const std::string& path) + """ + if len(points3D) == 0: + mean_track_length = 0 + else: + mean_track_length = sum((len(pt.image_ids) for _, pt in points3D.items())) / len(points3D) + HEADER = ( + "# 3D point list with one line of data per point:\n" + + "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n" + + "# Number of points: {}, mean track length: {}\n".format( + len(points3D), mean_track_length + ) + ) + + with open(path, "w") as fid: + fid.write(HEADER) + for _, pt in points3D.items(): + point_header = [pt.id, *pt.xyz, *pt.rgb, pt.error] + fid.write(" ".join(map(str, point_header)) + " ") + track_strings = [] + for image_id, point2D in zip(pt.image_ids, pt.point2D_idxs): + track_strings.append(" ".join(map(str, [image_id, point2D]))) + fid.write(" ".join(track_strings) + "\n") + + +def write_points3D_binary(points3D, path_to_model_file): + """ + see: src/colmap/scene/reconstruction.cc + void Reconstruction::ReadPoints3DBinary(const std::string& path) + void Reconstruction::WritePoints3DBinary(const std::string& path) + """ + with open(path_to_model_file, "wb") as fid: + write_next_bytes(fid, len(points3D), "Q") + for _, pt in points3D.items(): + write_next_bytes(fid, pt.id, "Q") + write_next_bytes(fid, pt.xyz.tolist(), "ddd") + write_next_bytes(fid, pt.rgb.tolist(), "BBB") + write_next_bytes(fid, pt.error, "d") + track_length = pt.image_ids.shape[0] + write_next_bytes(fid, track_length, "Q") + for image_id, point2D_id in zip(pt.image_ids, pt.point2D_idxs): + write_next_bytes(fid, [image_id, point2D_id], "ii") + + +def detect_model_format(path, ext): + if ( + os.path.isfile(os.path.join(path, "cameras" + ext)) + and os.path.isfile(os.path.join(path, "images" + ext)) + and os.path.isfile(os.path.join(path, "points3D" + ext)) + ): + print("Detected model format: '" + ext + "'") + return True + + return False + + +def read_model(path, ext=""): + # try to detect the extension automatically + if ext == "": + if detect_model_format(path, ".bin"): + ext = ".bin" + elif detect_model_format(path, ".txt"): + ext = ".txt" + else: + print("Provide model format: '.bin' or '.txt'") + return + + if ext == ".txt": + cameras = read_cameras_text(os.path.join(path, "cameras" + ext)) + images = read_images_text(os.path.join(path, "images" + ext)) + points3D = read_points3D_text(os.path.join(path, "points3D") + ext) + else: + cameras = read_cameras_binary(os.path.join(path, "cameras" + ext)) + images = read_images_binary(os.path.join(path, "images" + ext)) + points3D = read_points3D_binary(os.path.join(path, "points3D") + ext) + return cameras, images, points3D + + +def write_model(cameras, images, points3D, path, ext=".bin"): + if ext == ".txt": + write_cameras_text(cameras, os.path.join(path, "cameras" + ext)) + write_images_text(images, os.path.join(path, "images" + ext)) + write_points3D_text(points3D, os.path.join(path, "points3D") + ext) + else: + write_cameras_binary(cameras, os.path.join(path, "cameras" + ext)) + write_images_binary(images, os.path.join(path, "images" + ext)) + write_points3D_binary(points3D, os.path.join(path, "points3D") + ext) + return cameras, images, points3D + + +def qvec2rotmat(qvec): + return np.array( + [ + [ + 1 - 2 * qvec[2] ** 2 - 2 * qvec[3] ** 2, + 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3], + 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2], + ], + [ + 2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3], + 1 - 2 * qvec[1] ** 2 - 2 * qvec[3] ** 2, + 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1], + ], + [ + 2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2], + 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1], + 1 - 2 * qvec[1] ** 2 - 2 * qvec[2] ** 2, + ], + ] + ) + + +def rotmat2qvec(R): + Rxx, Ryx, Rzx, Rxy, Ryy, Rzy, Rxz, Ryz, Rzz = R.flat + K = ( + np.array( + [ + [Rxx - Ryy - Rzz, 0, 0, 0], + [Ryx + Rxy, Ryy - Rxx - Rzz, 0, 0], + [Rzx + Rxz, Rzy + Ryz, Rzz - Rxx - Ryy, 0], + [Ryz - Rzy, Rzx - Rxz, Rxy - Ryx, Rxx + Ryy + Rzz], + ] + ) + / 3.0 + ) + eigvals, eigvecs = np.linalg.eigh(K) + qvec = eigvecs[[3, 0, 1, 2], np.argmax(eigvals)] + if qvec[0] < 0: + qvec *= -1 + return qvec + + +def main(): + parser = argparse.ArgumentParser(description="Read and write COLMAP binary and text models") + parser.add_argument("--input_model", help="path to input model folder") + parser.add_argument( + "--input_format", + choices=[".bin", ".txt"], + help="input model format", + default="", + ) + parser.add_argument("--output_model", help="path to output model folder") + parser.add_argument( + "--output_format", + choices=[".bin", ".txt"], + help="output model format", + default=".txt", + ) + args = parser.parse_args() + + cameras, images, points3D = read_model(path=args.input_model, ext=args.input_format) + + print("num_cameras:", len(cameras)) + print("num_images:", len(images)) + print("num_points3D:", len(points3D)) + + if args.output_model is not None: + write_model( + cameras, + images, + points3D, + path=args.output_model, + ext=args.output_format, + ) + + +if __name__ == "__main__": + main() diff --git a/src/depth_anything_3/utils/registry.py b/src/depth_anything_3/utils/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..94e810757520135cbc622672c70109280ad8625a --- /dev/null +++ b/src/depth_anything_3/utils/registry.py @@ -0,0 +1,37 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +from addict import Dict + + +class Registry(Dict[str, Any]): + def __init__(self): + super().__init__() + self._map = Dict({}) + + def register(self, name=None): + def decorator(cls): + key = name or cls.__name__ + self._map[key] = cls + return cls + + return decorator + + def get(self, name): + return self._map[name] + + def all(self): + return self._map diff --git a/src/depth_anything_3/utils/sh_helpers.py b/src/depth_anything_3/utils/sh_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..dde30b5da6b8ea657816f63cc44926a68ff9821a --- /dev/null +++ b/src/depth_anything_3/utils/sh_helpers.py @@ -0,0 +1,89 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import isqrt + +import torch +from einops import einsum + +try: + from e3nn.o3 import matrix_to_angles, wigner_D +except ImportError: + from depth_anything_3.utils.logger import logger + + logger.warn("Dependency 'e3nn' not found. Required for rotating the camera space SH coeff") + + +def project_to_so3_strict(M: torch.Tensor) -> torch.Tensor: + if M.shape[-2:] != (3, 3): + raise ValueError("Input must be a batch of 3x3 matrices (i.e., shape [..., 3, 3]).") + + # 1. Compute SVD + U, S, Vh = torch.linalg.svd(M) + V = Vh.mH + + # 2. Handle reflection case (det = -1) + det_U = torch.det(U) + det_V = torch.det(V) + is_reflection = (det_U * det_V) < 0 + correction_sign = torch.where( + is_reflection[..., None], + torch.tensor([1, 1, -1.0], device=M.device, dtype=M.dtype), + torch.tensor([1, 1, 1.0], device=M.device, dtype=M.dtype), + ) + correction_matrix = torch.diag_embed(correction_sign) + U_corrected = U @ correction_matrix + R_so3_initial = U_corrected @ V.transpose(-2, -1) + + # 3. Explicitly ensure determinant is 1 (or extremely close) + current_det = torch.det(R_so3_initial) + det_correction_factor = torch.pow(current_det, -1 / 3)[..., None, None] + R_so3_final = R_so3_initial * det_correction_factor + + return R_so3_final + + +def rotate_sh( + sh_coefficients: torch.Tensor, # "*#batch n" + rotations: torch.Tensor, # "*#batch 3 3" +) -> torch.Tensor: # "*batch n" + # https://github.com/graphdeco-inria/gaussian-splatting/issues/176#issuecomment-2452412653 + device = sh_coefficients.device + dtype = sh_coefficients.dtype + + *_, n = sh_coefficients.shape + + with torch.autocast(device_type=rotations.device.type, enabled=False): + rotations_float32 = rotations.to(torch.float32) + + # switch axes: yzx -> xyz + P = torch.tensor([[0, 0, 1], [1, 0, 0], [0, 1, 0]]).unsqueeze(0).to(rotations_float32) + permuted_rotations = torch.linalg.inv(P) @ rotations_float32 @ P + + # ensure rotation has det == 1 in float32 type + permuted_rotations_so3 = project_to_so3_strict(permuted_rotations) + + alpha, beta, gamma = matrix_to_angles(permuted_rotations_so3) + result = [] + for degree in range(isqrt(n)): + with torch.device(device): + sh_rotations = wigner_D(degree, alpha, -beta, gamma).type(dtype) + sh_rotated = einsum( + sh_rotations, + sh_coefficients[..., degree**2 : (degree + 1) ** 2], + "... i j, ... j -> ... i", + ) + result.append(sh_rotated) + + return torch.cat(result, dim=-1) diff --git a/src/depth_anything_3/utils/visualize.py b/src/depth_anything_3/utils/visualize.py new file mode 100644 index 0000000000000000000000000000000000000000..8fd32bddf00e5461f674525e73653409270c0227 --- /dev/null +++ b/src/depth_anything_3/utils/visualize.py @@ -0,0 +1,120 @@ +# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import matplotlib +import numpy as np +import torch +from einops import rearrange + +from depth_anything_3.utils.logger import logger + + +def visualize_depth( + depth: np.ndarray, + depth_min=None, + depth_max=None, + percentile=2, + ret_minmax=False, + ret_type=np.uint8, + cmap="Spectral", +): + """ + Visualize a depth map using a colormap. + + Args: + depth: Input depth map array + depth_min: Minimum depth value for normalization. If None, uses percentile + depth_max: Maximum depth value for normalization. If None, uses percentile + percentile: Percentile for min/max computation if not provided + ret_minmax: Whether to return min/max depth values + ret_type: Return array type (uint8 or float) + cmap: Matplotlib colormap name to use + + Returns: + Colored depth visualization as numpy array + If ret_minmax=True, also returns depth_min and depth_max + """ + depth = depth.copy() + depth.copy() + valid_mask = depth > 0 + depth[valid_mask] = 1 / depth[valid_mask] + if depth_min is None: + if valid_mask.sum() <= 10: + depth_min = 0 + else: + depth_min = np.percentile(depth[valid_mask], percentile) + if depth_max is None: + if valid_mask.sum() <= 10: + depth_max = 0 + else: + depth_max = np.percentile(depth[valid_mask], 100 - percentile) + if depth_min == depth_max: + depth_min = depth_min - 1e-6 + depth_max = depth_max + 1e-6 + cm = matplotlib.colormaps[cmap] + depth = ((depth - depth_min) / (depth_max - depth_min)).clip(0, 1) + depth = 1 - depth + img_colored_np = cm(depth[None], bytes=False)[:, :, :, 0:3] # value from 0 to 1 + if ret_type == np.uint8: + img_colored_np = (img_colored_np[0] * 255.0).astype(np.uint8) + elif ret_type == np.float32 or ret_type == np.float64: + img_colored_np = img_colored_np[0] + else: + raise ValueError(f"Invalid return type: {ret_type}") + if ret_minmax: + return img_colored_np, depth_min, depth_max + else: + return img_colored_np + + +# GS video rendering visulization function, since it operates in Tensor space... + + +def vis_depth_map_tensor( + result: torch.Tensor, # "*batch height width" + color_map: str = "Spectral", +) -> torch.Tensor: # "*batch 3 height with" + """ + Color-map the depth map. + """ + far = result.reshape(-1)[:16_000_000].float().quantile(0.99).log().to(result) + try: + near = result[result > 0][:16_000_000].float().quantile(0.01).log().to(result) + except (RuntimeError, ValueError) as e: + logger.error(f"No valid depth values found. Reason: {e}") + near = torch.zeros_like(far) + result = result.log() + result = (result - near) / (far - near) + return apply_color_map_to_image(result, color_map) + + +def apply_color_map( + x: torch.Tensor, # " *batch" + color_map: str = "inferno", +) -> torch.Tensor: # "*batch 3" + cmap = matplotlib.cm.get_cmap(color_map) + + # Convert to NumPy so that Matplotlib color maps can be used. + mapped = cmap(x.float().detach().clip(min=0, max=1).cpu().numpy())[..., :3] + + # Convert back to the original format. + return torch.tensor(mapped, device=x.device, dtype=torch.float32) + + +def apply_color_map_to_image( + image: torch.Tensor, # "*batch height width" + color_map: str = "inferno", +) -> torch.Tensor: # "*batch 3 height with" + image = apply_color_map(image, color_map) + return rearrange(image, "... h w c -> ... c h w") diff --git a/src/depth_anything_3/utils/zero_copy.py b/src/depth_anything_3/utils/zero_copy.py new file mode 100644 index 0000000000000000000000000000000000000000..1d2e0a3b3306ddfd6ebeb90b21a4b412f0d84750 --- /dev/null +++ b/src/depth_anything_3/utils/zero_copy.py @@ -0,0 +1,169 @@ +# Copyright (c) 2025 Delanoe Pirard and/or its affiliates +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Zero-copy utilities for efficient tensor operations. + +Provides utilities to minimize memory copies between NumPy and PyTorch, +especially for CPU→GPU transfers. +""" + +from __future__ import annotations + +import numpy as np +import torch + + +def numpy_to_torch_zerocopy(arr: np.ndarray, dtype: torch.dtype | None = None, device: str | torch.device = "cpu") -> torch.Tensor: + """ + Convert NumPy array to PyTorch tensor with zero-copy when possible. + + Zero-copy is possible when: + 1. Array is C-contiguous + 2. Target device is CPU + 3. dtype is compatible + + For GPU transfers, this still saves one copy (CPU→pinned→GPU vs CPU→CPU→GPU). + + Args: + arr: Input NumPy array + dtype: Target PyTorch dtype (if None, infer from numpy dtype) + device: Target device ('cpu', 'cuda', 'mps') + + Returns: + PyTorch tensor on specified device + + Example: + >>> arr = np.random.rand(1000, 1000) + >>> tensor = numpy_to_torch_zerocopy(arr, device='cuda') + >>> # No intermediate copy on CPU if arr is C-contiguous + """ + # Check if zero-copy is possible + is_contiguous = arr.flags['C_CONTIGUOUS'] + + if not is_contiguous: + # Need to make contiguous copy anyway + arr = np.ascontiguousarray(arr) + + # Create tensor with zero-copy (shares memory on CPU) + tensor = torch.from_numpy(arr) + + # Apply dtype conversion if needed + if dtype is not None and tensor.dtype != dtype: + tensor = tensor.to(dtype) + + # Move to target device + if str(device) != "cpu": + # Use non_blocking for async transfer + tensor = tensor.to(device, non_blocking=True) + + return tensor + + +def ensure_pinned_memory(arr: np.ndarray) -> np.ndarray: + """ + Ensure NumPy array uses pinned (page-locked) memory for faster GPU transfers. + + Pinned memory allows DMA (Direct Memory Access) for faster CPU→GPU transfers. + Only beneficial for repeated transfers of the same data. + + Args: + arr: Input NumPy array + + Returns: + Array in pinned memory + + Note: + Pinned memory is a limited resource. Only use for frequently transferred data. + For CUDA devices only (no effect on MPS/CPU). + """ + if not torch.cuda.is_available(): + return arr + + # Convert to torch tensor with pinned memory + tensor = torch.from_numpy(arr).pin_memory() + + # Convert back to numpy (shares pinned memory) + # Note: This creates a new numpy array view over pinned memory + return tensor.numpy() + + +def stack_arrays_zerocopy(arrays: list[np.ndarray], dtype: np.dtype | None = None) -> np.ndarray: + """ + Stack list of arrays with minimal copying. + + Args: + arrays: List of NumPy arrays to stack + dtype: Target dtype (if None, use arrays[0].dtype) + + Returns: + Stacked array + + Note: + If all arrays already have compatible dtype and layout, + np.stack uses optimized C-level stacking. + """ + if not arrays: + raise ValueError("Cannot stack empty list") + + # Check if all arrays have compatible dtype + if dtype is None: + dtype = arrays[0].dtype + + # Ensure all arrays are C-contiguous with same dtype + # This may create copies, but better done once than repeatedly + arrays_contig = [] + for arr in arrays: + if arr.dtype != dtype or not arr.flags['C_CONTIGUOUS']: + arr = np.ascontiguousarray(arr, dtype=dtype) + arrays_contig.append(arr) + + # Stack (single memory allocation + copy) + return np.stack(arrays_contig, axis=0) + + +def batch_to_device( + tensors: list[torch.Tensor] | tuple[torch.Tensor, ...], + device: str | torch.device, + non_blocking: bool = True +) -> list[torch.Tensor]: + """ + Move multiple tensors to device with optimal settings. + + Args: + tensors: List/tuple of tensors to move + device: Target device + non_blocking: Use async transfer (default: True) + + Returns: + List of tensors on target device + + Example: + >>> tensors = [torch.rand(100), torch.rand(200)] + >>> gpu_tensors = batch_to_device(tensors, 'cuda') + """ + return [t.to(device, non_blocking=non_blocking) if t is not None else None for t in tensors] + + +def get_optimal_pin_memory() -> bool: + """ + Determine if pin_memory should be used for DataLoader. + + Returns: + True if CUDA is available and pinned memory is beneficial + + Usage: + >>> DataLoader(dataset, pin_memory=get_optimal_pin_memory()) + """ + return torch.cuda.is_available() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..39da0e577db4b15c0e43e584bdf2401b6f7afef9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +"""Tests for Depth Anything 3.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..fbc9bfe00e73345c5bcd04bd2f2ff488fdc6a308 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,39 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Pytest configuration and shared fixtures for Depth Anything 3 tests. +""" +from __future__ import annotations + +import pytest +import torch + + +@pytest.fixture +def cpu_device(): + """Return CPU device.""" + return torch.device("cpu") + + +@pytest.fixture +def mock_cuda_device(): + """Return mock CUDA device (doesn't require actual CUDA).""" + return torch.device("cuda:0") + + +@pytest.fixture +def mock_mps_device(): + """Return mock MPS device (doesn't require actual MPS).""" + return torch.device("mps") + + +@pytest.fixture +def sample_image_paths(): + """Create sample image paths for testing.""" + return [f"image_{i}.jpg" for i in range(10)] + + +@pytest.fixture +def large_sample_image_paths(): + """Create larger sample of image paths.""" + return [f"image_{i}.jpg" for i in range(100)] diff --git a/tests/test_adaptive_batching.py b/tests/test_adaptive_batching.py new file mode 100644 index 0000000000000000000000000000000000000000..c77f51f6a8d9b88e0f5f7d083bfa525dc85780d9 --- /dev/null +++ b/tests/test_adaptive_batching.py @@ -0,0 +1,872 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Comprehensive tests for the adaptive batching module. + +Tests cover: +- ModelMemoryProfile dataclass +- Memory utility functions +- AdaptiveBatchSizeCalculator +- BatchInfo and adaptive_batch_iterator +- High-level API functions +- Edge cases and error handling +""" +from __future__ import annotations + +import os +from unittest.mock import MagicMock, patch + +import pytest +import torch + +from depth_anything_3.utils.adaptive_batching import ( + MODEL_MEMORY_PROFILES, + AdaptiveBatchConfig, + AdaptiveBatchSizeCalculator, + BatchInfo, + ModelMemoryProfile, + adaptive_batch_iterator, + estimate_max_batch_size, + get_available_memory_mb, + get_total_memory_mb, + log_batch_plan, + process_with_adaptive_batching, +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def cpu_device(): + """Return CPU device.""" + return torch.device("cpu") + + +@pytest.fixture +def mock_cuda_device(): + """Return mock CUDA device.""" + return torch.device("cuda:0") + + +@pytest.fixture +def mock_mps_device(): + """Return mock MPS device.""" + return torch.device("mps") + + +@pytest.fixture +def default_config(): + """Return default adaptive batch config.""" + return AdaptiveBatchConfig() + + +@pytest.fixture +def calculator_cpu(cpu_device): + """Return calculator for CPU.""" + return AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + +# ============================================================================= +# ModelMemoryProfile Tests +# ============================================================================= + + +class TestModelMemoryProfile: + """Tests for ModelMemoryProfile dataclass.""" + + def test_default_values(self): + """Test default values are set correctly.""" + profile = ModelMemoryProfile( + base_memory_mb=1000, + per_image_mb_at_504=500, + ) + assert profile.base_memory_mb == 1000 + assert profile.per_image_mb_at_504 == 500 + assert profile.activation_scale == 1.0 + assert profile.safety_margin == 0.15 + + def test_custom_values(self): + """Test custom values override defaults.""" + profile = ModelMemoryProfile( + base_memory_mb=2000, + per_image_mb_at_504=800, + activation_scale=1.5, + safety_margin=0.2, + ) + assert profile.base_memory_mb == 2000 + assert profile.per_image_mb_at_504 == 800 + assert profile.activation_scale == 1.5 + assert profile.safety_margin == 0.2 + + def test_all_models_have_profiles(self): + """Test that all expected models have memory profiles.""" + expected_models = [ + "da3-small", + "da3-base", + "da3-large", + "da3-giant", + "da3metric-large", + "da3mono-large", + "da3nested-giant-large", + ] + for model_name in expected_models: + assert model_name in MODEL_MEMORY_PROFILES + profile = MODEL_MEMORY_PROFILES[model_name] + assert profile.base_memory_mb > 0 + assert profile.per_image_mb_at_504 > 0 + + def test_profiles_size_ordering(self): + """Test that model profiles have expected size ordering.""" + small = MODEL_MEMORY_PROFILES["da3-small"] + base = MODEL_MEMORY_PROFILES["da3-base"] + large = MODEL_MEMORY_PROFILES["da3-large"] + giant = MODEL_MEMORY_PROFILES["da3-giant"] + + # Base memory should increase with model size + assert small.base_memory_mb < base.base_memory_mb + assert base.base_memory_mb < large.base_memory_mb + assert large.base_memory_mb < giant.base_memory_mb + + # Per-image memory should also increase + assert small.per_image_mb_at_504 < base.per_image_mb_at_504 + assert base.per_image_mb_at_504 < large.per_image_mb_at_504 + assert large.per_image_mb_at_504 < giant.per_image_mb_at_504 + + +# ============================================================================= +# Memory Utility Tests +# ============================================================================= + + +class TestGetAvailableMemory: + """Tests for get_available_memory_mb function.""" + + def test_cpu_returns_infinity(self, cpu_device): + """CPU should return infinite memory.""" + result = get_available_memory_mb(cpu_device) + assert result == float("inf") + + @patch("torch.cuda.is_available", return_value=True) + @patch("torch.cuda.synchronize") + @patch("torch.cuda.get_device_properties") + @patch("torch.cuda.memory_reserved") + def test_cuda_memory_calculation( + self, + mock_reserved, + mock_properties, + mock_sync, + mock_available, + mock_cuda_device, + ): + """Test CUDA memory calculation.""" + # Setup mocks + mock_props = MagicMock() + mock_props.total_memory = 16 * 1024 * 1024 * 1024 # 16 GB + mock_properties.return_value = mock_props + mock_reserved.return_value = 4 * 1024 * 1024 * 1024 # 4 GB reserved + + result = get_available_memory_mb(mock_cuda_device) + + # Should be (16GB - 4GB) in MB = 12288 MB + expected = (16 - 4) * 1024 + assert result == expected + + def test_mps_memory_with_env_var(self, mock_mps_device, monkeypatch): + """Test MPS memory respects environment variable.""" + monkeypatch.setenv("DA3_MPS_MAX_MEMORY_GB", "16") + + with patch("torch.mps.current_allocated_memory", return_value=0): + result = get_available_memory_mb(mock_mps_device) + assert result == 16 * 1024 # 16 GB in MB + + def test_mps_memory_default(self, mock_mps_device, monkeypatch): + """Test MPS memory uses default when env var not set.""" + monkeypatch.delenv("DA3_MPS_MAX_MEMORY_GB", raising=False) + + with patch("torch.mps.current_allocated_memory", return_value=0): + result = get_available_memory_mb(mock_mps_device) + assert result == 8 * 1024 # Default 8 GB + + def test_mps_memory_subtracts_allocated(self, mock_mps_device, monkeypatch): + """Test MPS memory subtracts allocated memory.""" + monkeypatch.setenv("DA3_MPS_MAX_MEMORY_GB", "8") + + allocated_bytes = 2 * 1024 * 1024 * 1024 # 2 GB allocated + with patch("torch.mps.current_allocated_memory", return_value=allocated_bytes): + result = get_available_memory_mb(mock_mps_device) + expected = (8 - 2) * 1024 # 6 GB remaining + assert result == expected + + +class TestGetTotalMemory: + """Tests for get_total_memory_mb function.""" + + def test_cpu_returns_infinity(self, cpu_device): + """CPU should return infinite total memory.""" + result = get_total_memory_mb(cpu_device) + assert result == float("inf") + + @patch("torch.cuda.get_device_properties") + def test_cuda_total_memory(self, mock_properties, mock_cuda_device): + """Test CUDA total memory retrieval.""" + mock_props = MagicMock() + mock_props.total_memory = 24 * 1024 * 1024 * 1024 # 24 GB + mock_properties.return_value = mock_props + + result = get_total_memory_mb(mock_cuda_device) + assert result == 24 * 1024 # 24 GB in MB + + def test_mps_total_memory_env_var(self, mock_mps_device, monkeypatch): + """Test MPS total memory from environment variable.""" + monkeypatch.setenv("DA3_MPS_MAX_MEMORY_GB", "32") + result = get_total_memory_mb(mock_mps_device) + assert result == 32 * 1024 + + +# ============================================================================= +# AdaptiveBatchConfig Tests +# ============================================================================= + + +class TestAdaptiveBatchConfig: + """Tests for AdaptiveBatchConfig dataclass.""" + + def test_default_values(self): + """Test default configuration values.""" + config = AdaptiveBatchConfig() + assert config.min_batch_size == 1 + assert config.max_batch_size == 64 + assert config.target_memory_utilization == 0.85 + assert config.enable_profiling is True + assert config.profile_warmup_batches == 2 + + def test_custom_values(self): + """Test custom configuration values.""" + config = AdaptiveBatchConfig( + min_batch_size=2, + max_batch_size=32, + target_memory_utilization=0.90, + enable_profiling=False, + profile_warmup_batches=5, + ) + assert config.min_batch_size == 2 + assert config.max_batch_size == 32 + assert config.target_memory_utilization == 0.90 + assert config.enable_profiling is False + assert config.profile_warmup_batches == 5 + + +# ============================================================================= +# AdaptiveBatchSizeCalculator Tests +# ============================================================================= + + +class TestAdaptiveBatchSizeCalculator: + """Tests for AdaptiveBatchSizeCalculator class.""" + + def test_initialization_known_model(self, cpu_device): + """Test initialization with known model.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + assert calc.model_name == "da3-large" + assert calc.device == cpu_device + assert calc.profile == MODEL_MEMORY_PROFILES["da3-large"] + + def test_initialization_unknown_model_uses_fallback(self, cpu_device): + """Test initialization with unknown model falls back to da3-large.""" + calc = AdaptiveBatchSizeCalculator("unknown-model", cpu_device) + assert calc.profile == MODEL_MEMORY_PROFILES["da3-large"] + + def test_initialization_with_custom_config(self, cpu_device): + """Test initialization with custom config.""" + config = AdaptiveBatchConfig(max_batch_size=16) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + assert calc.config.max_batch_size == 16 + + def test_compute_optimal_batch_size_cpu(self, cpu_device): + """CPU should return min(num_images, max_batch_size).""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + # Small number of images + result = calc.compute_optimal_batch_size(num_images=10) + assert result == 10 + + # Large number of images + result = calc.compute_optimal_batch_size(num_images=100) + assert result == 64 # max_batch_size + + def test_compute_optimal_batch_size_respects_min(self, cpu_device): + """Batch size should not go below min_batch_size.""" + config = AdaptiveBatchConfig(min_batch_size=4) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + result = calc.compute_optimal_batch_size(num_images=2) + # For CPU, min(num_images, max) = 2, but min_batch is applied after GPU calc + # CPU returns min(num_images, max_batch_size) directly + assert result == 2 + + def test_compute_optimal_batch_size_respects_max(self, cpu_device): + """Batch size should not exceed max_batch_size.""" + config = AdaptiveBatchConfig(max_batch_size=8) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + result = calc.compute_optimal_batch_size(num_images=100) + assert result == 8 + + @patch("depth_anything_3.utils.adaptive_batching.get_available_memory_mb") + def test_compute_optimal_batch_size_memory_based( + self, mock_memory, mock_cuda_device + ): + """Test memory-based batch size calculation.""" + # 10GB available memory + mock_memory.return_value = 10000 + + calc = AdaptiveBatchSizeCalculator("da3-large", mock_cuda_device) + + result = calc.compute_optimal_batch_size(num_images=100, process_res=504) + + # Should compute based on memory + assert 1 <= result <= 64 + assert result < 100 # Should be less than num_images given memory constraints + + @patch("depth_anything_3.utils.adaptive_batching.get_available_memory_mb") + def test_compute_low_memory_returns_min(self, mock_memory, mock_cuda_device): + """Low memory should return min batch size.""" + # Only 500MB available (less than base memory for da3-large) + mock_memory.return_value = 500 + + calc = AdaptiveBatchSizeCalculator("da3-large", mock_cuda_device) + result = calc.compute_optimal_batch_size(num_images=100) + + assert result == 1 # min_batch_size + + def test_estimate_per_image_memory_resolution_scaling(self, cpu_device): + """Test that memory scales quadratically with resolution.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + mem_504 = calc._estimate_per_image_memory(504) + mem_1008 = calc._estimate_per_image_memory(1008) + + # Memory at 2x resolution should be ~4x (quadratic scaling) + ratio = mem_1008 / mem_504 + assert 3.5 <= ratio <= 4.5 # Allow some tolerance for activation_scale + + def test_update_from_profiling_warmup(self, cpu_device): + """Test that warmup batches are skipped during profiling.""" + config = AdaptiveBatchConfig(profile_warmup_batches=2) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + # First two batches (warmup) should be skipped + calc.update_from_profiling(batch_size=4, memory_used_mb=3000, process_res=504) + assert calc._measured_per_image_mb is None + + calc.update_from_profiling(batch_size=4, memory_used_mb=3000, process_res=504) + assert calc._measured_per_image_mb is None + + # Third batch should update + calc.update_from_profiling(batch_size=4, memory_used_mb=3000, process_res=504) + assert calc._measured_per_image_mb is not None + + def test_update_from_profiling_disabled(self, cpu_device): + """Test that profiling can be disabled.""" + config = AdaptiveBatchConfig(enable_profiling=False) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + for _ in range(5): + calc.update_from_profiling(batch_size=4, memory_used_mb=3000, process_res=504) + + assert calc._measured_per_image_mb is None + + def test_update_from_profiling_ema(self, cpu_device): + """Test exponential moving average in profiling.""" + config = AdaptiveBatchConfig(profile_warmup_batches=0) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + # First update + calc.update_from_profiling(batch_size=4, memory_used_mb=4000, process_res=504) + first_value = calc._measured_per_image_mb + + # Second update with different value + calc.update_from_profiling(batch_size=4, memory_used_mb=5000, process_res=504) + second_value = calc._measured_per_image_mb + + # EMA should smooth the values + assert second_value is not None + assert second_value != first_value + + def test_get_memory_estimate(self, cpu_device): + """Test memory estimation for batch.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + estimate = calc.get_memory_estimate(batch_size=4, process_res=504) + + # Should include base memory + per-image memory + expected_min = calc.profile.base_memory_mb + assert estimate > expected_min + assert estimate > calc.profile.base_memory_mb + + +# ============================================================================= +# BatchInfo Tests +# ============================================================================= + + +class TestBatchInfo: + """Tests for BatchInfo dataclass.""" + + def test_batch_info_creation(self): + """Test basic BatchInfo creation.""" + items = ["a", "b", "c"] + info = BatchInfo( + batch_idx=0, + start_idx=0, + end_idx=3, + items=items, + is_last=True, + ) + assert info.batch_idx == 0 + assert info.start_idx == 0 + assert info.end_idx == 3 + assert info.items == ["a", "b", "c"] + assert info.batch_size == 3 + assert info.is_last is True + + def test_batch_size_computed_from_items(self): + """Test that batch_size is computed from items.""" + info = BatchInfo( + batch_idx=0, + start_idx=0, + end_idx=5, + items=[1, 2, 3, 4, 5], + ) + assert info.batch_size == 5 + + def test_empty_batch(self): + """Test empty batch handling.""" + info = BatchInfo( + batch_idx=0, + start_idx=0, + end_idx=0, + items=[], + ) + assert info.batch_size == 0 + + +# ============================================================================= +# adaptive_batch_iterator Tests +# ============================================================================= + + +class TestAdaptiveBatchIterator: + """Tests for adaptive_batch_iterator function.""" + + def test_single_batch(self, calculator_cpu): + """Test single batch when all items fit.""" + items = list(range(10)) + batches = list(adaptive_batch_iterator(items, calculator_cpu)) + + assert len(batches) == 1 + assert batches[0].items == items + assert batches[0].is_last is True + + def test_multiple_batches(self, cpu_device): + """Test multiple batches with small max_batch_size.""" + config = AdaptiveBatchConfig(max_batch_size=3) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = list(range(10)) + batches = list(adaptive_batch_iterator(items, calc)) + + # Should have 4 batches: 3, 3, 3, 1 + assert len(batches) == 4 + assert batches[0].batch_size == 3 + assert batches[-1].batch_size == 1 + assert batches[-1].is_last is True + + def test_batch_indices_are_correct(self, cpu_device): + """Test that batch indices are sequential.""" + config = AdaptiveBatchConfig(max_batch_size=2) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = list(range(6)) + batches = list(adaptive_batch_iterator(items, calc)) + + for i, batch in enumerate(batches): + assert batch.batch_idx == i + + def test_start_end_indices_cover_all_items(self, cpu_device): + """Test that batches cover all items without gaps.""" + config = AdaptiveBatchConfig(max_batch_size=3) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = list(range(10)) + batches = list(adaptive_batch_iterator(items, calc)) + + # Verify no gaps + prev_end = 0 + for batch in batches: + assert batch.start_idx == prev_end + assert batch.end_idx > batch.start_idx + prev_end = batch.end_idx + + assert prev_end == len(items) + + def test_items_are_preserved(self, cpu_device): + """Test that all items are preserved in batches.""" + config = AdaptiveBatchConfig(max_batch_size=4) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + original_items = ["a", "b", "c", "d", "e", "f", "g"] + batches = list(adaptive_batch_iterator(original_items, calc)) + + # Collect all items from batches + collected = [] + for batch in batches: + collected.extend(batch.items) + + assert collected == original_items + + def test_empty_sequence(self, calculator_cpu): + """Test empty sequence returns no batches.""" + batches = list(adaptive_batch_iterator([], calculator_cpu)) + assert len(batches) == 0 + + def test_last_batch_flag(self, cpu_device): + """Test that only last batch has is_last=True.""" + config = AdaptiveBatchConfig(max_batch_size=2) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = list(range(5)) + batches = list(adaptive_batch_iterator(items, calc)) + + # All but last should be False + for batch in batches[:-1]: + assert batch.is_last is False + + # Last should be True + assert batches[-1].is_last is True + + +# ============================================================================= +# process_with_adaptive_batching Tests +# ============================================================================= + + +class TestProcessWithAdaptiveBatching: + """Tests for process_with_adaptive_batching function.""" + + def test_basic_processing(self, cpu_device): + """Test basic batch processing.""" + items = list(range(10)) + + def process_fn(batch): + return [x * 2 for x in batch] + + results = process_with_adaptive_batching( + items=items, + process_fn=process_fn, + model_name="da3-large", + device=cpu_device, + ) + + assert results == [x * 2 for x in items] + + def test_progress_callback(self, cpu_device): + """Test progress callback is called.""" + items = list(range(10)) + progress_calls = [] + + def process_fn(batch): + return batch + + def progress_callback(processed, total): + progress_calls.append((processed, total)) + + config = AdaptiveBatchConfig(max_batch_size=3) + + results = process_with_adaptive_batching( + items=items, + process_fn=process_fn, + model_name="da3-large", + device=cpu_device, + config=config, + progress_callback=progress_callback, + ) + + # Should have multiple progress calls + assert len(progress_calls) > 1 + + # Last call should show all items processed + assert progress_calls[-1][0] == len(items) + assert progress_calls[-1][1] == len(items) + + def test_single_result_handling(self, cpu_device): + """Test handling of non-list results.""" + items = list(range(5)) + + def process_fn(batch): + # Return a single value instead of list + return sum(batch) + + results = process_with_adaptive_batching( + items=items, + process_fn=process_fn, + model_name="da3-large", + device=cpu_device, + ) + + # Should still work and return list of results + assert isinstance(results, list) + + def test_empty_items(self, cpu_device): + """Test with empty items list.""" + results = process_with_adaptive_batching( + items=[], + process_fn=lambda x: x, + model_name="da3-large", + device=cpu_device, + ) + assert results == [] + + +# ============================================================================= +# Utility Function Tests +# ============================================================================= + + +class TestEstimateMaxBatchSize: + """Tests for estimate_max_batch_size function.""" + + def test_returns_positive_integer(self, cpu_device): + """Test that function returns positive integer.""" + result = estimate_max_batch_size("da3-large", cpu_device) + assert isinstance(result, int) + assert result > 0 + + def test_different_resolutions(self, cpu_device): + """Test that higher resolution gives lower batch size (for GPU).""" + # For CPU this doesn't apply, but the function should still work + low_res = estimate_max_batch_size("da3-large", cpu_device, process_res=504) + high_res = estimate_max_batch_size("da3-large", cpu_device, process_res=1008) + + # Both should be valid + assert low_res > 0 + assert high_res > 0 + + def test_different_utilization(self, cpu_device): + """Test different target utilization values.""" + low_util = estimate_max_batch_size( + "da3-large", cpu_device, target_utilization=0.5 + ) + high_util = estimate_max_batch_size( + "da3-large", cpu_device, target_utilization=0.95 + ) + + # Both should be valid (CPU returns max_batch_size anyway) + assert low_util > 0 + assert high_util > 0 + + +class TestLogBatchPlan: + """Tests for log_batch_plan function.""" + + def test_log_batch_plan_runs(self, cpu_device, caplog): + """Test that log_batch_plan runs without error.""" + import logging + + with caplog.at_level(logging.INFO): + # Should not raise + log_batch_plan( + num_images=100, + model_name="da3-large", + device=cpu_device, + process_res=504, + ) + + def test_log_batch_plan_different_models(self, cpu_device): + """Test log_batch_plan with different models.""" + for model_name in ["da3-small", "da3-base", "da3-large", "da3-giant"]: + # Should not raise for any model + log_batch_plan( + num_images=50, + model_name=model_name, + device=cpu_device, + ) + + +# ============================================================================= +# Integration Tests +# ============================================================================= + + +class TestIntegration: + """Integration tests for the adaptive batching module.""" + + def test_full_workflow_cpu(self, cpu_device): + """Test complete workflow on CPU.""" + # Create data + images = [f"image_{i}.jpg" for i in range(25)] + + # Track processing + processed_batches = [] + + def process_fn(batch): + processed_batches.append(len(batch)) + return [f"result_{item}" for item in batch] + + # Process with adaptive batching + config = AdaptiveBatchConfig(max_batch_size=8) + results = process_with_adaptive_batching( + items=images, + process_fn=process_fn, + model_name="da3-large", + device=cpu_device, + config=config, + ) + + # Verify results + assert len(results) == len(images) + assert all(r.startswith("result_") for r in results) + + # Verify batching + assert sum(processed_batches) == len(images) + assert max(processed_batches) <= 8 + + def test_calculator_reuse(self, cpu_device): + """Test that calculator can be reused across multiple iterations.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + # First computation + batch1 = calc.compute_optimal_batch_size(num_images=100) + + # Second computation should work + batch2 = calc.compute_optimal_batch_size(num_images=50) + + assert batch1 == 64 # max_batch_size for CPU + assert batch2 == 50 # min(50, max_batch_size) + + def test_iterator_with_strings(self, cpu_device): + """Test iterator works with string items.""" + config = AdaptiveBatchConfig(max_batch_size=3) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = ["path/to/image1.jpg", "path/to/image2.jpg", "path/to/image3.jpg", "path/to/image4.jpg"] + + batches = list(adaptive_batch_iterator(items, calc)) + + # Collect all paths + all_paths = [] + for batch in batches: + all_paths.extend(batch.items) + + assert all_paths == items + + def test_iterator_with_tuples(self, cpu_device): + """Test iterator works with tuple items.""" + config = AdaptiveBatchConfig(max_batch_size=2) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = [(1, "a"), (2, "b"), (3, "c")] + + batches = list(adaptive_batch_iterator(items, calc)) + + # Should preserve tuple structure + all_items = [] + for batch in batches: + all_items.extend(batch.items) + + assert all_items == list(items) + + +# ============================================================================= +# Edge Cases +# ============================================================================= + + +class TestEdgeCases: + """Tests for edge cases and boundary conditions.""" + + def test_single_image(self, cpu_device): + """Test with single image.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + result = calc.compute_optimal_batch_size(num_images=1) + assert result == 1 + + batches = list(adaptive_batch_iterator(["single"], calc)) + assert len(batches) == 1 + assert batches[0].items == ["single"] + assert batches[0].is_last is True + + def test_exact_batch_size_multiple(self, cpu_device): + """Test when num_images is exact multiple of batch_size.""" + config = AdaptiveBatchConfig(max_batch_size=5) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + items = list(range(15)) # Exactly 3 batches of 5 + batches = list(adaptive_batch_iterator(items, calc)) + + assert len(batches) == 3 + assert all(b.batch_size == 5 for b in batches) + + def test_very_large_num_images(self, cpu_device): + """Test with very large number of images.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + result = calc.compute_optimal_batch_size(num_images=1_000_000) + assert result == 64 # Should cap at max_batch_size + + def test_zero_reserved_memory(self, cpu_device): + """Test with zero reserved memory.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + result = calc.compute_optimal_batch_size( + num_images=100, + process_res=504, + reserved_memory_mb=0, + ) + assert result > 0 + + def test_high_resolution(self, cpu_device): + """Test with very high resolution.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + # 4K resolution + result = calc.compute_optimal_batch_size( + num_images=100, + process_res=2160, + ) + assert result > 0 # Should still return valid batch size + + def test_low_resolution(self, cpu_device): + """Test with very low resolution.""" + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device) + + result = calc.compute_optimal_batch_size( + num_images=100, + process_res=128, + ) + assert result > 0 + + def test_negative_memory_edge_case(self, cpu_device): + """Test handling when calculations could go negative.""" + config = AdaptiveBatchConfig( + min_batch_size=1, + target_memory_utilization=0.01, # Very low utilization + ) + calc = AdaptiveBatchSizeCalculator("da3-large", cpu_device, config) + + # Should still return valid result + result = calc.compute_optimal_batch_size(num_images=100) + assert result >= 1 + + +# ============================================================================= +# Run tests +# ============================================================================= + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_api_batching.py b/tests/test_api_batching.py new file mode 100644 index 0000000000000000000000000000000000000000..f7c037194706927773ba493424c82e91b3152a75 --- /dev/null +++ b/tests/test_api_batching.py @@ -0,0 +1,572 @@ +# Copyright (c) Delanoe Pirard / Aedelon +# Licensed under the Apache License, Version 2.0 +""" +Tests for batch_inference and get_optimal_batch_size methods in DepthAnything3 API. + +These tests mock the actual model inference to focus on testing the batching logic, +without needing to load heavy model weights. +""" +from __future__ import annotations + +from dataclasses import dataclass +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +import torch + + +# ============================================================================= +# Mock Prediction Class +# ============================================================================= + + +@dataclass +class MockPrediction: + """Mock Prediction object for testing.""" + + depth: np.ndarray + processed_images: np.ndarray + num_images: int + + @classmethod + def create(cls, num_images: int) -> "MockPrediction": + """Create a mock prediction for n images.""" + return cls( + depth=np.zeros((num_images, 256, 256), dtype=np.float32), + processed_images=np.zeros((num_images, 256, 256, 3), dtype=np.uint8), + num_images=num_images, + ) + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def cpu_device(): + """Return CPU device.""" + return torch.device("cpu") + + +@pytest.fixture +def mock_model(cpu_device): + """Create a mock DepthAnything3 model.""" + from depth_anything_3.api import DepthAnything3 + + # Create a minimal mock + model = MagicMock(spec=DepthAnything3) + model.device = cpu_device + model.model_name = "da3-large" + + # Setup inference to return mock predictions + def mock_inference(image, process_res=504, **kwargs): + num_images = len(image) if isinstance(image, list) else 1 + return MockPrediction.create(num_images) + + model.inference = MagicMock(side_effect=mock_inference) + + return model + + +@pytest.fixture +def sample_images(): + """Create sample image paths for testing.""" + return [f"image_{i}.jpg" for i in range(10)] + + +@pytest.fixture +def large_sample_images(): + """Create larger sample of image paths.""" + return [f"image_{i}.jpg" for i in range(100)] + + +# ============================================================================= +# batch_inference Tests +# ============================================================================= + + +class TestBatchInference: + """Tests for the batch_inference method.""" + + def test_batch_inference_empty_list(self, mock_model): + """Test batch_inference with empty image list.""" + from depth_anything_3.api import DepthAnything3 + + # Call the actual method implementation with mocked model + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference([]) + + assert results == [] + mock_model.inference.assert_not_called() + + def test_batch_inference_fixed_batch_size(self, mock_model, sample_images): + """Test batch_inference with fixed batch size.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(sample_images, batch_size=3) + + # 10 images with batch size 3 = 4 batches (3, 3, 3, 1) + assert len(results) == 4 + assert mock_model.inference.call_count == 4 + + def test_batch_inference_auto_batch_size(self, mock_model, sample_images): + """Test batch_inference with auto batch size.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(sample_images, batch_size="auto") + + # Should have at least 1 result + assert len(results) >= 1 + # Should have called inference at least once + assert mock_model.inference.call_count >= 1 + + def test_batch_inference_progress_callback(self, mock_model, sample_images): + """Test that progress callback is called.""" + from depth_anything_3.api import DepthAnything3 + + progress_calls = [] + + def progress_callback(processed, total): + progress_calls.append((processed, total)) + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + api.batch_inference( + sample_images, batch_size=3, progress_callback=progress_callback + ) + + # Should have progress calls + assert len(progress_calls) == 4 # 4 batches + + # Last call should have all images processed + assert progress_calls[-1][0] == len(sample_images) + assert progress_calls[-1][1] == len(sample_images) + + def test_batch_inference_single_image(self, mock_model): + """Test batch_inference with single image.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(["single.jpg"]) + + assert len(results) == 1 + mock_model.inference.assert_called_once() + + def test_batch_inference_batch_larger_than_images(self, mock_model): + """Test when batch size is larger than number of images.""" + from depth_anything_3.api import DepthAnything3 + + images = ["img1.jpg", "img2.jpg"] + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(images, batch_size=10) + + # Should only make one call with all images + assert len(results) == 1 + mock_model.inference.assert_called_once() + + def test_batch_inference_exact_batch_multiple(self, mock_model): + """Test when image count is exact multiple of batch size.""" + from depth_anything_3.api import DepthAnything3 + + images = [f"img{i}.jpg" for i in range(12)] # Exactly 4 batches of 3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(images, batch_size=3) + + assert len(results) == 4 + assert mock_model.inference.call_count == 4 + + def test_batch_inference_respects_process_res(self, mock_model, sample_images): + """Test that process_res is passed to inference.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + api.batch_inference(sample_images, batch_size=10, process_res=1024) + + # Check that inference was called with correct process_res + call_args = mock_model.inference.call_args + assert call_args.kwargs.get("process_res") == 1024 + + def test_batch_inference_max_batch_size_auto(self, mock_model, sample_images): + """Test max_batch_size parameter with auto batching.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + # With max_batch_size=2, should split 10 images into more batches + results = api.batch_inference( + sample_images, batch_size="auto", max_batch_size=2 + ) + + # Should have at least 5 batches (10 images / 2 max) + assert len(results) >= 5 + + +# ============================================================================= +# get_optimal_batch_size Tests +# ============================================================================= + + +class TestGetOptimalBatchSize: + """Tests for the get_optimal_batch_size method.""" + + def test_get_optimal_batch_size_returns_int(self, cpu_device): + """Test that get_optimal_batch_size returns an integer.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = cpu_device + api.model_name = "da3-large" + + result = api.get_optimal_batch_size() + + assert isinstance(result, int) + assert result > 0 + + def test_get_optimal_batch_size_respects_resolution(self, cpu_device): + """Test that different resolutions affect the result.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = cpu_device + api.model_name = "da3-large" + + low_res = api.get_optimal_batch_size(process_res=256) + high_res = api.get_optimal_batch_size(process_res=1024) + + # Both should be valid + assert low_res > 0 + assert high_res > 0 + + def test_get_optimal_batch_size_respects_utilization(self, cpu_device): + """Test that target_utilization parameter is used.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = cpu_device + api.model_name = "da3-large" + + low_util = api.get_optimal_batch_size(target_utilization=0.5) + high_util = api.get_optimal_batch_size(target_utilization=0.95) + + # Both should return valid results + assert low_util > 0 + assert high_util > 0 + + def test_get_optimal_batch_size_different_models(self, cpu_device): + """Test with different model names.""" + from depth_anything_3.api import DepthAnything3 + + models = ["da3-small", "da3-base", "da3-large", "da3-giant"] + + for model_name in models: + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = cpu_device + api.model_name = model_name + + result = api.get_optimal_batch_size() + assert result > 0, f"Failed for model {model_name}" + + +# ============================================================================= +# Integration Tests +# ============================================================================= + + +class TestBatchingIntegration: + """Integration tests for batching functionality.""" + + def test_auto_vs_fixed_batching_coverage(self, mock_model, sample_images): + """Test that both auto and fixed batching process all images.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + # Track images processed + auto_images_processed = [] + fixed_images_processed = [] + + def track_auto(image, **kwargs): + batch = image if isinstance(image, list) else [image] + auto_images_processed.extend(batch) + return MockPrediction.create(len(batch)) + + def track_fixed(image, **kwargs): + batch = image if isinstance(image, list) else [image] + fixed_images_processed.extend(batch) + return MockPrediction.create(len(batch)) + + # Test auto batching + mock_model.inference.side_effect = track_auto + api.inference = mock_model.inference + api.batch_inference(sample_images.copy(), batch_size="auto") + + # Test fixed batching + mock_model.inference.side_effect = track_fixed + api.inference = mock_model.inference + api.batch_inference(sample_images.copy(), batch_size=3) + + # Both should process all images + assert len(auto_images_processed) == len(sample_images) + assert len(fixed_images_processed) == len(sample_images) + + def test_batch_inference_preserves_order(self, mock_model): + """Test that batch_inference preserves image order in processing.""" + from depth_anything_3.api import DepthAnything3 + + images = ["first.jpg", "second.jpg", "third.jpg", "fourth.jpg", "fifth.jpg"] + processed_order = [] + + def track_order(image, **kwargs): + batch = image if isinstance(image, list) else [image] + processed_order.extend(batch) + return MockPrediction.create(len(batch)) + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + mock_model.inference.side_effect = track_order + api.inference = mock_model.inference + + api.batch_inference(images, batch_size=2) + + assert processed_order == images + + def test_progress_increases_monotonically(self, mock_model, sample_images): + """Test that progress always increases.""" + from depth_anything_3.api import DepthAnything3 + + progress_values = [] + + def progress_callback(processed, total): + progress_values.append(processed) + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + api.batch_inference( + sample_images, batch_size=3, progress_callback=progress_callback + ) + + # Progress should always increase + for i in range(1, len(progress_values)): + assert progress_values[i] > progress_values[i - 1] + + +# ============================================================================= +# Edge Cases +# ============================================================================= + + +class TestBatchingEdgeCases: + """Tests for edge cases in batching.""" + + def test_batch_size_one(self, mock_model, sample_images): + """Test with batch size of 1.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(sample_images, batch_size=1) + + # Should have one result per image + assert len(results) == len(sample_images) + assert mock_model.inference.call_count == len(sample_images) + + def test_very_large_batch_size(self, mock_model, sample_images): + """Test with very large batch size.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(sample_images, batch_size=1000) + + # Should process all in one batch + assert len(results) == 1 + + def test_auto_with_very_low_memory_utilization(self, mock_model, sample_images): + """Test auto batching with very low memory utilization target.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference( + sample_images, batch_size="auto", target_memory_utilization=0.1 + ) + + # Should still process all images + total_processed = sum(r.num_images for r in results) + assert total_processed == len(sample_images) + + def test_numpy_array_inputs(self, mock_model): + """Test with numpy array inputs instead of paths.""" + from depth_anything_3.api import DepthAnything3 + + # Create dummy numpy arrays + images = [np.zeros((256, 256, 3), dtype=np.uint8) for _ in range(5)] + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + results = api.batch_inference(images, batch_size=2) + + assert len(results) == 3 # 5 images in batches of 2: 2, 2, 1 + + +# ============================================================================= +# Memory Cleanup Tests +# ============================================================================= + + +class TestMemoryCleanup: + """Tests for memory cleanup during batching.""" + + def test_gc_collect_called_between_batches(self, mock_model, sample_images): + """Test that garbage collection is called between batches.""" + from depth_anything_3.api import DepthAnything3 + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + with patch("gc.collect") as mock_gc: + api.batch_inference(sample_images, batch_size=3) + + # Should call gc.collect between batches (not after last) + # 4 batches means 3 gc.collect calls + assert mock_gc.call_count == 3 + + def test_cuda_empty_cache_called(self, sample_images): + """Test that cuda empty_cache is called on CUDA device.""" + from depth_anything_3.api import DepthAnything3 + + def mock_inference(image, **kwargs): + num = len(image) if isinstance(image, list) else 1 + return MockPrediction.create(num) + + mock_model = MagicMock(spec=DepthAnything3) + mock_model.device = torch.device("cuda:0") + mock_model.model_name = "da3-large" + mock_model.inference = MagicMock(side_effect=mock_inference) + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + with patch("torch.cuda.empty_cache") as mock_empty: + api.batch_inference(sample_images, batch_size=3) + + # Should call empty_cache between batches + assert mock_empty.call_count == 3 + + def test_mps_empty_cache_called(self, sample_images): + """Test that mps empty_cache is called on MPS device.""" + from depth_anything_3.api import DepthAnything3 + + def mock_inference(image, **kwargs): + num = len(image) if isinstance(image, list) else 1 + return MockPrediction.create(num) + + mock_model = MagicMock(spec=DepthAnything3) + mock_model.device = torch.device("mps") + mock_model.model_name = "da3-large" + mock_model.inference = MagicMock(side_effect=mock_inference) + + with patch.object(DepthAnything3, "__init__", lambda x, **k: None): + api = DepthAnything3() + api.device = mock_model.device + api.model_name = mock_model.model_name + api.inference = mock_model.inference + + with patch("torch.mps.empty_cache") as mock_empty: + api.batch_inference(sample_images, batch_size=3) + + assert mock_empty.call_count == 3 + + +# ============================================================================= +# Run tests +# ============================================================================= + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..8b9474b1f94e5c391591df82a0b4b0f9baecb5b6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3507 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <=3.13" +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 sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "aiofiles" +version = "23.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/41/cfed10bc64d774f497a86e5ede9248e1d062db675504b41c320954d99641/aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a", size = 32072, upload-time = "2023-08-09T15:23:11.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/19/5af6804c4cc0fed83f47bff6e413a98a36618e7d40185cd36e69737f3b0e/aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107", size = 15727, upload-time = "2023-08-09T15:23:09.774Z" }, +] + +[[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 = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "awesome-depth-anything-3" +version = "0.0.0" +source = { editable = "." } +dependencies = [ + { name = "e3nn" }, + { name = "einops" }, + { name = "evo" }, + { name = "fastapi" }, + { name = "huggingface-hub" }, + { name = "imageio" }, + { name = "kornia" }, + { name = "moviepy" }, + { name = "numpy" }, + { name = "omegaconf" }, + { name = "open3d" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pillow-heif" }, + { name = "plyfile" }, + { name = "pycolmap" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "trimesh" }, + { name = "twine" }, + { name = "typer" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +all = [ + { name = "gradio" }, + { name = "gsplat", marker = "sys_platform != 'darwin'" }, + { name = "huggingface-hub" }, + { name = "pillow" }, + { name = "xformers", marker = "sys_platform != 'darwin'" }, +] +app = [ + { name = "gradio" }, + { name = "huggingface-hub" }, + { name = "pillow" }, +] +cuda = [ + { name = "gsplat", marker = "sys_platform != 'darwin'" }, + { name = "xformers", marker = "sys_platform != 'darwin'" }, +] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "ruff" }, +] +gs = [ + { name = "gsplat", marker = "sys_platform != 'darwin'" }, +] +xformers = [ + { name = "xformers", marker = "sys_platform != 'darwin'" }, +] + +[package.metadata] +requires-dist = [ + { name = "awesome-depth-anything-3", extras = ["app", "cuda"], marker = "extra == 'all'" }, + { name = "awesome-depth-anything-3", extras = ["xformers", "gs"], marker = "extra == 'cuda'" }, + { name = "e3nn" }, + { name = "einops" }, + { name = "evo" }, + { name = "fastapi" }, + { name = "gradio", marker = "extra == 'app'", specifier = "==4.44.1" }, + { name = "gsplat", marker = "sys_platform != 'darwin' and extra == 'gs'", specifier = ">=1.0.0" }, + { name = "huggingface-hub" }, + { name = "huggingface-hub", marker = "extra == 'app'", specifier = ">=0.19,<1.0" }, + { name = "imageio" }, + { name = "kornia", specifier = ">=0.7.0" }, + { name = "moviepy", specifier = "==1.0.3" }, + { name = "numpy", specifier = "<2" }, + { name = "omegaconf" }, + { name = "open3d" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pillow", marker = "extra == 'app'", specifier = ">=9.0" }, + { name = "pillow-heif" }, + { name = "plyfile" }, + { name = "pre-commit", marker = "extra == 'dev'" }, + { name = "pycolmap" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "requests" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "safetensors" }, + { name = "torch", specifier = ">=2" }, + { name = "torchvision" }, + { name = "trimesh" }, + { name = "twine", specifier = ">=6.2.0" }, + { name = "typer", specifier = ">=0.9.0,<0.13.0" }, + { name = "uvicorn" }, + { name = "xformers", marker = "sys_platform != 'darwin' and extra == 'xformers'" }, +] +provides-extras = ["app", "dev", "xformers", "gs", "cuda", "all"] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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/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/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/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" }, +] + +[[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.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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 = "configargparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, +] +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 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 sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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/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 = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, +] + +[[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 = "dash" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "importlib-metadata" }, + { name = "nest-asyncio" }, + { name = "plotly" }, + { name = "requests" }, + { name = "retrying" }, + { name = "setuptools" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/f9/516671861cf190bda37f6afa696d8a6a6ac593f23d8cf198e16faca044f5/dash-3.3.0.tar.gz", hash = "sha256:eaaa7a671540b5e1db8066f4966d0277d21edc2c7acdaec2fd6d198366a8b0df", size = 7579436, upload-time = "2025-11-12T15:51:54.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/cf/a4853e5b2b2bea55ae909095a8720b3ed50d07bdd40cbeafcedb5a6c47da/dash-3.3.0-py3-none-any.whl", hash = "sha256:8f52415977f7490492dd8a3872279160be8ff253ca9f4d49a4e3ba747fa4bd91", size = 7919707, upload-time = "2025-11-12T15:51:47.432Z" }, +] + +[[package]] +name = "decorator" +version = "4.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/93/84fa12f2dc341f8cf5f022ee09e109961055749df2d0c75c5f98746cfe6c/decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7", size = 33629, upload-time = "2020-02-29T05:24:43.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/1b/72a1821152d07cf1d8b6fce298aeb06a7eb90f4d6d41acec9861e7cc6df0/decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", size = 9239, upload-time = "2020-02-29T05:24:45.993Z" }, +] + +[[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 = "docutils" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, +] + +[[package]] +name = "e3nn" +version = "0.5.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opt-einsum-fx" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sympy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/9d/1fac1462adfa3923b8c104645f4af471a20539f35d8cb03bee013c6013df/e3nn-0.5.8.tar.gz", hash = "sha256:8329ab7477c6a124a8785c80adc2a2c26931cf0aa471e5728f97149bb80816f2", size = 437930, upload-time = "2025-10-07T02:05:15.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f0/a1434263789e035df7f18d663cf869ee02949574dd04de01b3fb88fb8501/e3nn-0.5.8-py3-none-any.whl", hash = "sha256:551b886a0761bce8022df242271c0397bcf5ee689834ddcb402c99ab985cbc58", size = 450056, upload-time = "2025-10-07T02:05:13.792Z" }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, +] + +[[package]] +name = "evo" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "colorama" }, + { name = "matplotlib" }, + { name = "natsort" }, + { name = "numexpr" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pygments" }, + { name = "pyyaml" }, + { name = "rosbags" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "seaborn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/fc/0d89fedf3e930d4d83b41471a266cfed2859b034d00cf9078814a2a3954f/evo-1.34.0.tar.gz", hash = "sha256:179a7ea4d8d537241a6c73684db293f9651f4c59b218016a3afe80b41c16f71b", size = 6188426, upload-time = "2025-11-12T22:45:46.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d6/f9dfc2e4ac855aea02bc9b239dbb52973e06d6b38c97b1721fe2c4e88b20/evo-1.34.0-py3-none-any.whl", hash = "sha256:aa696f6826a01af5a2a144c59ad4378baf1409b3c93a4dd1310bd866e12ea028", size = 148142, upload-time = "2025-11-12T22:45:43.716Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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 = "fastapi" +version = "0.123.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/70/b856e5db716c4d84cc9d7f69e7dba0f3f900e0deee01336a458f60add3d7/fastapi-0.123.4.tar.gz", hash = "sha256:c2d0ac82f3534c8e35692fda67e2412ac60bad846bb903a65cd8145a65741474", size = 350467, upload-time = "2025-12-02T10:48:21.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e9/1c266c3c6aeaab86b5f7b2d7bd3e3789ddc6780c0a4236d24d92814628f1/fastapi-0.123.4-py3-none-any.whl", hash = "sha256:fc2b5cbc10fa05f4f22d87ef7ebc8993b5110ffd9850c08e1fc35a0da37f492e", size = 111270, upload-time = "2025-12-02T10:48:19.707Z" }, +] + +[[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 = "ffmpy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151, upload-time = "2025-11-28T17:04:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769, upload-time = "2025-11-28T17:04:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189, upload-time = "2025-11-28T17:04:18.398Z" }, + { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415, upload-time = "2025-11-28T17:04:20.421Z" }, + { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927, upload-time = "2025-11-28T17:04:22.625Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674, upload-time = "2025-11-28T17:04:24.675Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663, upload-time = "2025-11-28T17:04:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143, upload-time = "2025-11-28T17:04:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" }, + { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" }, + { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" }, + { url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "gradio" +version = "4.44.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "importlib-resources" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "ruff", marker = "sys_platform != 'emscripten'" }, + { name = "semantic-version" }, + { name = "tomlkit" }, + { name = "typer", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/0c/8848a5f628b0de1b5b691f4f47de36a93e9d4f3d4157ac1dd8d2d57a5dec/gradio-4.44.1.tar.gz", hash = "sha256:a68a52498ac6b63f8864ef84bf7866a70e7d07ebe913edf921e1d2a3708ad5ae", size = 28309860, upload-time = "2024-09-30T17:52:37.306Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/6e/c0726e138f64cd98379a7bf95f4f3b15dd5a9f004b172540cee5653ec820/gradio-4.44.1-py3-none-any.whl", hash = "sha256:c908850c638e4a176b22f95a758ce6a63ffbc2a7a5a74b23186ceeeedc23f4d9", size = 18068711, upload-time = "2024-09-30T17:52:33.249Z" }, +] + +[[package]] +name = "gradio-client" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/86/2697930b274a5c8479a44dcb65c02d7e7445e5d88a72524a0a65593cbb58/gradio_client-1.3.0.tar.gz", hash = "sha256:d904afeae4f5682add0a6a263542c10e7669ff6c9de0a53a5c2fc9b719a24bb8", size = 316713, upload-time = "2024-08-08T10:30:23.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/fe/7e9cb4d0e6aa74268fa31089189e4855882a0f2a36c45d359336946d4ae1/gradio_client-1.3.0-py3-none-any.whl", hash = "sha256:20c40cb4d56e18de1a025ccf58079f08a304e4fb2dfbcf7c2352815b2cb31091", size = 318688, upload-time = "2024-08-08T10:30:21.659Z" }, +] + +[[package]] +name = "gsplat" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaxtyping", marker = "sys_platform != 'darwin'" }, + { name = "ninja", marker = "sys_platform != 'darwin'" }, + { name = "numpy", marker = "sys_platform != 'darwin'" }, + { name = "rich", marker = "sys_platform != 'darwin'" }, + { name = "torch", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/94/a0963873708223385ac926112dc3e40ab08297784ac53d95309b4cb5a801/gsplat-1.5.3.tar.gz", hash = "sha256:343f080c470e891ba3c697c03bdf0952cb982d1d535fdc24adb23c2cb4fba906", size = 4743603, upload-time = "2025-07-04T16:50:42.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/68/c5cbbdb86421a3f45ac0b52cf9860b1048d089335151d3f37c23aabcbef1/gsplat-1.5.3-py3-none-any.whl", hash = "sha256:515a3773641f5e7f7717acab6276c0b1d6dbcad087b7968ca653337c3189a982", size = 6519041, upload-time = "2025-07-04T16:50:40.559Z" }, +] + +[[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 = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "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 = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "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 = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jaxtyping" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wadler-lindig", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/1e/827f9e17b26e21c7d4d934fd1a214284ad05663afedd37c21ed105db366b/jaxtyping-0.3.3.tar.gz", hash = "sha256:8003cfd16ba2ad9b47fdda1d982a575299a81ddfc7997ad0e917c87a0897ea86", size = 45484, upload-time = "2025-10-01T13:46:51.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/97/88264b1af140f66ba7ca6eb2f3a108be233ee278bb3f1d5c750243e7458a/jaxtyping-0.3.3-py3-none-any.whl", hash = "sha256:a1c2f0f4351a8deda84b0e3b5c5a50894a1cdae2b82d841279fce4393aff4a7c", size = 55926, upload-time = "2025-10-01T13:46:50.621Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[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-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 = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "kornia" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kornia-rs" }, + { name = "packaging" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/e6/45e757d4924176e4d4e111e10effaab7db382313243e0188a06805010073/kornia-0.8.2.tar.gz", hash = "sha256:5411b2ce0dd909d1608016308cd68faeef90f88c47f47e8ecd40553fd4d8b937", size = 667151, upload-time = "2025-11-08T12:10:03.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d4/e9bd12b7b4cbd23b4dfb47e744ee1fa54d6d9c3c9bc406ec86c1be8c8307/kornia-0.8.2-py2.py3-none-any.whl", hash = "sha256:32dfe77c9c74a87a2de49395aa3c2c376a1b63c27611a298b394d02d13905819", size = 1095012, upload-time = "2025-11-08T12:10:01.226Z" }, +] + +[[package]] +name = "kornia-rs" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/17/8b3518ece01512a575b18f86b346879793d3dea264b314796bbd44d42e11/kornia_rs-0.1.10.tar.gz", hash = "sha256:5fd3fbc65240fa751975f5870b079f98e7fdcaa2885ea577b3da324d8bf01d81", size = 145610, upload-time = "2025-11-08T11:29:32.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/40/6a59c5b99e19e1be7fff1d68dd3b3eddc80e5304dab75ebb78d0199e2484/kornia_rs-0.1.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e95ccd4b3f73a0d5cbe16c03fd705fa8d75e9df7b044ba9a6c5957b2591003f3", size = 2815093, upload-time = "2025-11-08T11:30:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/17/aa/77f5882707467a6af0833b3ac1497638352bc391d082713b24a3bd187f73/kornia_rs-0.1.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bc8d5de7d7611b68746b2feff594a073740c4f915d0ccc37bd1d189029c20fe", size = 2078046, upload-time = "2025-11-08T11:30:04.952Z" }, + { url = "https://files.pythonhosted.org/packages/31/43/cff694d864bf60e1e3853ec98bdabc66433f0eeffb7e7f97d27a0b907a88/kornia_rs-0.1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb94ce80a614a5f00cb68755d7a182236482584e78388217974ef811f4dbb30", size = 2204974, upload-time = "2025-11-08T11:29:30.626Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/907580d1e573bc1862557cb70d1f1888bde1b33240d15cd10ff06c93a57c/kornia_rs-0.1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859942c70f503bba813c99a39d3011d3e51f294db8058f87842efa180955cab4", size = 3042947, upload-time = "2025-11-08T11:29:48.431Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/3d0a45f297b1a3f308893751b62080898ce415c61a3629a3d6b04cf55db7/kornia_rs-0.1.10-cp310-cp310-win_amd64.whl", hash = "sha256:74f17ea9afc0a5312832c32f6670e1a4b2162dcf9a8908fb46bbb56d2c707e9e", size = 2543744, upload-time = "2025-11-08T11:30:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/ab91a87cefd8d92a10749fa5d923366dfd2a2d240d9e57260e4218e9a5af/kornia_rs-0.1.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6757940733f13c52c4f142b9b11e3e9bd12ef9d209e333300602e86e21f5ae2f", size = 2811949, upload-time = "2025-11-08T11:30:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/61/6125a970249e04dd31cf3edf3fb0ceb98ea65269bc416ba48fd70f9a8f5e/kornia_rs-0.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68e90101a34ba2bbce920332b25fd4d25c8c546d9a241b2606a6d886df2dd1ed", size = 2078639, upload-time = "2025-11-08T11:30:06.363Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e4/c3484e5921a08e6368f0565c30646741fd12b46cb45c962d519cac3d12ad/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b0adb81858a8963455f2f0da01fcd6ea3296147b918306488edeeaf6bc2a979", size = 2204722, upload-time = "2025-11-08T11:29:33.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/a4/2e6e33da900f19ae6411bfad41d317e56f1ae4f204bd73e61f0881bd5418/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3e237a8428524ad9f86599c0c47b355bc3007669fe297ea3fbd59cd64bc2f7", size = 3042890, upload-time = "2025-11-08T11:29:50.15Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/5e171c98b742139bebd1bd593d768e3c045f824bf0ae14190b63f0ac0acc/kornia_rs-0.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:1d300ea6d4666e47302fba6cc438556d91e37ce41caf291a9a04a8f74c231d0b", size = 2544572, upload-time = "2025-11-08T11:30:32.32Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6c/8248f08c90a10d6b8ca2e74783da8df7fa509f46b64a3b4fbb7dd0ac4e9c/kornia_rs-0.1.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0809277e51156d59be3c39605ba9659e94f7a4cf3b0b6c035ec2f06f6067881", size = 2811606, upload-time = "2025-11-08T11:30:21.346Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/29e5710cbc5d01c155ee1fd7621db48b94378a7ae394741bb34a6bfb36d9/kornia_rs-0.1.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ecf2ba0291cc1bb178073d56e46b16296a8864a20272b63af02ee88771cb574", size = 2076141, upload-time = "2025-11-08T11:30:07.527Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/0b3e90b9d0a25e6211c7ac9fa1dfed4db1306a812c359ee49678390a1bdc/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d874ca12dd58871f9849672d9bf9fa998398470a88b52d61223ce2133b196662", size = 2205562, upload-time = "2025-11-08T11:29:35.353Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/315f358b2a2c29d9af3a73f3d1973c2fd8e0cdeb65a57af98643e66fa7c8/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f332a2a034cc791006f25c2d85e342a060887145e9236e8e43562badcadededf", size = 3042197, upload-time = "2025-11-08T11:29:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b8/0ddbdf1d35fec3ef24f5b8cc29eb633ce5ce16c94c9fb090408c1280abe9/kornia_rs-0.1.10-cp312-cp312-win_amd64.whl", hash = "sha256:34111ce1c8abe930079b4b0aeb8d372f876c621a867ed03f77181de685e71a8f", size = 2539656, upload-time = "2025-11-08T11:30:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/90/01/1d658b11635431f8c31f416c90ca99befdc1f4fdd20e91a05b480b9c0ea8/kornia_rs-0.1.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:950a943f91c2cff94d80282886b0d48bbc15ef4a7cc4b15ac819724dfdb2f414", size = 2811810, upload-time = "2025-11-08T11:30:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/bd970ded1d819557cc33055d982b1847eb385151ea5b0c915c16ed74f5c0/kornia_rs-0.1.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:63b802aaf95590276d3426edc6d23ff11caf269d2bc2ec37cb6c679b7b2a8ee0", size = 2076195, upload-time = "2025-11-08T11:30:08.726Z" }, + { url = "https://files.pythonhosted.org/packages/c1/10/afd700455105fdba5b043d724f3a65ca36259b89c736a3b71d5a03103808/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38087da7cdf2bffe10530c0d53335dd1fc107fae6521f2dd4797c6522b6d11b3", size = 2205781, upload-time = "2025-11-08T11:29:36.8Z" }, + { url = "https://files.pythonhosted.org/packages/25/16/ec8dc3ce1d79660ddd6a186a77037e0c3bf61648e6c72250280b648fb291/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa3464de8f9920d87415721c36840ceea23e054dcb54dd9f69189ba9eabce0c7", size = 3042272, upload-time = "2025-11-08T11:29:52.936Z" }, + { url = "https://files.pythonhosted.org/packages/f7/75/62785aba777d35a562a97a987d65840306fab7a8ecd2d928dd8ac779e29b/kornia_rs-0.1.10-cp313-cp313-win_amd64.whl", hash = "sha256:c57d157bebe64c22e2e44c72455b1c7365eee4d767e0c187dc28f22d072ebaf7", size = 2539802, upload-time = "2025-11-08T11:30:35.753Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d5/32b23d110109eb77b2dc952be75411f7e495da9105058e2cb08924a9cc90/kornia_rs-0.1.10-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:0b375f02422ef5986caed612799b4ddcc91f57f303906868b0a8c397a17e7607", size = 2810244, upload-time = "2025-11-08T11:30:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/5f/5ecde42b7c18e7df26c413848a98744427c3d370f5eed725b65f0bc356fb/kornia_rs-0.1.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2bcfa438d6b5dbe07d573afc980f2871f6639b2eac5148b8c0bba4f82357b9a", size = 2074220, upload-time = "2025-11-08T11:30:09.972Z" }, + { url = "https://files.pythonhosted.org/packages/18/6c/6fc86eb855bcc723924c3b91de98dc6c0f381987ce582e080b8eade3bc88/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:021b0a02b2356b12b3954a298f369ed4fe2dd522dcf8b6d72f91bf3bd8eea201", size = 2204672, upload-time = "2025-11-08T11:29:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/19/26/3ac706d1b36761c0f7a36934327079adcb42d761c8c219865123d49fc1b2/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b07e2ae79e423b3248d94afd092e324c5ddfe3157fafc047531cc8bffa6a3", size = 3042797, upload-time = "2025-11-08T11:29:54.719Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/d62728d86bc67f5516249b154ff0bdfcf38a854dae284ff0ce62da87af99/kornia_rs-0.1.10-cp313-cp313t-win_amd64.whl", hash = "sha256:b80a037e34d63cb021bcd5fc571e41aff804a2981311f66e883768c6b8e5f8de", size = 2543855, upload-time = "2025-11-08T11:30:37.437Z" }, +] + +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, + { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, + { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/2f/46/08fd8ef19b782f301d56a9ccfd7dafec5fd4fc1a9f017cf22a1accb585d7/lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c", size = 207171, upload-time = "2025-11-03T13:01:56.595Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3f/ea3334e59de30871d773963997ecdba96c4584c5f8007fd83cfc8f1ee935/lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a", size = 207163, upload-time = "2025-11-03T13:01:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/7b3a2a0feb998969f4793c650bb16eff5b06e80d1f7bff867feb332f2af2/lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d", size = 1292136, upload-time = "2025-11-03T13:02:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/d1/f1d259352227bb1c185288dd694121ea303e43404aa77560b879c90e7073/lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c", size = 1279639, upload-time = "2025-11-03T13:02:01.649Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fb/ba9256c48266a09012ed1d9b0253b9aa4fe9cdff094f8febf5b26a4aa2a2/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64", size = 1368257, upload-time = "2025-11-03T13:02:03.35Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6d/dee32a9430c8b0e01bbb4537573cabd00555827f1a0a42d4e24ca803935c/lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832", size = 88191, upload-time = "2025-11-03T13:02:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/18/e0/f06028aea741bbecb2a7e9648f4643235279a770c7ffaf70bd4860c73661/lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22", size = 99502, upload-time = "2025-11-03T13:02:05.886Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/5bef44afb303e56078676b9f2486f13173a3c1e7f17eaac1793538174817/lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9", size = 91285, upload-time = "2025-11-03T13:02:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/6a5c2952971af73f15ed4ebfdd69774b454bd0dc905b289082ca8664fba1/lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f", size = 207348, upload-time = "2025-11-03T13:02:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d7/fd62cbdbdccc35341e83aabdb3f6d5c19be2687d0a4eaf6457ddf53bba64/lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba", size = 207340, upload-time = "2025-11-03T13:02:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/225ffadaacb4b0e0eb5fd263541edd938f16cd21fe1eae3cd6d5b6a259dc/lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d", size = 1293398, upload-time = "2025-11-03T13:02:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/2ce59ba4a21ea5dc43460cba6f34584e187328019abc0e66698f2b66c881/lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67", size = 1281209, upload-time = "2025-11-03T13:02:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/4d946bd1624ec229b386a3bc8e7a85fa9a963d67d0a62043f0af0978d3da/lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d", size = 1369406, upload-time = "2025-11-03T13:02:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/d429ba4720a9064722698b4b754fb93e42e625f1318b8fe834086c7c783b/lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901", size = 88325, upload-time = "2025-11-03T13:02:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/7ba10c9b97c06af6c8f7032ec942ff127558863df52d866019ce9d2425cf/lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb", size = 99643, upload-time = "2025-11-03T13:02:15.978Z" }, + { url = "https://files.pythonhosted.org/packages/77/4d/a175459fb29f909e13e57c8f475181ad8085d8d7869bd8ad99033e3ee5fa/lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd", size = 91504, upload-time = "2025-11-03T13:02:17.313Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +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'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/87/3932d5778ab4c025db22710b61f49ccaed3956c5cf46ffb2ffa7492b06d9/matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380", size = 8247141, upload-time = "2025-10-09T00:26:06.023Z" }, + { url = "https://files.pythonhosted.org/packages/45/a8/bfed45339160102bce21a44e38a358a1134a5f84c26166de03fb4a53208f/matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d", size = 8107995, upload-time = "2025-10-09T00:26:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503, upload-time = "2025-10-09T00:26:10.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982, upload-time = "2025-10-09T00:26:12.594Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429, upload-time = "2025-10-09T00:26:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174, upload-time = "2025-10-09T00:26:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204, upload-time = "2025-10-09T00:27:48.806Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607, upload-time = "2025-10-09T00:27:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257, upload-time = "2025-10-09T00:27:52.597Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, +] + +[[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 = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "moviepy" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "numpy" }, + { name = "proglog" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/54/01a8c4e35c75ca9724d19a7e4de9dc23f0ceb8769102c7de056113af61c3/moviepy-1.0.3.tar.gz", hash = "sha256:2884e35d1788077db3ff89e763c5ba7bfddbd7ae9108c9bc809e7ba58fa433f5", size = 388311, upload-time = "2020-05-07T16:27:46.856Z" } + +[[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 = "narwhals" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/ea/f82ef99ced4d03c33bb314c9b84a08a0a86c448aaa11ffd6256b99538aa5/narwhals-2.13.0.tar.gz", hash = "sha256:ee94c97f4cf7cfeebbeca8d274784df8b3d7fd3f955ce418af998d405576fdd9", size = 594555, upload-time = "2025-12-01T13:54:05.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl", hash = "sha256:9b795523c179ca78204e3be53726da374168f906e38de2ff174c2363baaaf481", size = 426407, upload-time = "2025-12-01T13:54:03.861Z" }, +] + +[[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 = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6" +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 sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464, upload-time = "2025-11-24T03:03:47.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "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" }, +] + +[[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 = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "open3d" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "addict" }, + { name = "configargparse" }, + { name = "dash" }, + { name = "flask" }, + { name = "matplotlib" }, + { name = "nbformat" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyquaternion" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "tqdm" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/4b/91e8a4100adf0ccd2f7ad21dd24c2e3d8f12925396528d0462cfb1735e5a/open3d-0.19.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f7128ded206e07987cc29d0917195fb64033dea31e0d60dead3629b33d3c175f", size = 103086005, upload-time = "2025-01-08T07:25:56.755Z" }, + { url = "https://files.pythonhosted.org/packages/c7/45/13bc9414ee9db611cba90b9efa69f66f246560e8ade575f1ee5b7f7b5d31/open3d-0.19.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:5b60234fa6a56a20caf1560cad4e914133c8c198d74d7b839631c90e8592762e", size = 447678387, upload-time = "2025-01-08T07:21:55.27Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/0219416429f88ebc94fcb269fb186b153affe5b91dffe8f9062330d7776d/open3d-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:18bb8b86e5fa9e582ed11b9651ff6e4a782e6778c9b8bfc344fc866dc8b5f49c", size = 69150378, upload-time = "2025-01-08T07:27:10.462Z" }, + { url = "https://files.pythonhosted.org/packages/a7/37/8d1746fcb58c37a9bd868fdca9a36c25b3c277bd764b7146419d11d2a58d/open3d-0.19.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:117702467bfb1602e9ae0ee5e2c7bcf573ebcd227b36a26f9f08425b52c89929", size = 103098641, upload-time = "2025-01-08T07:26:12.371Z" }, + { url = "https://files.pythonhosted.org/packages/bc/50/339bae21d0078cc3d3735e8eaf493a353a17dcc95d76bcefaa8edcf723d3/open3d-0.19.0-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:678017392f6cc64a19d83afeb5329ffe8196893de2432f4c258eaaa819421bb5", size = 447683616, upload-time = "2025-01-08T07:22:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3c/358f1cc5b034dc6a785408b7aa7643e503229d890bcbc830cda9fce778b1/open3d-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:02091c309708f09da1167d2ea475e05d19f5e81dff025145f3afd9373cbba61f", size = 69151111, upload-time = "2025-01-08T07:27:22.662Z" }, + { url = "https://files.pythonhosted.org/packages/37/c5/286c605e087e72ad83eab130451ce13b768caa4374d926dc735edc20da5a/open3d-0.19.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:9e4a8d29443ba4c83010d199d56c96bf553dd970d3351692ab271759cbe2d7ac", size = 103202754, upload-time = "2025-01-08T07:26:27.169Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/3723e5ade77c234a1650db11cbe59fe25c4f5af6c224f8ea22ff088bb36a/open3d-0.19.0-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:01e4590dc2209040292ebe509542fbf2bf869ea60bcd9be7a3fe77b65bad3192", size = 447665185, upload-time = "2025-01-08T07:23:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/35a6e0a35aa72420e75dc28d54b24beaff79bcad150423e47c67d2ad8773/open3d-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:665839837e1d3a62524804c31031462c3b548a2b6ed55214e6deb91522844f97", size = 69169961, upload-time = "2025-01-08T07:27:35.392Z" }, +] + +[[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 = "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 = "opt-einsum-fx" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opt-einsum" }, + { name = "packaging" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/de/856dab99be0360c7275fee075eb0450a2ec82a54c4c33689606f62e9615b/opt_einsum_fx-0.1.4.tar.gz", hash = "sha256:7eeb7f91ecb70be65e6179c106ea7f64fc1db6319e3d1289a4518b384f81e74f", size = 12969, upload-time = "2021-11-07T20:49:33.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/4c/e0370709aaf9d7ceb68f975cac559751e75954429a77e83202e680606560/opt_einsum_fx-0.1.4-py3-none-any.whl", hash = "sha256:85f489f4c7c31fd88d5faf9669c09e61ec37a30098809fdcfe2a08a9e42f23c9", size = 13213, upload-time = "2021-11-07T20:49:32.395Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/30/5aed63d5af1c8b02fbd2a8d83e2a6c8455e30504c50dbf08c8b51403d873/orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", size = 243870, upload-time = "2025-10-24T15:48:28.908Z" }, + { url = "https://files.pythonhosted.org/packages/44/1f/da46563c08bef33c41fd63c660abcd2184b4d2b950c8686317d03b9f5f0c/orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", size = 130622, upload-time = "2025-10-24T15:48:31.361Z" }, + { url = "https://files.pythonhosted.org/packages/02/bd/b551a05d0090eab0bf8008a13a14edc0f3c3e0236aa6f5b697760dd2817b/orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", size = 129344, upload-time = "2025-10-24T15:48:32.71Z" }, + { url = "https://files.pythonhosted.org/packages/87/6c/9ddd5e609f443b2548c5e7df3c44d0e86df2c68587a0e20c50018cdec535/orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", size = 136633, upload-time = "2025-10-24T15:48:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/95/f2/9f04f2874c625a9fb60f6918c33542320661255323c272e66f7dcce14df2/orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", size = 137695, upload-time = "2025-10-24T15:48:35.654Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c2/c7302afcbdfe8a891baae0e2cee091583a30e6fa613e8bdf33b0e9c8a8c7/orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", size = 136879, upload-time = "2025-10-24T15:48:37.483Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3a/b31c8f0182a3e27f48e703f46e61bb769666cd0dac4700a73912d07a1417/orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff", size = 136374, upload-time = "2025-10-24T15:48:38.624Z" }, + { url = "https://files.pythonhosted.org/packages/29/d0/fd9ab96841b090d281c46df566b7f97bc6c8cd9aff3f3ebe99755895c406/orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", size = 140519, upload-time = "2025-10-24T15:48:39.756Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ce/36eb0f15978bb88e33a3480e1a3fb891caa0f189ba61ce7713e0ccdadabf/orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", size = 406522, upload-time = "2025-10-24T15:48:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/e8af3161a288f5c6a00c188fc729c7ba193b0cbc07309a1a29c004347c30/orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", size = 149790, upload-time = "2025-10-24T15:48:42.664Z" }, + { url = "https://files.pythonhosted.org/packages/ea/96/209d52db0cf1e10ed48d8c194841e383e23c2ced5a2ee766649fe0e32d02/orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", size = 140040, upload-time = "2025-10-24T15:48:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/526db1395ccb74c3d59ac1660b9a325017096dc5643086b38f27662b4add/orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", size = 135955, upload-time = "2025-10-24T15:48:45.495Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/18a778c9de3702b19880e73c9866b91cc85f904b885d816ba1ab318b223c/orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", size = 131577, upload-time = "2025-10-24T15:48:46.609Z" }, + { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498, upload-time = "2025-10-24T15:48:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961, upload-time = "2025-10-24T15:48:49.571Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321, upload-time = "2025-10-24T15:48:50.713Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207, upload-time = "2025-10-24T15:48:52.193Z" }, + { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323, upload-time = "2025-10-24T15:48:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440, upload-time = "2025-10-24T15:48:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680, upload-time = "2025-10-24T15:48:57.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160, upload-time = "2025-10-24T15:48:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318, upload-time = "2025-10-24T15:49:00.834Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330, upload-time = "2025-10-24T15:49:02.327Z" }, + { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580, upload-time = "2025-10-24T15:49:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846, upload-time = "2025-10-24T15:49:04.761Z" }, + { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781, upload-time = "2025-10-24T15:49:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391, upload-time = "2025-10-24T15:49:07.355Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" }, + { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" }, + { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" }, + { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" }, + { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" }, + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.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" }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, +] + +[[package]] +name = "pillow-heif" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/85/d26cc132e66e8c3c9ffa85c256717995214bf7f1f2af8c13beb56bcfb535/pillow_heif-0.22.0.tar.gz", hash = "sha256:61d473929340d3073722f6316b7fbbdb11132faa6bac0242328e8436cc55b39a", size = 18551571, upload-time = "2025-03-15T13:20:37.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/bf/ecfeb3db6da8513cbb953d456fed981364724424f0c53636f2a1e0966a3d/pillow_heif-0.22.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:bc78e4eb3198462ad6a79a73b7da0bc83a7d50a24e65429104e491cd3924ec08", size = 5399903, upload-time = "2025-03-15T13:19:20.246Z" }, + { url = "https://files.pythonhosted.org/packages/c5/99/2e5a00a504826c8f90c462e718a6ff53c5844922468b66c51898c94c75e7/pillow_heif-0.22.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e78b48e85c981d03a3307bcf258763d54a2ab3d2426f11b3e6345d63fda038cb", size = 3983905, upload-time = "2025-03-15T13:19:22.587Z" }, + { url = "https://files.pythonhosted.org/packages/83/e0/7bb0b5245cb212e6c39426b485b98b0cdb224d004cff03e6228177f95c8e/pillow_heif-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b8b1535df032730d3a1b4050f235b539dddac784dfc58612b072dcab3f85c1", size = 6978836, upload-time = "2025-03-15T13:19:24.231Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/ef099f686a1f275e7336ec7fda3ef9a24de3a39e4b584ec0a2b7236c2953/pillow_heif-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68d5eb3ea132613ed2fc6b20cf30359b886786b5b428554cd951a8aab4cd1e4", size = 7814079, upload-time = "2025-03-15T13:19:26.05Z" }, + { url = "https://files.pythonhosted.org/packages/af/58/f688756f7329e2bed7a9d7314479af29a5fca744ce1858d2cbe14469b1e2/pillow_heif-0.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:27400e7abc476df122d712b44cfe2eac5b2825cbe8761541bfc9a1c64ba2cb56", size = 8313153, upload-time = "2025-03-15T13:19:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/52/61/01a799b61c17c232573e16abd9ca2d451a61fd70baaf1e668326a46768e2/pillow_heif-0.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b7d765b00e7b80245120e0d53259b61f54bba2957f116f5ee07fcc1769be93f", size = 9060195, upload-time = "2025-03-15T13:19:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/cd/f8d50769c40fe4df1ab08b497ceaa659c2dda70c08fe1973967e40a490c8/pillow_heif-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:2345932d4efea71fce7990a8412c37f2e1dd19bf97909f42d96a452ad18a21db", size = 8567164, upload-time = "2025-03-15T13:19:31.696Z" }, + { url = "https://files.pythonhosted.org/packages/81/a2/bf816b52618e46575c3e3d6d7bedde74dc45586a0f48b43e09048f0c4bba/pillow_heif-0.22.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:782eef461c836b3947fe253baaa5dcf6c75fba484c4905b4ac6b5b1a0bb04b7c", size = 5399904, upload-time = "2025-03-15T13:19:33.589Z" }, + { url = "https://files.pythonhosted.org/packages/91/9e/b82eed1bd96324ca76e9221702123fbd6daa14f2608d1d95a37a7f8b7d9b/pillow_heif-0.22.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b3035b4b4304e7624f9559989618ccad8ac614144befd30361ff51941b8c1b44", size = 3983900, upload-time = "2025-03-15T13:19:35.272Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/ce321dae8bd854fa0bdce6f2b349dac65eda095e5eb63af27ae61a6eb7de/pillow_heif-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73b7ce0d47e2dbd4f89de765a0c2d6e75f7dde95c5a5d87c4a6ad11bc9da4a3", size = 6980537, upload-time = "2025-03-15T13:19:36.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/393d8efd72912b06d3bbed7c7776ef6bfe97144ef460cbb8783dbf19c452/pillow_heif-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550093ee350c8cd404dbba61f7449d4ecc018109ab65f6f89b96a6dc39dde177", size = 7815808, upload-time = "2025-03-15T13:19:38.007Z" }, + { url = "https://files.pythonhosted.org/packages/60/9b/1b7532a6a0f8b3aea8426cb56820835f27a7623136f6355ab9c606eb6322/pillow_heif-0.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:646f2d05dd4a84ee1feb3a4aaa092269bf8f3f615c8f36d6e5d15b22a79d7cdd", size = 8314801, upload-time = "2025-03-15T13:19:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/8102775071070c56984494eb9582df37afb845f8b85ac948d9cba6390472/pillow_heif-0.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8ccd70ff3c30e93f8153fa08b5533aee54a73574c980784f0b74f5fba7b61e19", size = 9061610, upload-time = "2025-03-15T13:19:41.195Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0d/ca94079714327781522576e08fa8490afe601dfceae0727dd6b17dc112cf/pillow_heif-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf30bcaab9d2c0dbc43bb58d385faa9d3d8a693392beb50287aa6cda7a2f769e", size = 8567168, upload-time = "2025-03-15T13:19:43.307Z" }, + { url = "https://files.pythonhosted.org/packages/88/f8/010e13512e7fb455c934b2c50c14c0861f53f479f7fc5a81c48a17356131/pillow_heif-0.22.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:8b6e195b4cb17bf71e374b167f14be434dde54bb68afee6fba5aa1b6f7644bee", size = 5400135, upload-time = "2025-03-15T13:19:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a5/ff1ed132addbe550d0eeec1b32e0232e8abd51fd73a1807b44710cefb0e0/pillow_heif-0.22.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e31596116328d0a3bd5a3be9fbacea56e28d5950c824b12d2486e9989364bc0", size = 3983868, upload-time = "2025-03-15T13:19:46.231Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bf/6444b13f69e3b87ddec2bc1e4f5abad1ec6e1b5bff3e5899c97a64845de5/pillow_heif-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af08d451689539a2f9c4c6088180548b6146475f34d41a1334bc4ee1eab7a0b", size = 6978860, upload-time = "2025-03-15T13:19:47.498Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c6/8ebc06867ad21112ae1a71857b32d4d513f0e06e5913ac27e16c0a249ac7/pillow_heif-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a058d7243779f5b02736b16d5be8f4a13321cb9163dd06a3ea90052dd68cb8", size = 7814755, upload-time = "2025-03-15T13:19:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/a1/8aed37178aa5c50deca336bd50e9c491625978c0dba15ef02d71b91f7628/pillow_heif-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ff9f295a89b616e2f1648286752f269d4e3055f54884a7a46c5c74ea4b23c20c", size = 8313529, upload-time = "2025-03-15T13:19:50.198Z" }, + { url = "https://files.pythonhosted.org/packages/bf/89/c9ce1a30a6b5437f3cdf9fff7935f4ddb78c7b0bb7ac4385817ed863e046/pillow_heif-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f548d852405a84bdfbc76ec060e94c0b17c9a06da968c104fd6146d874d9f07", size = 9060942, upload-time = "2025-03-15T13:19:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5d/fd732649142a6d5872f4725ea23accc40290e9da704a3d4d284b5ba7f0d9/pillow_heif-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:f0e980ac065690a61732dbdc3bac50de4064d09df24fca435178bd63df31a180", size = 8567196, upload-time = "2025-03-15T13:19:54.341Z" }, + { url = "https://files.pythonhosted.org/packages/12/ec/6837894b01467d67e3e17714cf384635b6d97c1dc225681baf29ea7d08ed/pillow_heif-0.22.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:31a2a4838b3eacec665befbd621a43201c2ece0e35d636e001c4039cca875ba8", size = 5400123, upload-time = "2025-03-15T13:19:56.106Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6e/1b5ce58305496a3d1ade256e629ec717d30b292601a0ce99bd4568ced4e6/pillow_heif-0.22.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c5b25c2c4f147ca57e51ecfdd833c9ae9cbf00c8da34b7892ec0c8f4b57785b9", size = 3983862, upload-time = "2025-03-15T13:19:57.471Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/7cfc6fcf88877c6836312e47ee81171ad4fa63ffd53d99bc37402f7c6c2d/pillow_heif-0.22.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8563f14d76e544f5d1e8915c34dc2b01863351b7f74efbbaf9671d599b4ea5b", size = 6978827, upload-time = "2025-03-15T13:19:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/f1fdc597ddee1dec886225766c9ac5d8940fabda1515298f448273c69878/pillow_heif-0.22.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b7b5bdc5a3a953cdf1de8aa8ee83b0305ab6be3c7808b1ca67594df0d750e2", size = 7814723, upload-time = "2025-03-15T13:20:01.704Z" }, + { url = "https://files.pythonhosted.org/packages/06/3c/a0e63f3750a8c7e6800c4b5079ce55ed6538c69a604e92d6d9562278f8c1/pillow_heif-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9dcde90c30e61f1f0da30393bf1983fe8dad3b890f52406e617b7840c682948", size = 8313606, upload-time = "2025-03-15T13:20:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/e4/63/81c6c0b5ba2fc6474a9a68d44bd7c0c3891ec5bc4401b1c60823c3837ff4/pillow_heif-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:29caf663afcf142ac7ffb903fda4e5a01991054a0fe4abd379fef3d42575ca67", size = 9060974, upload-time = "2025-03-15T13:20:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/20/e3/bbd66a4f81a5e77e364effbedc8c5767ad1cc481f0a082093189e56d65e5/pillow_heif-0.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:bac5e9a4d85ffc724180eb0fa3aef304aa9b67faea6f86c33e4c2e6a447db098", size = 8567192, upload-time = "2025-03-15T13:20:07.909Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/3a0f0b0c7aea0b9bbfe7a2c12bc5f71d10016acacf69bea9ff76b63679ef/pillow_heif-0.22.0-pp310-pypy310_pp73-macosx_13_0_x86_64.whl", hash = "sha256:49f2258f08d85cab66408c48fa437accbe9b89f2387a4e847664645c8ce7e669", size = 5388679, upload-time = "2025-03-15T13:20:21.122Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ca/b20cff7e0ada0b51eaa6ead9060d1781635bd65d78c395a29abf0f412866/pillow_heif-0.22.0-pp310-pypy310_pp73-macosx_14_0_arm64.whl", hash = "sha256:a15ecf743d14bc772188bf8f25f15f81ddf9a441e8dbe53f6ee10582431ccb01", size = 3980472, upload-time = "2025-03-15T13:20:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/a05ee84acd657a85f7c79061bd766c5237111411bf11219336ee95de7aa0/pillow_heif-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760f910ff90a97f9764be79e55a14c4ad0470322bb60b40789b3a0c38200dd3", size = 6937700, upload-time = "2025-03-15T13:20:24.332Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/93e617eb1d37a4b9246f7cb4bb31757cb297eadceaf2ba7ad55ceb45a215/pillow_heif-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b91c8a76bafdf776c4251475805ce777c2803e30386e997738a6b5c80efe437", size = 7770893, upload-time = "2025-03-15T13:20:25.683Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b2/7650b22ceb08fbe5c8981cc2a0785406e053cc37ae8e7bc882a55fb35ea0/pillow_heif-0.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7c70fe0b5b6c232749ac7cbb608f0732a66804598a422cfcfbb1fb77e076f77", size = 8567539, upload-time = "2025-03-15T13:20:27.095Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "plotly" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/05/1199e2a03ce6637960bc1e951ca0f928209a48cfceb57355806a88f214cf/plotly-6.5.0.tar.gz", hash = "sha256:d5d38224883fd38c1409bef7d6a8dc32b74348d39313f3c52ca998b8e447f5c8", size = 7013624, upload-time = "2025-11-17T18:39:24.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl", hash = "sha256:5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a", size = 9893174, upload-time = "2025-11-17T18:39:20.351Z" }, +] + +[[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 = "plyfile" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d8/f68ec9a54568236ba4c00fc0b002f74d2a559841c1fce86ab356599da032/plyfile-1.1.3.tar.gz", hash = "sha256:1c37720cb0470b762cec2dfef573ee7996a616c359c0ec34fdd766ace3ea0634", size = 36163, upload-time = "2025-10-22T01:58:40.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/22/1755bb4c7db15bb1ed63b4eb7a7fc133bf42a3f9cc806c0d5941e107ba90/plyfile-1.1.3-py3-none-any.whl", hash = "sha256:581302f07b1c298431dcaa9038bba2ae80f3f7868b29ccb826a07bc4488ff38a", size = 36455, upload-time = "2025-10-22T01:58:38.614Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.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/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, +] + +[[package]] +name = "proglog" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/af/c108866c452eda1132f3d6b3cb6be2ae8430c97e9309f38ca9dbd430af37/proglog-0.1.12.tar.gz", hash = "sha256:361ee074721c277b89b75c061336cb8c5f287c92b043efa562ccf7866cda931c", size = 8794, upload-time = "2025-05-09T14:36:18.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/1b/f7ea6cde25621cd9236541c66ff018f4268012a534ec31032bcb187dc5e7/proglog-0.1.12-py3-none-any.whl", hash = "sha256:ccaafce51e80a81c65dc907a460c07ccb8ec1f78dc660cfd8f9ec3a22f01b84c", size = 6337, upload-time = "2025-05-09T14:36:16.798Z" }, +] + +[[package]] +name = "pycolmap" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/6f/5511b1035e3660b7e09741962bd9cd41da42be648b3ae563792acd78b60d/pycolmap-3.13.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:823e6463f490ba58570c4c63b4a242d0afca005f572927abc55bb65d58484f0a", size = 14513187, upload-time = "2025-11-07T17:00:02.582Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ec/cf4ae8384f8b941a0c220eb460ecdbb01d7a0f149d11d61d38b9d1044df8/pycolmap-3.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:40c03735760a97f6e242cb7c6c748aed9314d0425afb8e590f684dbc47581888", size = 20320230, upload-time = "2025-11-07T17:00:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/be8960a01ea1fd08df9e25a935f617476d7fd650e36ba52c16a2e71fc8b8/pycolmap-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d42f7487cb462dc024bd1e78670880b953ffa4b1cd60fba250eea8101a13a7d7", size = 18644691, upload-time = "2025-11-07T17:00:07.861Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e599a321a5c0308e1cebc5d757645c827df6f645fd4835ba2f47eb62e7c9/pycolmap-3.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6f1c0e6c7594caaa5fd3f556a01a349ddeb41f1c5b06579eeb51acb14395f606", size = 14515957, upload-time = "2025-11-07T17:00:10.677Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/0145456119a22f7f8436dd6b994cb1defa11d56d703ba5514bc4be9e918d/pycolmap-3.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:88e7faecd46a4db7839ea1fe71bd481b477e153bf0cfd598ed541740c7be834d", size = 20320211, upload-time = "2025-11-07T17:00:13.171Z" }, + { url = "https://files.pythonhosted.org/packages/56/70/9a7c7c0a9077d005baa19b241c054bf0103d3c00b6783d5210bf206f25f0/pycolmap-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:e80fefab544bb57039375abd3be16108a4a079d0345270872b2387ca3ac62325", size = 18645230, upload-time = "2025-11-07T17:00:15.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a5/adeec1cbf9881b7c7259c33111408f618ff849b6474cd73652ff628a6ef6/pycolmap-3.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6ea432f1f9b1dab0e805678d5280d55fbe034a12c3a5fa5bd3c85792d523b27a", size = 14541939, upload-time = "2025-11-07T17:00:21.677Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c2/73bdc187c4b8c819b4a0d13523d98b8a72ce18fb689d613c3554fd14b5ab/pycolmap-3.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fd1b676c2f8ecf7f18e5a9240347ad4946ccd0c83bc92f5fdc39a498ae6c998b", size = 20337524, upload-time = "2025-11-07T17:00:23.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/31018a15af2ad0da922946a4f28c31de2ead79ad8f08a5cd0961a1d87fda/pycolmap-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:070e7554cfb9f0443876a055153547a4b54addbec77519f10269b4601dde63b1", size = 18661604, upload-time = "2025-11-07T17:00:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c3eb91b2b25b3ebbb32c95a9777a3cb9ea0fbb4318dab54bd050bf7891a0/pycolmap-3.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:46d2108eaa1191584796a63f17a5f0204d59e30ec5f2778489ce44e19fff6ac2", size = 14542168, upload-time = "2025-11-07T17:00:30.052Z" }, + { url = "https://files.pythonhosted.org/packages/d1/aa/342ae17a3f22beffe3fd01ffff7afd28d66dcb57c306ef4991dfc738255a/pycolmap-3.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:dc749231ef310d867ad33db56feb9656c9223dd5db8bd816007b8cfdf7f1b0cc", size = 20337637, upload-time = "2025-11-07T17:00:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/53/54/e85329ec5768bd3d13b0e0217e3b07c215003df487e5c95bb6df43768c7d/pycolmap-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:906511975ec614d9dd4f7566f38e17523106002f69b65adca3b13b1ed261088c", size = 18661874, upload-time = "2025-11-07T17:00:35.6Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.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/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pyquaternion" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/3d092aa20efaedacb89c3221a92c6491be5b28f618a2c36b52b53e7446c2/pyquaternion-0.9.9.tar.gz", hash = "sha256:b1f61af219cb2fe966b5fb79a192124f2e63a3f7a777ac3cadf2957b1a81bea8", size = 15530, upload-time = "2020-10-05T01:31:30.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/d8482e8cacc8ea15a356efea13d22ce1c5914a9ee36622ba250523240bf2/pyquaternion-0.9.9-py3-none-any.whl", hash = "sha256:e65f6e3f7b1fdf1a9e23f82434334a1ae84f14223eee835190cd2e841f8172ec", size = 14361, upload-time = "2020-10-05T01:31:37.575Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.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" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "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'" }, +] +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.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "retrying" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rosbags" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lz4" }, + { name = "numpy" }, + { name = "ruamel-yaml" }, + { name = "typing-extensions" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/2f/b6d11cc9fac4003b284e2c152b5c34ca5b6d26caf5f6979118e2d588a0d7/rosbags-0.11.0.tar.gz", hash = "sha256:e73bbe79a15d7f539653375278ffca6c48e56b5c2ada4a4531de0b17737cbc46", size = 254438, upload-time = "2025-10-15T09:49:54.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/75/b66d992a87539cd9e0070bca6ba49694b9258a4f9bc905d0576354d7864a/rosbags-0.11.0-py3-none-any.whl", hash = "sha256:2ce27d8dc37f554f10bf5962f665331ce16f43aba304c6c233eb15fab05b61ab", size = 137920, upload-time = "2025-10-15T09:49:50.329Z" }, +] + +[[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/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.18.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, + { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, + { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, +] + +[[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-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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.16.3" +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 sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform != 'darwin'" }, + { name = "jeepney", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[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 = "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 = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/9b/42f93f459cf03062c8b3aab812475f01456fd42e04b08bad69bcaedd15c8/tomlkit-0.12.0.tar.gz", hash = "sha256:01f0477981119c7d8ee0f67ebe0297a7c95b14cf9f4b102b45486deb77018716", size = 190497, upload-time = "2023-07-27T07:49:05.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/4f/12207897848a653d03ebbf6775a29d949408ded5f99b2d87198bc5c93508/tomlkit-0.12.0-py3-none-any.whl", hash = "sha256:926f1f37a1587c7a4f6c7484dae538f1345d96d793d9adab5d3675957b1d0766", size = 37334, upload-time = "2023-07-27T07:49:04.789Z" }, +] + +[[package]] +name = "torch" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/86/245c240d2138c17ed572c943c289056c2721abab70810d772c6bf5495b28/torch-2.9.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:030bbfe367379ae6a4ae4042b6c44da25383343b8b3c68abaa9c7231efbaf2dd", size = 104213554, upload-time = "2025-10-15T15:45:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/58/1d/fd1e88ae0948825efcab7dd66d12bec23f05d4d38ed81573c8d453c14c06/torch-2.9.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:51cb63902182a78e90886e8068befd8ea102af4b00e420263591a3d70c7d3c6c", size = 899795167, upload-time = "2025-10-15T15:47:12.695Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/496197b45c14982bef4e079b24c61dc108e3ab0d0cc9718dba9f54f45a46/torch-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:3f6aad4d2f0ee2248bac25339d74858ff846c3969b27d14ac235821f055af83d", size = 109310314, upload-time = "2025-10-15T15:46:16.633Z" }, + { url = "https://files.pythonhosted.org/packages/58/b0/2b4e647b0fc706e88eb6c253d05511865578f5f67b55fad639bf3272a4a1/torch-2.9.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:413e1654c9203733138858780e184d9fc59442f0b3b209e16f39354eb893db9b", size = 74452019, upload-time = "2025-10-15T15:46:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/58/fe/334225e6330e672b36aef23d77451fa906ea12881570c08638a91331a212/torch-2.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c596708b5105d0b199215acf0c9be7c1db5f1680d88eddadf4b75a299259a677", size = 104230578, upload-time = "2025-10-15T15:46:08.182Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/49566caaa218872ec9a2912456f470ff92649894a4bc2e5274aa9ef87c4a/torch-2.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51de31219c97c51cf4bf2be94d622e3deb5dcc526c6dc00e97c17eaec0fc1d67", size = 899815990, upload-time = "2025-10-15T15:48:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/e9ab21d5925b642d008f139d4a3c9664fc9ee1faafca22913c080cc4c0a5/torch-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd515c70059afd95f48b8192733764c08ca37a1d19803af6401b5ecad7c8676e", size = 109313698, upload-time = "2025-10-15T15:46:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b7/205ef3e94de636feffd64b28bb59a0dfac0771221201b9871acf9236f5ca/torch-2.9.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:614a185e4986326d526a91210c8fc1397e76e8cfafa78baf6296a790e53a9eec", size = 74463678, upload-time = "2025-10-15T15:46:29.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898, upload-time = "2025-10-15T15:46:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273, upload-time = "2025-10-15T15:50:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887, upload-time = "2025-10-15T15:46:26.228Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983, upload-time = "2025-10-15T15:46:39.406Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1c/90eb13833cdf4969ea9707586d7b57095c3b6e2b223a7256bf111689bcb8/torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e", size = 104111330, upload-time = "2025-10-15T15:46:35.238Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/2254c54b8d523592c25ef4434769aa23e29b1e6bf5f4c0ad9e27bf442927/torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642", size = 899750243, upload-time = "2025-10-15T15:48:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a5/5cb94fa4fd1e78223455c23c200f30f6dc10c6d4a2bcc8f6e7f2a2588370/torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece", size = 109284513, upload-time = "2025-10-15T15:46:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/66/e8/fc414d8656250ee46120b44836ffbb3266343db424b3e18ca79ebbf69d4f/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5", size = 74830362, upload-time = "2025-10-15T15:46:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/9474c98fc5ae0cd04b9466035428cd360e6611a86b8352a0fc2fa504acdc/torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637", size = 104144940, upload-time = "2025-10-15T15:47:29.076Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/8e0c1cf57830172c109d4bd6be2708cabeaf550983eee7029291322447a0/torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e", size = 899744054, upload-time = "2025-10-15T15:48:29.864Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/82c28b30fcb4b7c9cdd995763d18bbb830d6521356712faebbad92ffa61d/torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8", size = 109517546, upload-time = "2025-10-15T15:47:33.395Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a91f96ec74347fa5fd24453fa514bc61c61ecc79196fa760b012a1873d96/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a", size = 74480732, upload-time = "2025-10-15T15:47:38.002Z" }, +] + +[[package]] +name = "torchvision" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/5b/1404eeab00819df71a30e916c2081654366741f7838fcc4fff86b7bd9e7e/torchvision-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8d5e667deff87bd66d26df6d225f46224bb0782d4f3f8f5d2f3068b5fd4492", size = 1891723, upload-time = "2025-10-15T15:51:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/88/e3/1b003ecd52bd721f8304aeb66691edfbc2002747ec83d36188ad6abab506/torchvision-0.24.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a110a51c75e89807a8382b0d8034f5e180fb9319570be3389ffd3d4ac4fd57a9", size = 2418988, upload-time = "2025-10-15T15:51:25.195Z" }, + { url = "https://files.pythonhosted.org/packages/56/2e/3c19a35e62da0f606baf8f6e2ceeab1eb66aaa2f84c6528538b06b416d54/torchvision-0.24.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:81d5b12a6df1bb2cc8bdbad837b637d6ea446f2866e6d94f1b5d478856331be3", size = 8046769, upload-time = "2025-10-15T15:51:15.221Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1d/e7ab614a1ace820a2366eab1532679fbe81bd9501ffd6a1b7be14936366d/torchvision-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:0839dbb305d34671f5a64f558782095134b04bbeff8b90f11eb80515d7d50092", size = 3686529, upload-time = "2025-10-15T15:51:20.982Z" }, + { url = "https://files.pythonhosted.org/packages/a3/17/54ed2ec6944ea972b461a86424c8c7f98835982c90cbc45bf59bd962863a/torchvision-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f771cf918351ad509a28488be475f3e9cc71a750d6b1467842bfb64863a5e986", size = 1891719, upload-time = "2025-10-15T15:51:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/f8/07/0cd6776eee784742ad3cb2bfd3295383d84cb2f9e87386119333d1587f0f/torchvision-0.24.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbd63bf4ebff84c48c50123eba90526cc9f794fe45bc9f5dd07cec19e8c62bce", size = 2420513, upload-time = "2025-10-15T15:51:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f4/6026c08011ddcefcbc14161c5aa9dce55c35c6b045e04ef0952e88bf4594/torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:78fe414b3bb6dbf7e6f6da6f733ba96881f6b29a9b997228de7c5f603e5ed940", size = 8048018, upload-time = "2025-10-15T15:51:13.579Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b4/362b4e67ed87cee0fb4f8f0363a852eaeef527968bf62c07ed56f764d729/torchvision-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:629584b94e52f32a6278f2a35d85eeaae95fcc38730fcb765064f26c3c96df5d", size = 4027686, upload-time = "2025-10-15T15:51:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/47/ef/81e4e69e02e2c4650b30e8c11c8974f946682a30e0ab7e9803a831beff76/torchvision-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c61d40bcd2e2451e932902a702ad495ba1ec6f279e90b1e15cef2bb55dc911e2", size = 1891726, upload-time = "2025-10-15T15:51:16.977Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e3809b3302caea9a12c13f3adebe4fef127188438e719fd6c8dc93db1da6/torchvision-0.24.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b0531d1483fc322d7da0d83be52f0df860a75114ab87dbeeb9de765feaeda843", size = 2419495, upload-time = "2025-10-15T15:51:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e6/7324ead6793075a8c75c56abeed1236d1750de16a5613cfe2ddad164a92a/torchvision-0.24.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26b9dd9c083f8e5f7ac827de6d5b88c615d9c582dc87666770fbdf16887e4c25", size = 8050480, upload-time = "2025-10-15T15:51:24.012Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ad/3c56fcd2a0d6e8afa80e115b5ade4302232ec99655220a51d05709819523/torchvision-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:060b7c50ed4b3fb0316b08e2e31bfd874ec2f63ef5ae02f81e54341ca4e88703", size = 4292225, upload-time = "2025-10-15T15:51:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/b2008e4b77a8d6aada828dd0f6a438d8f94befa23fdd2d62fa0ac6e60113/torchvision-0.24.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84d79cfc6457310107ce4d712de7a3d388b24484bc9aeded4a76d8f8e3a2813d", size = 1891722, upload-time = "2025-10-15T15:51:28.854Z" }, + { url = "https://files.pythonhosted.org/packages/8f/02/e2f6b0ff93ca4db5751ac9c5be43f13d5e53d9e9412324f464dca1775027/torchvision-0.24.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fec12a269cf80f6b0b71471c8d498cd3bdd9d8e892c425bf39fecb604852c3b0", size = 2371478, upload-time = "2025-10-15T15:51:37.842Z" }, + { url = "https://files.pythonhosted.org/packages/77/85/42e5fc4f716ec7b73cf1f32eeb5c77961be4d4054b26cd6a5ff97f20c966/torchvision-0.24.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7323a9be5e3da695605753f501cdc87824888c5655d27735cdeaa9986b45884c", size = 8050200, upload-time = "2025-10-15T15:51:46.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/48cb0b6b26276d2120b1e0dbc877579a748eae02b4091a7522ce54f6d5e1/torchvision-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:08cad8b204196e945f0b2d73adee952d433db1c03645851d52b22a45f1015b13", size = 4309939, upload-time = "2025-10-15T15:51:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/3dd10830b047eeb46ae6b465474258d7b4fbb7d8872dca69bd42449f5c82/torchvision-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ab956a6e588623353e0f20d4b03eb1656cb4a3c75ca4dd8b4e32e01bc43271a", size = 2028355, upload-time = "2025-10-15T15:51:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/2d7e43409089ce7070f5336161f9216d58653ee1cb26bcb5d6c84cc2de36/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b1b3db80609c32a088554e8e94b4fc31f1033fe5bb4ac0673ec49c3eb03fb4da", size = 2374466, upload-time = "2025-10-15T15:51:35.382Z" }, + { url = "https://files.pythonhosted.org/packages/e9/30/8f7c328fd7e0a9665da4b6b56b1c627665c18470bfe62f3729ad3eda9aec/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:e6635f100d455c80b43f297df4b8585a76c6a2e114802f6567ddd28d7b5479b0", size = 8217068, upload-time = "2025-10-15T15:51:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/55/a2/b6f9e40e2904574c80b3bb872c66af20bbd642053e7c8e1b9e99ab396535/torchvision-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4ce158bbdc3a9086034bced0b5212888bd5b251fee6d08a9eff151d30b4b228a", size = 4273912, upload-time = "2025-10-15T15:51:33.866Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "trimesh" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/f3/bd9b67575bddcc7e4cca927461bd58006455208a2331262f7c9de0db8bb3/trimesh-4.10.0.tar.gz", hash = "sha256:502710a0b1f0317816507828a41e0cb1c595b895e344567fa42cd47388c2b72b", size = 831462, upload-time = "2025-11-24T19:57:51.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/81/951a108396aa850a096be95cd9ccd38ef9f33ae35b40b512be975e2c994d/trimesh-4.10.0-py3-none-any.whl", hash = "sha256:bdafda66d5c8a9564d7d82f5673c06f8b4fc51c86de959fafee0e169fcfea12a", size = 736626, upload-time = "2025-11-24T19:57:49.158Z" }, +] + +[[package]] +name = "triton" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/eb/09e31d107a5d00eb281aa7e6635ca463e9bca86515944e399480eadb71f8/triton-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5d3b3d480debf24eaa739623c9a42446b0b77f95593d30eb1f64cd2278cc1f0", size = 170333110, upload-time = "2025-10-13T16:37:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/3d/78/949a04391c21956c816523678f0e5fa308eb5b1e7622d88c4e4ef5fceca0/triton-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f34bfa21c5b3a203c0f0eab28dcc1e49bd1f67d22724e77fb6665a659200a4ec", size = 170433488, upload-time = "2025-10-13T16:37:57.132Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3a/e991574f3102147b642e49637e0281e9bb7c4ba254edb2bab78247c85e01/triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833", size = 170476535, upload-time = "2025-10-13T16:38:05.18Z" }, + { url = "https://files.pythonhosted.org/packages/6c/29/10728de8a6e932e517c10773486b8e99f85d1b1d9dd87d9a9616e1fef4a1/triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12", size = 170487289, upload-time = "2025-10-13T16:38:11.662Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/db80e48b9220c9bce872b0f616ad0446cdf554a40b85c7865cbca99ab3c2/triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9", size = 170577179, upload-time = "2025-10-13T16:38:17.865Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "typer" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953, upload-time = "2024-08-24T21:17:57.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288, upload-time = "2024-08-24T21:17:55.451Z" }, +] + +[[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 = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[[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'" }, +] +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 = "wadler-lindig" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/67/cbae4bf7683a64755c2c1778c418fea96d00e34395bb91743f08bd951571/wadler_lindig-0.1.7.tar.gz", hash = "sha256:81d14d3fe77d441acf3ebd7f4aefac20c74128bf460e84b512806dccf7b2cd55", size = 15842, upload-time = "2025-06-18T07:00:42.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/96/04e7b441807b26b794da5b11e59ed7f83b2cf8af202bd7eba8ad2fa6046e/wadler_lindig-0.1.7-py3-none-any.whl", hash = "sha256:e3ec83835570fd0a9509f969162aeb9c65618f998b1f42918cfc8d45122fe953", size = 20516, upload-time = "2025-06-18T07:00:41.684Z" }, +] + +[[package]] +name = "websockets" +version = "12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload-time = "2023-10-21T14:21:11.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/b9/360b86ded0920a93bff0db4e4b0aa31370b0208ca240b2e98d62aad8d082/websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", size = 124025, upload-time = "2023-10-21T14:19:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d3/1eca0d8fb6f0665c96f0dc7c0d0ec8aa1a425e8c003e0c18e1451f65d177/websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", size = 121261, upload-time = "2023-10-21T14:19:30.203Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/f6c3ecf7f1bfd9209e13949db027d7fdea2faf090c69b5f2d17d1d796d96/websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", size = 121328, upload-time = "2023-10-21T14:19:31.765Z" }, + { url = "https://files.pythonhosted.org/packages/74/4d/f88eeceb23cb587c4aeca779e3f356cf54817af2368cb7f2bd41f93c8360/websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", size = 130925, upload-time = "2023-10-21T14:19:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/16/17/f63d9ee6ffd9afbeea021d5950d6e8db84cd4aead306c6c2ca523805699e/websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", size = 129930, upload-time = "2023-10-21T14:19:35.109Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/c7a7504f5bf74d6ee0533f6fc7d30d8f4b79420ab179d1df2484b07602eb/websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", size = 130245, upload-time = "2023-10-21T14:19:36.761Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6a/3600c7771eb31116d2e77383d7345618b37bb93709d041e328c08e2a8eb3/websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", size = 134966, upload-time = "2023-10-21T14:19:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/22/26/df77c4b7538caebb78c9b97f43169ef742a4f445e032a5ea1aaef88f8f46/websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", size = 134196, upload-time = "2023-10-21T14:19:40.264Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/18ce9a4a08203c8d0d3d561e3ea4f453daf32f099601fc831e60c8a9b0f2/websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", size = 134822, upload-time = "2023-10-21T14:19:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/51/1f823a341fc20a880e67ae62f6c38c4880a24a4b60fbe544a38f516f39a1/websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", size = 124454, upload-time = "2023-10-21T14:19:43.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/5ec054cfcf23adfc88d39359b85e81d043af8a141e3ac8ce40f45a5ce5f4/websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", size = 124974, upload-time = "2023-10-21T14:19:44.934Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/9c1e168a2e7fdf26841dc98f5f5502e91dea47428da7690a08101f616169/websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", size = 124047, upload-time = "2023-10-21T14:19:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2d/9a683359ad2ed11b2303a7a94800db19c61d33fa3bde271df09e99936022/websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", size = 121282, upload-time = "2023-10-21T14:19:47.739Z" }, + { url = "https://files.pythonhosted.org/packages/95/aa/75fa3b893142d6d98a48cb461169bd268141f2da8bfca97392d6462a02eb/websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", size = 121325, upload-time = "2023-10-21T14:19:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/51a25e591d645df71ee0dc3a2c880b28e5514c00ce752f98a40a87abcd1e/websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", size = 131502, upload-time = "2023-10-21T14:19:50.683Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ea/0ceeea4f5b87398fe2d9f5bcecfa00a1bcd542e2bfcac2f2e5dd612c4e9e/websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", size = 130491, upload-time = "2023-10-21T14:19:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/e3/05/f52a60b66d9faf07a4f7d71dc056bffafe36a7e98c4eb5b78f04fe6e4e85/websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", size = 130872, upload-time = "2023-10-21T14:19:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/c7361b2d7b964c40fea924d64881145164961fcd6c90b88b7e3ab2c4f431/websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", size = 136318, upload-time = "2023-10-21T14:19:54.41Z" }, + { url = "https://files.pythonhosted.org/packages/0a/31/337bf35ae5faeaf364c9cddec66681cdf51dc4414ee7a20f92a18e57880f/websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", size = 135594, upload-time = "2023-10-21T14:19:55.982Z" }, + { url = "https://files.pythonhosted.org/packages/95/aa/1ac767825c96f9d7e43c4c95683757d4ef28cf11fa47a69aca42428d3e3a/websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", size = 136191, upload-time = "2023-10-21T14:19:57.349Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/344ec5cfeb6bc417da097f8253607c3aed11d9a305fb58346f506bf556d8/websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", size = 124453, upload-time = "2023-10-21T14:19:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/6b169cd1957476374f51f4486a3e85003149e62a14e6b78a958c2222337a/websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", size = 124971, upload-time = "2023-10-21T14:20:00.243Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061, upload-time = "2023-10-21T14:20:02.221Z" }, + { url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296, upload-time = "2023-10-21T14:20:03.591Z" }, + { url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326, upload-time = "2023-10-21T14:20:04.956Z" }, + { url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807, upload-time = "2023-10-21T14:20:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751, upload-time = "2023-10-21T14:20:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176, upload-time = "2023-10-21T14:20:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246, upload-time = "2023-10-21T14:20:10.423Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466, upload-time = "2023-10-21T14:20:11.826Z" }, + { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload-time = "2023-10-21T14:20:13.451Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload-time = "2023-10-21T14:20:14.719Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload-time = "2023-10-21T14:20:15.817Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/554a8a8bb6da9dd1ce04c44125e2192af7b7beebf6e3dbfa5d0e285cc20f/websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", size = 121110, upload-time = "2023-10-21T14:20:48.335Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8e/58b8812940d746ad74d395fb069497255cb5ef50748dfab1e8b386b1f339/websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", size = 123216, upload-time = "2023-10-21T14:20:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/272cb67ace1786ce6d9f39d47b3c55b335e8b75dd1972a7967aad39178b6/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", size = 122821, upload-time = "2023-10-21T14:20:51.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/03/387fc902b397729df166763e336f4e5cec09fe7b9d60f442542c94a21be1/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", size = 122768, upload-time = "2023-10-21T14:20:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/50/f0/5939fbc9bc1979d79a774ce5b7c4b33c0cefe99af22fb70f7462d0919640/websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", size = 125009, upload-time = "2023-10-21T14:20:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload-time = "2023-10-21T14:21:10.075Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] + +[[package]] +name = "xformers" +version = "0.0.33.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform != 'darwin'" }, + { name = "torch", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/c1/cd0d6b89da38d8aa174e8eabf29530f8871daf53b886ec6b680ef9d3e71f/xformers-0.0.33.post1.tar.gz", hash = "sha256:e555258249b514ba117b3403523fe0bd7d3e92e930575f0e0dbf5f7db5b42677", size = 14784437, upload-time = "2025-11-13T20:16:14.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/94/3ad80d1070ddfb280c20a67dfbc094a93579a02910ef41f20631a9b566fe/xformers-0.0.33.post1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a8d72c6272453450eede2ed9aaa14448e6525569e14217573057ded146090db3", size = 122884756, upload-time = "2025-11-13T20:16:04.002Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ef/4f59589fe37e206f5bb6158aa1294cfa0e79d52bca99ea0fd3f5c8a73404/xformers-0.0.33.post1-cp39-abi3-win_amd64.whl", hash = "sha256:e20729ca1647d53f86143bd57451af953bb78e72677548c972cd016238a066e3", size = 105088581, upload-time = "2025-11-13T20:16:11.221Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, +]