diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 89737aa..3dfa0fe 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Agent skill and Claude Code plugin for the remove-ai-watermarks CLI.", - "version": "1.0.4" + "version": "1.0.5" }, "plugins": [ { diff --git a/.claude/rules/development.md b/.claude/rules/development.md index 22e9ba0..af098c0 100644 --- a/.claude/rules/development.md +++ b/.claude/rules/development.md @@ -73,6 +73,11 @@ The same rule applies to install hints: name the extra that actually makes the c Run `bash maintain.sh` from the repository root. The authoritative type gate is scoped to `src/`; full-project Pyright can exhaust Node memory on the ML dependency graph. +The security scanner's exit status is authoritative, including a failure after a success +message. `tests/test_maintenance.py` checks clean, vulnerable, and broken scanner +runs through the real shell entry point. Recheck unresolved advisories using the +procedure in `docs/development.md`; do not revive the old text-matching bypass. + Boundary modules for cv2, Torch, and Diffusers may carry narrow per-file relaxations for unknown third-party types. Keep pure-logic files strict, preserve the local piexif stub, and fix real errors before widening a pragma. From a worktree, `uv run` imports the package from the MAIN checkout -- that is where the editable install points. A script measuring a worktree's edit must insert that worktree's `src` at `sys.path[0]` and assert `module.__file__` resolves inside it, or it silently compares unmodified code against itself. @@ -262,3 +267,25 @@ frame-count check cannot see and what catch. Environment setup, dependency recovery, CI behavior, and fixture policy: [`../../docs/development.md`](../../docs/development.md). + +## Audit regression contracts + +Image output paths must describe the artifact published by the current call, +including skipped invisible stages and in-place writes. Carry explicit EXIF +decode state through image staging; equal dimensions do not prove that a +mirror or 180-degree rotation was applied. Metadata collection must propagate +late-read failures into an incomplete record, and stream-copy metadata removal +must map every input stream before atomically publishing the output. + +Research observations must describe the decoded, hash-bound saved artifact. +Keep period selection independent of confirmation patches, require explicit +negative evidence, and deduplicate source corpora by content identity rather +than detector-score equality. Use the metric's actual denominator in its name; +normalized edit distance is not reference-normalized character error rate. + +Tests that reload a module after changing environment variables must restore +the environment before reloading the original state. Otherwise a temporary +cache path survives teardown and later model tests silently attempt downloads. +Dotenv configuration tests must clear every fixture variable from the invoked +process environment. Seed conflicting inherited values to prove isolation; +clearing only some fields lets the remaining host settings override the fixture. diff --git a/.env.example b/.env.example index dea13ba..a418ca3 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,23 @@ # Get yours at: https://huggingface.co/settings/tokens # HF_TOKEN= -# OpenAI API key (optional; only for explicit verify-openai-synthid uploads) +# OpenAI API key (optional; only for development scripts/openai_provenance_check.py) # Create and manage keys at: https://platform.openai.com/api-keys # OPENAI_API_KEY= +# Local photo-classifier weights (optional; otherwise fetched from Hugging Face) +# Directory contents are documented in docs/photo-classify.md. +# RAIW_CLASSIFY_WEIGHTS=/path/to/classifier-weights + +# Local VideoSeal checkpoint cache (optional; development benchmarks only) +# Defaults to ~/.cache/remove-ai-watermarks-dev/videoseal. +# VIDEOSEAL_CACHE_DIR=/path/to/videoseal-cache + +# Hugging Face cache root (optional; shared by model downloads and prompt caches) +# HF_HOME=/path/to/huggingface-cache +# Fallback cache root when HF_HOME is unset (optional) +# XDG_CACHE_HOME=/path/to/cache + # Named OpenAI keys for provider-oracle slots (optional; keep each value secret) # OPENAI_API_KEY_1= # OPENAI_API_KEY_2= diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3eac6d1..57db320 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,7 +15,7 @@ updates: - "minor" - "patch" # The `all` extra must remain jointly resolvable. These candidates violate - # upstream constraints: TrustMark still requires NumPy 1.x, OpenCV 4.12+ + # compatibility constraints: older supported TrustMark builds require NumPy 1.x, OpenCV 4.12+ # requires NumPy 2 on newer Python versions, and this project keeps tokenizers # below 0.23 for the stable Transformers path. Keep Dependabot on the # compatible lines until those constraints move. diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml index 3b9b7ed..e768e2a 100644 --- a/.github/workflows/distribute.yml +++ b/.github/workflows/distribute.yml @@ -42,6 +42,27 @@ jobs: echo "version=$v" >> "$GITHUB_OUTPUT" echo "Target version: $v" + - name: Record distributed release + env: + VERSION: ${{ steps.v.outputs.version }} + run: | + python3 - <<'PYTHON' + import json, os + from pathlib import Path + Path("release-identity.json").write_text(json.dumps({ + "version": os.environ["VERSION"], + "run_id": os.environ["GITHUB_RUN_ID"], + "run_attempt": os.environ["GITHUB_RUN_ATTEMPT"], + }), encoding="utf-8") + PYTHON + + - name: Upload distributed release identity + uses: actions/upload-artifact@v7 + with: + name: distributed-release-${{ github.run_attempt }} + path: release-identity.json + if-no-files-found: error + - name: Wait for the sdist on PyPI, capture url + sha256 id: sdist run: | @@ -130,15 +151,15 @@ jobs: print(m.group(1)) PY ) - clawhub login --token "$CLAWHUB_TOKEN" >/dev/null - published=$(clawhub inspect remove-ai-watermarks --file SKILL.md 2>/dev/null \ + ./node_modules/.bin/clawhub login --token "$CLAWHUB_TOKEN" >/dev/null + published=$(./node_modules/.bin/clawhub inspect remove-ai-watermarks --file SKILL.md 2>/dev/null \ | python3 -c 'import re,sys; t=sys.stdin.read(); m=re.search(r"(?m)^ version:\s*[\"'"'"']?([^\"'"'"'\s]+)", t); print(m.group(1) if m else "")' || true) if [ "$published" = "$skill_version" ]; then echo "ClawHub already at $skill_version; nothing to publish." exit 0 fi echo "ClawHub at ${published:-}; publishing skill $skill_version." - clawhub skill publish skills/remove-ai-watermarks \ + ./node_modules/.bin/clawhub skill publish skills/remove-ai-watermarks \ --slug remove-ai-watermarks \ --name "Remove AI Watermarks" \ --version "$skill_version" \ diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml index c356f0a..ef7ae24 100644 --- a/.github/workflows/verify-release.yml +++ b/.github/workflows/verify-release.yml @@ -17,11 +17,10 @@ on: workflows: ["Distribute on release"] types: [completed] -# No step reads the GitHub token any more: the ComfyUI check moved to the -# registry's public API. Keep the job read-only so it cannot write on a repo -# whose default token permission is read/write. +# Read the triggering run's release identity while keeping all permissions read-only. permissions: contents: read + actions: read jobs: verify: @@ -37,16 +36,51 @@ jobs: SPACE_URL: https://wiltodelta-remove-ai-watermarks.hf.space steps: + - name: Download distributed release identity + if: ${{ github.event_name == 'workflow_run' }} + uses: actions/download-artifact@v8 + with: + pattern: distributed-release-* + path: distribution-identities + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - name: Resolve target version id: v + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ github.event.inputs.version }} + DISTRIBUTION_RUN_ID: ${{ github.event.workflow_run.id }} + DISTRIBUTION_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} run: | set -euo pipefail - if [ -n "${{ github.event.inputs.version }}" ]; then - v="${{ github.event.inputs.version }}" + if [ "$EVENT_NAME" = "workflow_run" ]; then + v=$(python3 - <<'PYTHON' + import json, os + from pathlib import Path + identities = [] + for path in Path("distribution-identities").glob("*/release-identity.json"): + item = json.loads(path.read_text(encoding="utf-8")) + assert path.parent.name == 'distributed-release-' + item['run_attempt'], "Artifact attempt mismatch" + identities.append(item) + assert identities, "No distributed release identity found" + assert all(item["run_id"] == os.environ["DISTRIBUTION_RUN_ID"] for item in identities), "Distribution run mismatch" + # Rerunning only failed jobs can reuse resolve from an earlier attempt. + eligible = [item for item in identities if + int(item["run_attempt"]) <= int(os.environ["DISTRIBUTION_RUN_ATTEMPT"])] + assert eligible, "No identity exists for this distribution attempt" + identity = max(eligible, key=lambda item: int(item["run_attempt"])) + print(identity["version"]) + PYTHON + ) + elif [ -n "$INPUT_VERSION" ]; then + v="$INPUT_VERSION" else v=$(curl -fsSL https://pypi.org/pypi/remove-ai-watermarks/json \ | python3 -c "import sys,json;print(json.load(sys.stdin)['info']['version'])") fi + # Versions become URL components and step outputs. Reject malformed identities. + [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.+-]*)$ ]] echo "version=$v" >> "$GITHUB_OUTPUT" echo "Verifying release $v" @@ -116,14 +150,30 @@ jobs: # review window a correctly published version looks identical to one # that was never uploaded. Those are opposite situations: one is a # queue nobody here controls, the other is a broken release. + python3 -m pip install --quiet packaging versions=$(curl -fsSL "https://api.comfy.org/nodes/remove-ai-watermarks/versions") status=$(printf '%s' "$versions" | VERSION="$VERSION" python3 -c " import json, os, sys version = os.environ['VERSION'] - floor = f'>={version}' + from packaging.requirements import InvalidRequirement, Requirement + from packaging.utils import canonicalize_name + from packaging.version import Version + target = Version(version) + def requires_release(value): + try: + requirement = Requirement(value) + except InvalidRequirement: + return False + return ( + canonicalize_name(requirement.name) == 'remove-ai-watermarks' + and requirement.url is None + and requirement.marker is None + and target in requirement.specifier + and any(s.operator == '>=' and Version(s.version) == target for s in requirement.specifier) + ) matches = [ v for v in json.load(sys.stdin) - if any(floor in d for d in (v.get('dependencies') or [])) + if any(requires_release(d) for d in (v.get('dependencies') or [])) ] if not matches: sys.exit(f'::error::No ComfyUI node version requires {version}; the sync never published one') diff --git a/.gitignore b/.gitignore index 9132433..9ce5b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ tmp/ # Environment secrets .env +.mcp.json # OS files .DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index 8fa9b2f..9e795b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ Optional features and installation groups are documented in [`docs/installation. `maintain.sh` runs dependency freshness and security checks, Ruff, Pyright scoped to `src/`, and the parallel test suite. Full-project Pyright is not the project gate because the ML dependency graph can exhaust Node memory. -The gate does not pass `--ignore-unfixed` to uv-secure, so a transitive CVE with no released fix stops it before Ruff, Pyright and the tests run, which a sibling repository that ignores unfixed findings would not do. The global rule applies: run and report those three separately. Current triaged case: `Known security-gate blocks` in [`docs/development.md`](docs/development.md). +The gate does not pass `--ignore-unfixed` to uv-secure, so a transitive CVE with no released fix stops it before Ruff, Pyright and the tests run, which a sibling repository that ignores unfixed findings would not do. The global rule applies: run and report those three separately. Scanner failures are fatal too. Triaged blockers and the recheck procedure: `Known security-gate blocks` in [`docs/development.md`](docs/development.md). Command, gate, typing, and model-test invariants auto-load from [`.claude/rules/development.md`](.claude/rules/development.md). Environment recovery, CI behavior, and fixture policy live in [`docs/development.md`](docs/development.md). @@ -47,7 +47,7 @@ Research and current constraints are routed through [`docs/index.md`](docs/index ## Data safety -Follow [`data/README.md`](data/README.md) for public fixture, calibration, oracle, and evaluation layout. Local-only image pulls and research data never enter this repository: `data/spaces/` and `data/research/` are gitignored, and nothing under either may be committed, quoted, or turned into a fixture. Store each tracked binary once and keep generated evaluation outputs outside the repository. +Follow [`data/README.md`](data/README.md) for public fixture, calibration, oracle, and evaluation layout. Use only publication-cleared inputs in tracked data paths. Store each tracked binary once and keep generated evaluation outputs outside the repository. ## Rules and conventions diff --git a/README.md b/README.md index 0f1fe5a..4bdb3d2 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ ControlNet, followed by SAM-masked Z-Image repair of any detected face. The alternative, `sdxl-zimage`, swaps the global stage for SDXL and keeps the same face stage. A third profile, `chroma-zimage`, uses the Apache-2.0 Chroma1 global pass with its own flat vendor floors; see `docs/chroma1-engine-research.md` for the -calibration. `--pipeline auto` picks chroma-zimage for OpenAI and Microsoft +calibration. `--pipeline auto` picks chroma-zimage for Microsoft provenance and qwen-zimage otherwise. All are CUDA only. ```bash @@ -486,8 +486,8 @@ uv run --extra video --extra diffusion python scripts/video_synthid_sweep.py inp ``` To score what an output actually cost, use `scripts/video_fidelity_probe.py`: -the engine's own PSNR is measured before the resize and the encode, so only the -probe sees the delivered picture. +the engine's own PSNR is measured after the input resize and before final +encoding, so only the probe sees the delivered picture. ```bash uv run --extra video python scripts/video_fidelity_probe.py input.mp4 input_clean.mp4 diff --git a/data/README.md b/data/README.md index d3b0dae..82c4c37 100644 --- a/data/README.md +++ b/data/README.md @@ -33,14 +33,8 @@ duplicating it and record unsupported gaps and source-specific licensing. ## Storage rules -Large local-only research data (the 211 GB private research corpus and the frozen -checkpoints, research corpora, and campaign reports) lives under gitignored -paths documented in `data/research/README.md` (local-only, like the tree it -describes). `data/research/` and `data/spaces/` are ignored by the tracked -`.gitignore`, so a fresh clone carries the rule rather than relying on a local -`.git/info/exclude`; `test_local_only_data_stays_ignored` pins both entries. -Worktree `.local-eval/` directories are scratch copies; the durable store is -`data/research/` in the main checkout. +Use only publication-cleared inputs in tracked data paths. Keep generated +evaluation outputs outside the repository. 1. Store each binary image once. Evaluation manifests and documentation point to its canonical location. diff --git a/data/evaluations/engine-selection/README.md b/data/evaluations/engine-selection/README.md index 909c948..25d513d 100644 --- a/data/evaluations/engine-selection/README.md +++ b/data/evaluations/engine-selection/README.md @@ -48,11 +48,12 @@ strengths. The tracked oracle/carrier inputs remain in their canonical homes: - Meta Content Seal: `data/contentseal/originals/` with `data/contentseal/manifest.csv`. -There is no tracked, publication-cleared Microsoft InvisMark carrier yet. The -three Paint sources used by the earlier calibration are not publication-cleared -and remain outside git. A Microsoft row may be added only from a newly generated or -otherwise publication-cleared source, with a pixel-identical metadata-stripped -control that remains positive in Microsoft's detector. +A publication-cleared Microsoft Paint InvisMark carrier is tracked at +`data/fixtures/provenance/microsoft-paint-invismark.png`; its source and MIT +license are recorded in `data/image-watermark-examples.csv` and `data/licenses/`. +It does not by itself certify an engine-selection row. That requires a fresh +paired oracle check, including a pixel-identical metadata-stripped control +that remains positive in Microsoft's detector. ## Integrity check diff --git a/docs/agent-skill.md b/docs/agent-skill.md index 1667fec..f507704 100644 --- a/docs/agent-skill.md +++ b/docs/agent-skill.md @@ -113,7 +113,12 @@ Write for every harness and a weaker model, not only Claude Opus: pixel stack reaches. Reporting a found binary as ready is what sent an agent into a missing-cv2 crash on the Homebrew build, which carries no extras. It also compares the installed release against `MIN_CLI_VERSION`, so a stale CLI - is named instead of being reported as a broken flag; + is named instead of being reported as a broken flag. Video capability is + checked separately in the installed CLI's Python environment: + `video_stacks.pixels` reports `ok`, `missing`, or `unknown`, and + `video_stacks.invisible` reports `installed`, `missing`, or `unknown`. + These checks load no weights. An unrecognized launcher leaves video + guidance unverified rather than inferring readiness from image support; - uv, pipx, and pip as installer fallbacks; - forward slashes only; - no `allowed-tools` (that field is experimental and host-specific). diff --git a/docs/ai-generated-image-classifiers.md b/docs/ai-generated-image-classifiers.md index c449852..2225fff 100644 --- a/docs/ai-generated-image-classifiers.md +++ b/docs/ai-generated-image-classifiers.md @@ -723,8 +723,8 @@ must change, not the example selection or the veto geometry. ### Wild vendor-flagged AI, 2026-08-27 -A free wild-AI positive cell came from the stock providers behind the private -veedma-blog image pipeline. Pixabay exposes a vendor-declared +A wild-AI positive cell used publicly available stock images. Pixabay exposes +a vendor-declared `isAiGenerated` flag and the query `ai generated` surfaces that pool (3,287 results); 284 content-hash-unique rows were harvested (300 minus 16 lost to a duplicate-download bug, since fixed, manifest repaired to on-disk truth). The @@ -753,10 +753,11 @@ homogeneous, but the sample was not. A stratified reharvest capped at 15 rows per contributor over eight AI-marker queries produced 300 unique images from 180 contributors (top-10 share 33%) with the same sieves. On the balanced cell the frozen numbers are: Model 1 recall 209/300 (69.7%, median score 0.482, -lowest quartile below 0.144), direct quarter-hard 189/300 repairing 22 Model 1 -misses and losing 2 (the one place the quarter representation still helps), -conditional routing 210/300 adding one repair and nothing else, provider -cascade margin 252/300 `no_ai` with 1 `openai` call, and 0/300 metadata +lowest quartile below 0.144). The direct quarter-hard comparison is withdrawn: +its recorded candidate total and paired gains/losses are inconsistent, and the +source rows must be recovered before choosing a correction. The separately +recorded conditional routing result was 210/300, adding one repair and +nothing else; provider cascade margin 252/300 `no_ai` with 1 `openai` call, and 0/300 metadata signals. The wild-AI recall ceiling under Model 1 therefore settles near 65-70% rather than 93%, robust to contributor stratification; unknown-renderer AI re-encoded by stock pipelines is the largest measured positive-side gap. @@ -1478,7 +1479,7 @@ After the public UI swap: | Provider | Meta hold-out v2 | **85.7%** (66/77 listed 79) | | Provider | Meta hold-out v3 | **89.4%** (177/198) | | Provider | Meta hold-out pooled | **88.4%** (243/275 listed 277) | -| Provider | meme_template ungated | 29.1% leak (86 of 97) | +| Provider | meme_template ungated | Withdrawn: recorded percentage and fraction disagree; source row must be recovered | A second Meta hold-out (`photo_ai_meta_v3`, 198 of 200 requested; two near-duplicates rejected) was generated the same day from 50 new diff --git a/docs/benchmark-kernel.md b/docs/benchmark-kernel.md index f3b3cd7..61aefb8 100644 --- a/docs/benchmark-kernel.md +++ b/docs/benchmark-kernel.md @@ -43,7 +43,7 @@ The fixed fields are: | `media_type` | `image`, `audio`, or `video` in schema v1. | | `adapter` | `dwt-dct` or `trustmark` for `image`; `audioseal` for `audio`; `videoseal` for `video`. The loader rejects a known adapter named against the wrong media type. | | `arm` | `positive`, `matched_negative`, `wrong_key`, or `hard_negative`. | -| `state` | `clean`, `marked`, `attacked`, `removed`, or `forged`. `forged` names an artifact carrying a watermark with a message different from the adapter oracle's fixed one; the matched verifier's correct answer there is `not_detected` with the forensic detail carried by the study layer, not the verdict. | +| `state` | `clean`, `marked`, `attacked`, `removed`, or `forged`. `forged` names an artifact carrying a watermark with a message different from the adapter oracle's fixed one. Expected detection depends on the adapter: VideoSeal checks the fixed message, while AudioSeal's presence rule can still report `detected` for a foreign message. The study records message accuracy separately. | | `path`, `sha256` | Artifact path and pinned content digest. | | `reference_path`, `reference_sha256` | Both strings or both `null`; the fidelity reference. | | `source_revision` | Corpus, generator, or acquisition revision. | @@ -385,7 +385,8 @@ claim. The controlled resource profile below remains the performance evidence. frames in [0, 1] as (T, H, W, 3) through the system `ffmpeg`, staying outside the detector interval like the image and audio decodes. Video fidelity compares decoded frames against the explicit reference clip: mean PSNR over -frames, a changed-frame fraction, `unbounded_identical` for bit-identical +frames (`mean_psnr_db`, reported separately from image `psnr_db`), a +changed-frame fraction, `unbounded_identical` for bit-identical pixel sequences, and an explicit `shape_mismatch` state when frame counts or geometry differ - which is why geometry-changing attack arms carry no reference instead of a fabricated comparison. @@ -511,8 +512,11 @@ adapters as separate evidence. A repeated `case_id` whose artifact, transform, or other case identity changes is rejected rather than merged. Input file digests in the final report bind every aggregate to its exact case-level source. The runner decodes an artifact used by multiple cases only once per process and -passes the decoded pixels to both local adapters; the adapter timing therefore -does not include file decoding. +passes the decoded pixels to both local adapters. Synthetic recipes record +the seed actually passed to generation; a manifest seed is not a substitute +for that generator input. Video observations must decode the saved artifact +whose digest the row records, including controls and double embeds. Adapter +timing does not include file decoding. ## Profile process cost diff --git a/docs/chroma1-engine-research.md b/docs/chroma1-engine-research.md index 7a00364..788a7e1 100644 --- a/docs/chroma1-engine-research.md +++ b/docs/chroma1-engine-research.md @@ -1,5 +1,10 @@ # Chroma1 engine research (2026-08-29/30) +> Metric correction: the OCR metric from `scripts/fidelity_metrics.py` is +> normalized edit distance (NED), edit distance divided by the longer +> normalized string. The historical values are unchanged; the earlier CER +> label incorrectly implied normalization by reference length. + > Research archive. This page records experiments and decisions from the dates > above. Current package behavior is defined by the > [supported signals](supported-signals.md), [known limitations](known-limitations.md), @@ -18,9 +23,11 @@ before any integration decision. All Chroma1 runs: `lodestones/Chroma1-HD` bf16 on Modal H100, seed 0, the neutral scrub prompt (`high quality, sharp, detailed, faithful to the original` / `blurry, lowres, distorted text, garbled text, artifacts`), guidance 5.0, and -**step compensation**: Diffusers truncates the step COUNT (`int(steps * -strength)`), so the requested count is scaled to always spend 4 effective -denoising steps, the same semantics as `sdxl_zimage_pipeline.requested_steps`. +**step compensation**: the calibrated request is `ceil(4 / strength)`. +Chroma rounds the start index rather than the active step count: this runs +five effective steps at strengths 0.09 and 0.17, and four at 0.20, 0.125, +and 0.40. The nominal count is four, not a promise of four effective steps. +The calibrated requests remain unchanged. Without it, strength 0.05 runs a single step and the floor reads artificially high (the first, uncompensated sweep put the OpenAI boundary at (0.05, 0.10]; compensated it is (0.05, 0.06]). @@ -125,13 +132,13 @@ H100 (production path, both stacks resident, prompt cache warm). OpenAI typography (chroma 0.06 vs qwen 0.07675): -| | CER | img LPIPS | SSIM | PSNR | +| | NED | img LPIPS | SSIM | PSNR | |---|---|---|---|---| | qwen-zimage | 0.241 | 0.083 | 0.834 | 27.7 | | chroma1 | **0.138** | **0.049** | **0.881** | **31.2** | OpenAI full-pipeline fixture (chroma 0.09 vs qwen 0.07675): chroma wins every -metric (CER 0.155 vs 0.259, LPIPS 0.056 vs 0.094, PSNR 29.6 vs 26.1). +metric (NED 0.155 vs 0.259, LPIPS 0.056 vs 0.094, PSNR 29.6 vs 26.1). OpenAI 9-face grid (chroma 0.075 global-only vs qwen 0.07675 with its Z-Image face stage): @@ -149,7 +156,7 @@ measurement, not a given. Meta at the respective floors (chroma 0.17 vs qwen 0.1): qwen wins perceptual fidelity on textured content (LPIPS: fox 0.163 vs 0.211, lighthouse 0.173 vs 0.248, night city 0.187 vs 0.277), chroma wins the text poster decisively -(LPIPS 0.008 vs 0.012, CER 0.000 vs 0.021) and the studio mug is a split. The +(LPIPS 0.008 vs 0.012, NED 0.000 vs 0.021) and the studio mug is a split. The extra 0.07 of strength costs real fidelity on photoreal content. Google at the respective floors (chroma 0.40 vs qwen 0.27, qwen row includes @@ -157,20 +164,20 @@ its Z-Image face stage): | fixture | qwen LPIPS/SSIM/ID cos | chroma LPIPS/SSIM/ID cos | |---|---|---| -| 633uuy | **0.405 / 0.577** / - | 0.499 / 0.576 / - (CER 1.000 vs 0.333) | -| akdbei | **0.440 / 0.552** / - | 0.540 / 0.557 / - (CER 0.796 vs 0.367) | +| 633uuy | **0.405 / 0.577** / - | 0.499 / 0.576 / - (NED 1.000 vs 0.333) | +| akdbei | **0.440 / 0.552** / - | 0.540 / 0.557 / - (NED 0.796 vs 0.367) | | y48j3c (18 faces) | **0.436 / 0.608 / 0.801** | 0.560 / 0.548 / 0.279 | | 3mc4t9 (5 faces) | **0.422 / 0.529 / 0.921** | 0.562 / 0.461 / 0.143 | qwen wins every axis that matters on every fixture; at 0.40 the Chroma global regeneration itself collapses face identity (0.14-0.28 cosine) and destroys -dense text (CER up to 1.0). The extra 0.13 of strength is simply too much +dense text (NED up to 1.0). The extra 0.13 of strength is simply too much regeneration. This is the Meta pattern amplified. Microsoft at the respective floors (chroma 0.125 vs qwen 0.15): chroma wins EVERY metric on EVERY source while scrubbing at lower strength -- -| source | CER q/ch | img LPIPS q/ch | SSIM q/ch | PSNR q/ch | +| source | NED q/ch | img LPIPS q/ch | SSIM q/ch | PSNR q/ch | |---|---|---|---|---| | paint-1 | 0.741 / 0.241 | 0.164 / 0.106 | 0.742 / 0.847 | 24.8 / 29.3 | | paint-2 | 0.826 / 0.391 | 0.253 / 0.108 | 0.536 / 0.633 | 22.5 / 25.5 | @@ -202,7 +209,7 @@ rungs, all oracle-verified above), the picture reverses: Google (chroma@first-clean vs qwen@0.27): -| fixture | chroma rung | LPIPS q/ch | SSIM q/ch | PSNR q/ch | CER q/ch | ID q/ch | +| fixture | chroma rung | LPIPS q/ch | SSIM q/ch | PSNR q/ch | NED q/ch | ID q/ch | |---|---|---|---|---|---|---| | 633uuy | 0.25 | 0.405/**0.375** | 0.577/**0.673** | 18.8/**22.3** | **0.333**/0.667 | - | | akdbei | 0.25 | 0.440/**0.409** | 0.552/**0.650** | 18.6/**22.5** | 0.367/**0.163** | - | @@ -211,7 +218,7 @@ Google (chroma@first-clean vs qwen@0.27): Meta (chroma@first-clean vs qwen@0.1): -| fixture | chroma rung | LPIPS q/ch | SSIM q/ch | PSNR q/ch | CER q/ch | +| fixture | chroma rung | LPIPS q/ch | SSIM q/ch | PSNR q/ch | NED q/ch | |---|---|---|---|---|---| | lighthouse | 0.10 (equal) | 0.173/**0.097** | 0.783/**0.886** | 25.6/**30.8** | - | | fox | 0.08 | 0.163/**0.083** | 0.778/**0.907** | 25.6/**31.4** | - | @@ -370,7 +377,7 @@ committed five), chosen to span class and `flat_ratio`: Harness: `chroma_muse_expansion.py` (kept outside this repository). Outputs under `out/cohort-calibration/meta-api/` (gitignored). Chroma1 ladders (seven -rungs, seed 0, four effective steps) ran for all six. Oracle +rungs, seed 0, nominal four-step compensated requests) ran for all six. Oracle 2026-08-31, anonymous `playwright-isolated` against `meta.ai/identification`. All six controls DETECTED (valid carriers). diff --git a/docs/code-and-documentation-audit.md b/docs/code-and-documentation-audit.md new file mode 100644 index 0000000..a7891e9 --- /dev/null +++ b/docs/code-and-documentation-audit.md @@ -0,0 +1,226 @@ +# Code and documentation audit + +Review snapshot: 2026-09-08, working tree based on `f127ecb`, including the +uncommitted maintenance changes. The finding tables describe that initial +snapshot. The approved repairs are recorded below; no commit or release is +implied by their working-tree status. + +## Repair status (2026-09-08) + +All 32 package, research, and tooling findings (C01-C13, R01-R15, T01-T04) +have working-tree repairs. All 19 documentation discrepancies (D01-D19) +have corrections, with contradictory historical numbers withdrawn where the +underlying evidence could not establish a replacement. Existing maintenance +changes were preserved; the lockfile was unchanged during these repairs. + +| Area | Repair and evidence | +| --- | --- | +| Image and metadata output | Current batch outcomes control publication; default invisible output survives temporary-file cleanup; ICC, orientation, and alpha travel through the real writer seams. ffmpeg maps all streams and stages in-place output. Synthetic MKV tests compare both audio packet hashes. Remove-all drops EXIF, and collection failures remain error/partial records. | +| Detection and calibrated profiles | Doubao receives vendor-specific structural confirmation; Baidu scores the actual Qwen rival. All 42 public image verdict records match the pre-fix baseline. A 15-frame Sora removal has identical output SHA-256 before and after. Chroma retains its calibrated requests; tests call the installed upstream timestep method and document four or five effective steps. | +| Research measurements | Period selection excludes confirmation; exported fallback identity is retained; old incompatible score files require rescoring. Geometry mismatch fails explicitly. Negative evidence, process outcomes, per-response persistence, executed seeds, and content deduplication have regression guards. The historical OCR formula is named NED. | +| Corrected VideoSeal evidence | Fresh local runs measured 32 temporal artifacts and 12 forgery artifacts. All 44 recorded hashes match the saved files. Results and pins are recorded in the [temporal study](videoseal-temporal-evaluation.md#corrected-artifact-measurements-2026-09-08) and [forgery study](watermark-forgery-study.md#corrected-videoseal-measurements-2026-09-08). The existing checkpoint was used without downloads or provider calls. | +| Release and skill tooling | The probe checks the installed CLI's video runtime; ClawHub uses its local executable; automatic release verification follows the triggering distribution run; ComfyUI dependencies are parsed as requirements. Skill metadata is consistently 1.0.5. No workflow was dispatched or skill published. | +| Final cleanup | Four independent simplify angles found shared crash-marker reuse, unnecessary in-place passthrough encoding, needless alpha copying, and nested probe conditionals. Those were simplified. The full suite also exposed a pre-existing environment/reload teardown leak; the test now restores the cache environment before reloading, with a mutation-checked regression. | + +Repair agents wrote only disposable source archives. The coordinator inspected +and integrated their exact file lists, reproduced the failures, checked the +mutation guards, and ran the combined working tree. + +The final pre-commit run also exposed a configuration-test isolation gap: +inherited Azure settings overrode the test's dotenv fixture. The test now +clears every fixture variable and seeds conflicting inherited values to verify +that isolation without making external calls. + +| Post-repair check | Result | +| --- | --- | +| `bash maintain.sh` | Exit 2 at the existing Accelerate/Lightning security findings; versions retained by explicit decision | +| C2PA soft-binding snapshot | Current | +| Ruff check and format | Passed, 278 Python files formatted | +| `pyright src/` | Passed, no errors or warnings | +| `pytest -n auto` | 2,358 passed, 3 skipped, 5 upstream warnings; exit 0 | +| Relative Markdown file links | Checked for file existence, no missing targets; heading anchors are not included in this check | + +Remaining evidence limits are explicit: historical multi-period affine +calibration and external-negative labels need fresh evaluation before reuse; +the inconsistent D07/D19 source numbers remain withdrawn. Image GPU inference, +provider-oracle certification, and remote release execution were not part of +this repair verification. The canonical regression rules are in +[development invariants](../.claude/rules/development.md#audit-regression-contracts). + +## Initial review scope and method + +The review covered the package, maintainer scripts, published skill probe, +configuration, workflows, tests, and tracked Markdown documentation. Seven +read-only review agents divided the package, research scripts, current +documentation, and historical archive. The coordinating review independently +checked the findings against source and exercised the important failure paths +with synthetic inputs or controlled substitutes for model and service calls. + +| Baseline inventory | Coverage | +| --- | --- | +| 61 package Python files, 25,112 lines | Read in full across three subsystem reviews and the coordinator | +| 96 maintainer Python scripts, 28,151 lines | Read in full across two script reviews | +| Published skill probe, maintenance shell, local typing stub, project configuration and workflows | Read by the coordinator | +| 60 tracked Markdown files, 24,948 lines | 59 read in full by the documentation reviewer; the 6,416-line historical archive read in full by a separate reviewer | +| 111 tracked test modules plus the existing untracked maintenance regression module | Complete test execution; source review depth varies as described below | + +Package and script source files were read in full. Test coverage combined a +complete suite run with source review; some large test modules were reviewed +by their assertions and relevant fixtures rather than every fixture body. +Documentation review distinguishes current contracts from dated historical +observations. It does not revalidate every external paper, provider statement, +historical oracle response, or inaccessible experiment artifact. + +No production code, dependency, model, external account, or release was changed +by this review. Existing maintenance changes were preserved. This document and +its link from the verification plan are the review's repository changes. + +## Initial review verification results + +| Check | Result | +| --- | --- | +| `uv run --no-sync ruff check` | Passed | +| `uv run --no-sync ruff format --check` | Passed, 271 files | +| `uv run --no-sync pyright src/` | Passed, no errors or warnings | +| `uv run --no-sync pytest -n auto` | 2,208 passed, 3 skipped, 5 upstream warnings | +| `uvx uv-secure uv.lock` | Exit 2, existing Accelerate and Lightning findings | +| Relative Markdown file targets at the start of review | 402 checked across 60 documents, none missing | + +The vulnerability scanner's three advisory entries include two entries for +the same Lightning vulnerability. The decision to retain both dependencies +is unchanged; see [Known security-gate blocks](development.md#known-security-gate-blocks). +The complete maintenance script was not rerun because this review uses checks +that do not synchronize dependencies or rewrite source. + +The green suite does not cover the failure cases below. GPU inference, +provider uploads, production deployments, and complete historical experiments +were not run. Model-dependent reproductions test the surrounding real code +with the model operation substituted, not the quality of generated pixels. + +## Package findings + +P1 means a failure to address before relying on the affected data/output +contract. P2 means a concrete correctness or contract problem to repair in +the corresponding subsystem. Line numbers refer to the review snapshot. + +| ID | Priority | Location | Finding and evidence | +| --- | --- | --- | --- | +| C01 | P1 | [api.py](../src/remove_ai_watermarks/api.py), line 779 | An invisible-only batch leaves an existing output untouched when the current outcome is `no-signal` or `unavailable`. A synthetic red source and old blue output produced `processed=1`, `failed=0`, with the old blue output. The copy decision must follow the current outcome, not file existence. | +| C02 | P1 | [invisible_engine.py](../src/remove_ai_watermarks/invisible_engine.py), line 335 | With `output_path=None`, the input path has already been replaced by a temporary PNG. The inner remover writes there; `finally` deletes the result. A real outer-method reproduction returned a nonexistent path and left the original present. Resolve the destination before replacing the input. | +| C03 | P1 | [metadata.py](../src/remove_ai_watermarks/metadata.py), line 1253 | The ffmpeg metadata stripper omits explicit stream mapping. A real synthetic MKV containing two audio streams became a file with one stream. `-c copy` alone does not preserve all streams. Test media inventory and payload preservation, not only tag removal. | +| C04 | P1 | [metadata.py](../src/remove_ai_watermarks/metadata.py), line 1768 | JPEG output re-emits EXIF even with `keep_standard=False`. Real synthetic Artist, Orientation, and GPS tags survived `remove_ai_metadata(..., keep_standard=False)`. This contradicts `metadata --remove-all`. The test currently checks which branch runs, not which tags remain. | +| C05 | P2 | [metadata.py](../src/remove_ai_watermarks/metadata.py), line 409 | Compressed metadata is decoded only when the scan buffer reaches its size limit. A 264-byte PNG containing compressed AI XMP yielded no file-based signal, while the portable record recognized it. A complete compressed byte buffer still needs decompression. | +| C06 | P2 | [metadata_record.py](../src/remove_ai_watermarks/metadata_record.py), line 379 | Collection completeness is decided by `stat`, while subsequent read errors are suppressed. Calling the real collector on an existing directory returned `status=complete`, no issues, and empty metadata. Preserve read failure separately from an unknown provenance verdict. | +| C07 | P2 | [metadata.py](../src/remove_ai_watermarks/metadata.py), line 1258 | The default in-place call passes the same input and output to ffmpeg. A real MKV reproduction failed with ffmpeg's same-input/output error. Stage the remux and publish it after success. | +| C08 | P2 | [api.py](../src/remove_ai_watermarks/api.py), lines 498, 519, 757, 783 | `all`, visible batch, and invisible passthrough omit `display_tags_from` when writing. A real no-signal `remove_all` run lost the input ICC profile. The unchanged-orientation decode also requires correct orientation handling. Cover each public writer seam; the existing low-level writer test supplies the missing argument itself. | +| C09 | P2 | [chroma_zimage_pipeline.py](../src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py), line 153 | Chroma's installed `get_timesteps` rounds the interval start, unlike the SDXL formula used by `requested_steps`. At strength 0.17, 24 requested steps produce five effective steps, not the claimed four. The test checks the wrong formula for this pipeline. Recover the calibrated schedule before changing behavior. | +| C10 | P2 | [invisible_engine.py](../src/remove_ai_watermarks/invisible_engine.py), line 315 | `tile=True` still applies `max_resolution`, contradicting the parameter's documented bypass. A synthetic 2048x1024 input reached the substituted remover as 512x256 with `tile=True, max_resolution=512`, too small to engage default tiling. | +| C11 | P2 | [video.py](../src/remove_ai_watermarks/video.py), line 289 | The provenance predicate table omits Doubao. Its stable weak-confidence branch therefore never receives TC260 confirmation. A controlled real-arbiter comparison accepted 12 matching frames with confirmation and none through the public planning seam. Preserve vendor specificity when repairing the mapping. | +| C12 | P2 | [_text_mark_engine.py](../src/remove_ai_watermarks/_text_mark_engine.py), line 250 | Baidu declares `qwen_alpha.png` as a rival, but the mapping lacks that asset. The fallback returns Baidu's entire configuration, so the runtime compares against Baidu instead of Qwen. The actual resolver returned `baidu_alpha.png` for this rival. Aggregate detector impact has not been measured. | +| C13 | P2 | [text_draft.py](../src/remove_ai_watermarks/text_draft.py), line 141 | CJK recognition ignores `vertical_pad_ratio`; all three crop-jitter passes inspect the same crop. Consequently `accepted` does not establish the claimed crop stability for CJK. The corresponding test changes responses by call count rather than by the crop supplied. | + +C09 predates the helper deduplication in the uncommitted maintenance changes: +the previous Chroma helper used the same compensation formula. Sharing it did +not establish that the two upstream pipelines have identical timestep semantics. +Do not repair C09, C11, or C12 by changing a constant or mapping without the +project's detector/output baseline checks. + +## Research and evaluation findings + +These findings concern development tools and the evidence they produce. +They are not claims that the installed package contains a SynthID pixel detector. + +| ID | Priority | Location | Finding and evidence | +| --- | --- | --- | --- | +| R01 | P1 | [watermark_benchmark.py](../scripts/watermark_benchmark.py), line 528 | `_decode_video` reads ffprobe's width,height output into height,width and reshapes raw pixels accordingly. A real 16x8 clip decoded as `(1,16,8,3)` instead of `(1,8,16,3)`. This scrambles rectangular media, including the real-carrier crop used by temporal studies. Uniform-color fixtures can miss the error. | +| R02 | P1 | [smoke_matrix.py](../scripts/smoke_matrix.py), line 91, and [robustness_suite.py](../scripts/robustness_suite.py), line 93 | `expect_exit=None` accepts every process return code. The real smoke runner classified a substituted SIGSEGV return code, -11, as `pass`, despite call-site comments requiring crashes to fail. The robustness checker likewise accepts a negative code without a textual crash marker. Use an explicit acceptable outcome set and check the command reached the intended behavior. | +| R03 | P2 | [synthid_runtime_expert_scores.py](../scripts/synthid_runtime_expert_scores.py), line 77 | Registered fallback observations are always named registered-v3. A fine-opponent detector result was exported under the wrong expert name, losing its threshold and calibration identity. Preserve the returned detector and account for every supported expert in consumers. | +| R04 | P2 | [synthid_affine_lattice_probe.py](../scripts/synthid_affine_lattice_probe.py), line 1005 | Confirmation values participate in the `argmax` that selects the period. Holding selection values fixed and changing only confirmation changed the selected period from 17 to 16. The selected result is therefore not independently confirmed by that group. Use an untouched final group or revise and recalibrate the methodology. | +| R05 | P2 | [synthid_periodic_tile_probe.py](../scripts/synthid_periodic_tile_probe.py), line 114 | `score --register` carries NumPy integer shifts into `asdict` and JSON. A real numeric-score reproduction raised `TypeError: Object of type int64 is not JSON serializable`. | +| R06 | P2 | [synthid_periodic_tile_probe.py](../scripts/synthid_periodic_tile_probe.py), line 175 | Discovery supports partial edge tiles, but loading requires exact divisibility. Three synthetic 65x65 PNGs with 8x8 tiles produced a saved model that the same module rejected on load. Producer and consumer need the same geometry contract. | +| R07 | P2 | [synthid_pixel_probe.py](../scripts/synthid_pixel_probe.py), line 132 | Positive and cleaned geometry are validated separately. Incompatible lengths become NCC zero and then `carrier attenuated`. A controlled real-command reproduction with 64x64 positives and 32x32 cleaned inputs produced this false success. Report incomparable inputs instead. | +| R08 | P2 | [synthid_research_manifest.py](../scripts/synthid_research_manifest.py), line 200 | An external training negative without evidence fails as `source-evidence` but passes after changing only `verified_via` to `none` or a nonmatching provider. The real auditor accepted empty evidence and session fields in both cases. Require independent label evidence before admission to a final split. | +| R09 | P2 | [provider_oracle_web.py](../scripts/provider_oracle_web.py), line 301 | Results are persisted only after the complete browser batch returns. A later browser exception loses the already-settled earlier results from the manifest and leaves those rows pending. Persist each response immediately with its own timestamp; do not repeat already-completed uploads. Verified by control flow, without sending media. | +| R10 | P2 | [visible_positives.py](../scripts/visible_positives.py), line 62, and [pill_gate_audit.py](../scripts/pill_gate_audit.py) | A pool timeout exits through executor shutdown, which waits for unfinished workers before reaching fallback. The serial fallback also runs potentially crashing native code inside the parent process; catching `BaseException` cannot isolate a native process crash. The advertised timeout and crash containment do not hold. No actual process crash was induced. | +| R11 | P2 | [watermark_benchmark_report.py](../scripts/watermark_benchmark_report.py), line 426 | Video fidelity emits `mean_psnr_db`, but aggregation reads only `psnr_db`. A measured video row with finite PSNR 40 produced `finite_psnr=0` and no percentile. Preserve metric semantics explicitly instead of silently dropping the observation. | +| R12 | P1 | [videoseal_temporal_study.py](../scripts/videoseal_temporal_study.py), line 212, and [watermark_forgery_study.py](../scripts/watermark_forgery_study.py), line 146 | Several baseline/forged arms measure pre-encode arrays while recording the hash of a lossy encoded MP4. Attack/removal arms instead decode the recorded artifact. These hashes do not bind the observations to the measured bytes, and comparisons mix domains. Re-read every encoded artifact before measurement or label the pre-encode measurements separately. | +| R13 | P2 | [fidelity_metrics.py](../scripts/fidelity_metrics.py), line 170 | The metric called CER is normalized by the longer string rather than reference length. The current helper reports 0.75 for reference `a`, hypothesis `aaaa`; reference-normalized character error rate would count three insertions per reference character. Rename the metric or correct its definition and re-evaluate affected reports. | +| R14 | P2 | [audio cohort](../scripts/watermark_benchmark_audio_cohort.py), line 204, and [video cohort](../scripts/watermark_benchmark_video_cohort.py), line 368 | Recorded seeds do not consistently describe the generators: audio synthesis uses a carrier-derived seed while the manifest records the shared base; the video hard-negative manifest derives its seed from a different name than the generator uses. Have the producer return the executed recipe and record that exact value. | +| R15 | P2 | [visible_recall_sample.py](../scripts/visible_recall_sample.py), line 81 | Sampling deduplicates by dimensions plus detector confidence vector rather than image content. Distinct inputs with the same detector observations collapse into one row, biasing the sample. Content equality must be established independently of the detector being evaluated. | + +The reference-length definition used in R13 is also documented by +[TorchMetrics](https://lightning.ai/docs/torchmetrics/latest/text/char_error_rate.html). + +R01 and R12 mean that affected VideoSeal research results need a fresh run +against decoded, correctly shaped, hash-bound artifacts before they support +new conclusions. R04 and R08 similarly require an evidence audit of affected +research outputs, not only a green unit test after a code edit. No historical +metric has been silently recalculated or replaced in this review. + +## Tooling and release findings + +| ID | Priority | Location | Finding and evidence | +| --- | --- | --- | --- | +| T01 | P2 | [skill probe](../skills/remove-ai-watermarks/scripts/probe.py), line 246 | The advice marks video removal ready from image pixels plus ffmpeg without checking PyAV or the video diffusion runtime. With `pixels=ok`, `ffmpeg=True`, and `invisible=missing`, the real helper reported `video_visible=ok` and `video_invisible=ok_cpu_or_gpu`. These statuses need actual capability checks. | +| T02 | P2 | [distribute.yml](../.github/workflows/distribute.yml), line 124 | ClawHub is installed locally, then invoked as a bare executable without adding its local bin directory to PATH or using npm execution. A fresh runner with a configured token cannot rely on that executable being globally present. npm documents local executables under `node_modules/.bin`; use an explicit executable path or npm's execution interface. | +| T03 | P2 | [verify-release.yml](../.github/workflows/verify-release.yml), line 44 | Automatic verification selects current PyPI latest rather than the release handled by the triggering distribution run. Concurrent releases or manual distribution of an older version can verify the wrong version. Carry a version identity from the triggering operation. | +| T04 | P2 | [verify-release.yml](../.github/workflows/verify-release.yml), line 130 | The registry requirement check uses substring membership for `>=version`, without checking the package name or parsing the requirement. The real expression accepts both a different package at the same version and a longer version sharing the prefix. Parse the distribution name and version constraint. | + +T02 was checked against the workflow and npm's primary +[executable-location documentation](https://github.com/npm/cli/blob/latest/docs/lib/content/configuring-npm/folders.md#executables). +Release workflows were not dispatched, and these findings do not assert a +particular past remote run failed. + +## Documentation corrections + +The following are current internal contradictions or disagreements with the +checked source. They are separate from the runtime fixes above. + +| ID | Page | Correction needed | +| --- | --- | --- | +| D01 | [README](../README.md), line 268 | `--pipeline auto` currently selects Chroma only for Microsoft, not OpenAI. `_ENGINE_BY_VENDOR` is the source of truth. | +| D02 | [Installation](installation.md), line 122 | The `pixels` extra does not install the research SynthID carrier detector. Remove that capability from the extras table. | +| D03 | [Published skill](../skills/remove-ai-watermarks/SKILL.md), line 185; its [commands reference](../skills/remove-ai-watermarks/references/commands.md); [module internals](module-internals.md), line 1953 | The absolute claim that Meta output has no usable provenance conflicts with the shipped standalone-IPTC attribution and automatic routing. Distinguish files that retain the tag from stripped files. Synchronize the related source comments too. | +| D04 | [Python API](python-api.md) and [known limitations](known-limitations.md) | Visible-video lists omit Doubao. [Supported signals](supported-signals.md) also places Doubao and Seedance in the opposite order from the live specificity order. Derive or verify both membership and order. | +| D05 | [Photo model card](photo-classify-hf/README.md), line 52 | When Model 1 is DEFINITELY and 124-d features are unavailable, runtime retains `label=ai, provider=None`; the card incorrectly promises `unknown`. Its own diagram and tests show the actual behavior. | +| D06 | [Photo model card](photo-classify-hf/README.md), architecture and files sections | Some lists describe five provider heads while the current manifest and other sections include ByteDance as the sixth class. Synchronize the list with the operating-point artifact and code. | +| D07 | [Photo model card](photo-classify-hf/README.md), line 197, and [classifier research](ai-generated-image-classifiers.md), line 1481 | The printed percentage disagrees with the accompanying fraction. Recover the intended source values before editing either value; no replacement was inferred. | +| D08 | [Photo training](photo-classify-training.md), final paragraph | The stated outstanding metadata prerequisite contradicts the preceding correction that the necessary metadata is already available. Reconcile the actual remaining prerequisite. | +| D09 | [VideoSeal temporal evaluation](videoseal-temporal-evaluation.md), line 47 | The claim that halving frame rate preserved the message on every carrier contradicts the table's real-Veo value below the stated threshold. R01 and R12 also require rechecking the underlying run. | +| D10 | [Module internals](module-internals.md), line 367 | Hailuo's policy accepts confirmed provenance in code; the text says it rejects it. Keep this distinct from the missing Doubao mapping in C11. | +| D11 | [Known limitations](known-limitations.md), line 499 | The claim that metadata stripping still requires a lossless 16-bit PNG path is stale: `_strip_png_metadata_lossless` is already routed for that input. Do not confuse this with pixel inpainting's separate precision limits. | +| D12 | [External benchmark licensing](external-benchmark-licensing.md), line 45 | This repository is Apache-2.0, not MIT. This is a local license-description correction, not a new legal assessment of external datasets. | +| D13 | [Benchmark kernel](benchmark-kernel.md), state table | Forged does not universally imply expected `not_detected`: the AudioSeal presence rule can detect a foreign message. Explain expectations per adapter and message-verification contract. | +| D14 | [README](../README.md), line 489 | Video engine PSNR is computed after the input resize and before final encoding. The current text says it precedes the resize. | +| D15 | [Engine-selection fixtures](../data/evaluations/engine-selection/README.md), line 50 | The absence of a cleared Microsoft fixture is no longer the right prerequisite: a licensed fixture is now tracked. A new paired oracle result is still a separate requirement. | +| D16 | [Historical SynthID archive](synthid-detector-removal-plan.md), lines 5874-5926 | The reported registered-v3 false-positive confidence bound combines the controls used to select the v3 gates with a subsequent holdout. Only the untouched evaluation set supports an independent bound. This is distinct from the code-level confirmation leakage in R04. Recompute the bound with the eligible denominator. | +| D17 | [Historical SynthID archive](synthid-detector-removal-plan.md), line 3692, and [detector research](synthid-detector-research.md), line 33 | Normalized residual correlations are interpreted as absolute carrier amplitude. The stated amplitude ratio does not follow from the reported correlations, and normalization discards the absolute scale. Retain the measured correlation statement without that unsupported inference. | +| D18 | [Historical SynthID archive](synthid-detector-removal-plan.md), line 4851 | A crop at half the width and half the height is described as half the area. It retains one quarter of the area; a later archive entry correctly describes the linear scale. | +| D19 | [Classifier research](ai-generated-image-classifiers.md), lines 755-757 | Baseline, candidate total, and paired gains/losses are arithmetically inconsistent. The candidate total must equal the baseline plus improvements minus regressions. Recover the source row before choosing which value to correct. | + +The public documentation also needs a focused removal of local operational +details in [data layout](../data/README.md), lines 35-42, and the +[historical archive](synthid-detector-removal-plan.md), lines 4305-4306, as well +as the product-specific passage in [classifier research](ai-generated-image-classifiers.md), +lines 726-727. +Those details are deliberately not repeated here. Preserve only the public +fixture policy and the technical experiment conclusions. + +Lower-priority drift remains in dated plans: some completed smoke, full-clip, +audio, and forgery checks are still listed as absent or future work. Keep the +historical chronology, but make current status pointers agree with the live +tests and newer studies. + +## Original repair order + +Start with C01-C04, R01-R02, and R12: they lose output/data, retain explicitly +removed metadata, or turn failed or mismatched validation into successful evidence. Add +regressions at the public seam using the synthetic scenarios above and +mutation-check that those tests distinguish the broken behavior. + +Then repair the remaining subsystem contracts and their documentation +together. Detector, video, and calibrated pipeline changes still require +the parity and output checks in the development rules. GPU/oracle calibration +and historical evidence repair remain distinct from ordinary unit testing. +Run the full gate after fixes, and report core checks separately while the +documented security block remains. diff --git a/docs/development.md b/docs/development.md index 37fb84c..3302f74 100644 --- a/docs/development.md +++ b/docs/development.md @@ -22,7 +22,41 @@ The optional TrustMark decoder downloads weights into its installed package dire ### Known security-gate blocks -`maintain.sh` fails while PyPI ships no fixed release for a transitive CVE. Current case (triaged 2026-08-17): `lightning` 2.6.5, pulled only by the optional `trustmark` extra, carries PYSEC-2026-3624 (RCE via `load_from_checkpoint` on an attacker-crafted checkpoint). The vulnerable API is unreachable here, because the TrustMark decoder loads only its own pinned weights downloaded from the TrustMark release, never a user-supplied checkpoint. The upstream fix is merged but unreleased, and ignores are never added, so run and report the remaining core checks (Ruff, Pyright, tests) separately until a fixed `lightning` release lands; bump it with `uv lock --upgrade-package lightning` as soon as one does. The gate prints the same advisory twice, as PYSEC-2026-3624 and as its alias GHSA-qqmf-gpg7-g8gw (CVE-2026-58659); the `2022.6.15` fix version on the second row belongs to the retired calver line and is not an upgrade from 2.6.5, so neither row is actionable. +`maintain.sh` treats both vulnerability findings and scanner failures as fatal. +A success sentence followed by a nonzero scanner exit is still a failed check; +`tests/test_maintenance.py` exercises that case with isolated command stubs. +Do not add advisory ignores or suppress the scanner's exit status. Run and +report Ruff, Pyright scoped to `src/`, and tests separately when security blocks +the gate. + +Rechecked on 2026-09-08 against PyPI release metadata and the advisory sources: + +- `lightning`, pulled by the optional `trustmark` extra, remains affected by + [PYSEC-2026-3624](https://github.com/pypa/advisory-database/blob/main/vulns/lightning/PYSEC-2026-3624.yaml) + and its alias [GHSA-qqmf-gpg7-g8gw](https://github.com/advisories/GHSA-qqmf-gpg7-g8gw). + These describe one checkpoint-loading vulnerability. The latest PyPI release + is still affected; the advisory's `2022.6.15` fix entry is not a newer release + on the supported 2.x line. The adapter selects TrustMark's supplied weights, + rather than accepting a caller-supplied Lightning checkpoint. +- `accelerate`, required by the diffusion stack, is affected by + [GHSA-4j2p-28q2-5m79](https://github.com/advisories/GHSA-4j2p-28q2-5m79). + The sharded-checkpoint loader accepts unsafe `weight_map` paths; the advisory + lists no patched release. The project has no direct call to + `load_checkpoint_in_model` or `load_checkpoint_and_dispatch`, but this does + not prove indirect model-loading paths are unaffected. + +Before carrying either block forward, rerun `uvx uv-secure uv.lock` and check +PyPI for a fixed release. Upgrade a fixed package with +`uv lock --upgrade-package ` and rerun the gate. Removing either +current dependency would remove a supported optional feature, so that is not a +maintenance-only repair. + +Minor-only lock refreshes must retain each package's current major and lower +bound. Use version-qualified `uv lock --upgrade-package` arguments, inspect a +`--dry-run` first, and compare the resulting package versions. NumPy has two +Python-dependent branches, so apply separate constraints on either side of +Python 3.13. A blanket upgrade can both cross a NumPy major and downgrade a +neighboring OCR package to satisfy the new graph. ## CI diff --git a/docs/external-benchmark-licensing.md b/docs/external-benchmark-licensing.md index cdbd4d5..e7a4bfb 100644 --- a/docs/external-benchmark-licensing.md +++ b/docs/external-benchmark-licensing.md @@ -42,7 +42,7 @@ Sources verified: the Hugging Face dataset `Shilin-LU/W-Bench` at revision card wins. - **Code license: NTUITIVE Non-Commercial** (NTU technology transfer; commercial use requires contacting the authors). The VINE code cannot be - vendored into this MIT-licensed project, and pipeline outputs derived + vendored into this Apache-2.0-licensed project, and pipeline outputs derived from running it would inherit that restriction. What remains lawful and useful: consuming the MIT dataset directly with our own pinned oracles and benchmark kernel, adopting the attack taxonomy as protocol diff --git a/docs/installation.md b/docs/installation.md index 70ff60f..eb1de5a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -119,7 +119,7 @@ application actually uses: | Extra | Capability | Automatically includes | Torch or model download | | --- | --- | --- | --- | -| `pixels` | Shared BGR runtime and calibrated-size SynthID carrier detection | NumPy, headless OpenCV | No | +| `pixels` | Shared BGR image runtime | NumPy, headless OpenCV | No | | `heif` | HEIC, HEIF, and AVIF pixel decoding | pillow-heif | No | | `visible` | Visible mark detection, OpenCV inpainting, and manual erasing | `pixels` | No | | `video` | Visible video identification/removal and timestamp preservation | `visible`, PyAV | No | @@ -157,9 +157,10 @@ flowchart LR `heif`, `trustmark`, and `text-draft` are independent branches. Combine them explicitly with another feature when required. `text-draft` is excluded from `all` because it proposes unverified OCR annotations and is not a production -removal path. TrustMark requires NumPy 1.x, which has no -CPython 3.13 or 3.14 wheels, so that branch is available only on Python -3.11-3.12. The `all` bundle contains every production branch compatible with +removal path. The TrustMark branch remains available only on Python 3.11-3.12. +Its supported dependency floor includes releases requiring NumPy 1.x, which has +no CPython 3.13 or 3.14 wheels; newer TrustMark releases relaxing that requirement +do not by themselves widen this package's declared support range. The `all` bundle contains every production branch compatible with the active Python and never includes `dev`. Examples: diff --git a/docs/known-limitations.md b/docs/known-limitations.md index 8e65f58..93ed65f 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -403,7 +403,8 @@ supported AI provenance metadata without transcoding streams. `video visible` and `remove_video_visible` additionally support the moving Sora 2 mascot and wordmark, the current Veo four-point diamond, the legacy -`Veo` text, the Seedance boxed `AI` label, the fixed `Dola AI` text, the Hailuo AI +`Veo` text, the Seedance boxed `AI` label, the fixed Doubao `豆包AI生成` +label, the fixed `Dola AI` text, the Hailuo AI MINIMAX/Hailuo AI composite label, and the bottom-right Kling AI `KLING AI` or `KlingAI` label with its version suffix. Detection requires a recurring visual candidate across adjacent frames. Fixed-mark candidates must remain anchored rather than @@ -491,11 +492,13 @@ when `image_io` can still decode the raster by normalizing the container and checking again. A truly undecodable file still reports the surviving markers. The CLI uses this verified path. -### Sixteen bit PNG output is not preserved +### Sixteen bit PNG metadata stripping -The Pillow based PNG metadata rewrite uses the normal image save path and may -reduce a sixteen bit PNG to eight bits. A byte-level PNG metadata stripper -would be required to preserve that bit depth. +Sixteen bit PNG metadata stripping uses `_strip_png_metadata_lossless`, which +preserves the coded pixel chunks and bit depth. Decoder-based normalization +of malformed files can still reduce precision. Pixel editing and inpainting +have separate precision limits; lossless metadata stripping does not extend +those pixel-processing contracts. ## Detection extras diff --git a/docs/module-internals.md b/docs/module-internals.md index 19d5923..16c9cf6 100644 --- a/docs/module-internals.md +++ b/docs/module-internals.md @@ -73,6 +73,9 @@ Important contracts: - `batch` counts per-file failures and exits nonzero if any file failed or an applicable invisible stage was skipped because its dependencies were absent. +The batch CLI prints plain text and forwards verbose progress events directly. +Its library summary derives counts from the recorded per-file results and errors. + The decorators for diffusion options are shared by `invisible`, `all`, and `batch`. The runtime help generated by Click is the source of truth for option names and defaults. @@ -141,6 +144,13 @@ objects adds no detector or fill pass. The original positional constructors for `RemoveAllResult` and `BatchSummary` remain valid through defaulted appended fields. +An invisible batch no-op replaces any existing output with the current input; +it cannot count a stale file as this run's result. A successful invisible +result names the published output even when the engine used a temporary +working file. `remove_all` carries display tags through its intermediate, +and keeps the original alpha aligned if diffusion bakes EXIF orientation +into the RGB raster. + The package root exposes all of them lazily through [`__getattr__`](../src/remove_ai_watermarks/__init__.py), keeping a plain package import free of the heavier image and model imports. @@ -362,16 +372,17 @@ Every per-frame result is untrusted. Each provider's floors, minimum-run policy, fill padding and mask style are one row in `VISIBLE_MARK_POLICIES`, and every mark enters the same `stabilize_localizations` entry point; the recurrence implementation underneath knows nothing about providers. That policy row also -carries `accepts_provenance`, which forces `provenance=False` for Hailuo AI and Kling AI -— they have no metadata that could confirm them, and the guarantee used to be -structural (their wrappers took no `provenance` parameter at all). Provenance can relax a low-contrast run only +carries `accepts_provenance`, which forces `provenance=False` for Kling AI. +Hailuo AI accepts an explicit MiniMax TC260 producer. Doubao accepts its +registry-listed TC260 producer codes, including their structured USCC form; +an unrelated or absent producer does not confirm Doubao. Provenance can relax a low-contrast run only after recurring visual evidence exists. Sora transition frames follow the nearest confirmed moving position only with Sora provenance. Seedance, Doubao, Dola, Hailuo AI, and Kling AI additionally require candidates to remain anchored to the start of a run. This rejects slowly drifting scene details that still have -high frame-to-frame overlap. Hailuo AI and Kling AI do not infer provenance from -technical encoder tags; their confirmed public samples carried no provider -metadata. +high frame-to-frame overlap. Technical encoder tags do not establish provider +provenance. Hailuo AI confirmation requires its explicit producer marker, +while Kling AI remains visual-only. Removal runs in a second decode pass. Sora, legacy Veo text, Dola text, Seedance, Hailuo AI, and Kling AI use box masks. Seedance deliberately fills the @@ -517,7 +528,8 @@ Key contracts: pixel payload to reach metadata placed beyond the window: `isobmff.scan_c2pa_region`, `_png_late_metadata`, `_riff_late_metadata`. A decoder-backed fallback last, `_decoder_visible_text`, for metadata the raw bytes do not spell at all — a - zlib-compressed PNG `zTXt` packet is readable only after inflation. The layers are + zlib-compressed PNG `zTXt` packet is readable only after inflation. Compressed + text is inspected even when the whole file fits within the head window. The layers are ordered that way because the structural readers work on files no decoder can open. - A C2PA reader failure is logged at warning, not debug. It returns the same `None` as a file with no manifest, so nothing downstream can distinguish "no credentials" from @@ -538,7 +550,11 @@ Key contracts: - Native AVI and FLV TC260 entries are read from `LIST/INFO/AIGC` and `script.onMetaData.AIGC`, respectively, then removed through ffmpeg stream copying. -- Supported non-ISOBMFF audio and video containers use ffmpeg stream copying. +- Supported non-ISOBMFF audio and video containers use ffmpeg stream copying + with explicit `-map 0` to preserve every input stream. A same-directory + temporary output is published atomically, including for in-place removal. +- `keep_standard=False` removes EXIF (including GPS) and ICC profiles rather + than restoring them through the Pillow save arguments. - The low-level remover is fail-safe and can copy an undecodable file through unchanged. - A caller that reports success must use `strip_and_verify`, which scans the @@ -815,7 +831,14 @@ and lossy JPEG/WebP views remain inconclusive. The runtime precedence is registered-v3, the bounded opponent fallback, then large-v1 above 10 megapixels. Passing `register_scale=False` selects the legacy fixed diagnostic explicitly. The research bank in -`scripts/synthid_routed_expert_bank.py` keeps all three observations for audits. +`scripts/synthid_routed_expert_bank.py` preserves five explicit expert +identities for audits: fixed, registered-v3, large, opponent-registered-v1, +and opponent-fine-registered-v1. An inactive fallback is unsupported, not a +new independent score: two runtime calls populate the five slots. Schema-2 +exports retain the actual routed identity; legacy schema-1 registered +observations must be rescored, and conformal calibration must match the new +expert set. Each expert uses +its own threshold, including 1.05 for the fine fallback. Neither runtime nor research routing returns a clean-image verdict. An unchanged registered challenge from 10 to 18 megapixels retained only 1 of @@ -927,10 +950,20 @@ are in the detector research plan. The separately measured registered geometry range remains 250,000 through 10,000,000 decoded pixels with both sides at least 256 pixels. The default path -and `identify` use registered-v3, then the narrower opponent-registered-v1 +in the development-only research runtime uses registered-v3, then the narrower opponent-registered-v1 fallback in its 1-10 megapixel domain, and large-v1 above 10 megapixels. A 20-image real-corpus drift check was byte-identical after the earlier v2 integration. The calibration history and caveats are in the linked detector research plan. +The separate `scripts/synthid_affine_lattice_probe.py` now emits schema 14 +with `period_selection="selection-only-whitened-amplitude-v1"`. Period +selection cannot read confirmation patches; schema 13 and earlier multi-period +probe results need rerunning and recalibration. This does not change the +frozen runtime detector's independent implementation or thresholds. Learned +folds may include partial edge tiles when both raster dimensions support at +least one full tile. A residual-correlation comparison requires compatible +geometry; a mismatch is an error rather than measured attenuation. +External `not_detected` controls require either the matching target verifier +or an explicit independent `evidence_reference`. ### Development OpenAI SynthID oracle @@ -991,7 +1024,9 @@ defects found while establishing that equality are the reason each rule exists: The transport is independently versioned as `provenance_metadata` schema 1. Native records require the exact integer schema version and a `complete` status. Source -read failures are explicit error records and cannot be judged. WebP walks the full +or head-read failures produce `error`; later region, trailer, decoder, or C2PA +read failures retain collected evidence with `partial` and stage-specific +`issues`. Neither incomplete status can be judged as a complete scan. WebP walks the full declared RIFF container by seeking over `VP8`, `VP8L`, `ALPH`, and `ANMF`, so late XMP/C2PA remains visible without shipping coded frames or parsing appended trailer bytes as chunks. @@ -1750,7 +1785,10 @@ an unconditional quality claim: Qwen CER stayed 0.074, while Chroma moved from 0.037 to 0.074 despite better LPIPS, SSIM, PSNR, and text-box pixel error. The stage is therefore implemented and fidelity-positive on both OpenAI sources, but not assumed to improve every OCR reading. This is a fidelity check, not a provider -oracle certification; exact output hashes and per-box metrics are produced by +oracle certification. These restoration-study CER values are distinct from +the longer-string-normalized NED emitted by `scripts/fidelity_metrics.py`; +the latter's former CER label was corrected without changing its values. +Exact output hashes and per-box metrics are produced by the `chroma_text_restoration_study.py` harness (kept outside this repository) under the gitignored `out/text-restoration-engine-study/` directory. @@ -1810,8 +1848,11 @@ sdxl-zimage, asserted by the same test shape). Three things are bound to the calibration and must not drift: the NEUTRAL prompt (`high quality, sharp, detailed, faithful to the original` -- deliberately NOT the shared canny-stage prompt, which was never calibrated against Chroma1), guidance 5.0, and the -four-EFFECTIVE-step schedule with `requested_steps` compensation (the same -Diffusers truncation trap sdxl-zimage documents). There is no Canny +calibrated `ceil(4 / strength)` requested-step schedule. +`CHROMA_NOMINAL_STEPS = 4` is a nominal count: Chroma rounds the start +index, so strengths 0.09 and 0.17 execute five steps, while 0.20, 0.125, +and 0.40 execute four. Preserve those requests rather than changing the +calibration to force four effective steps. There is no Canny conditioning: the floors were measured on a plain strength pass. The flat vendor floors (`CHROMA_ZIMAGE_*_STRENGTH` in watermark_profiles): @@ -1880,7 +1921,9 @@ profile takes a vendor policy (`SDXL_ZIMAGE_OPENAI_STRENGTH` 0.15, `resolution_adaptive_denoise`. Flat values are what was measured; no size dependence has been established for this stage, so none is asserted. -`requested_steps` exists because the two runtimes truncate differently. DiffSynth sets +`requested_steps` is shared by SDXL and Chroma in `two_stage_pipeline.py`, +which also owns `_target_size` with each profile's grid passed explicitly. +The step helper exists because the two runtimes truncate differently. DiffSynth sets `sigma_start = denoising_strength` and runs every requested step across the shortened sigma range; Diffusers img2img truncates the step *count* (`init_timestep = int(steps * strength)`), so asking it for four steps at 0.15 executes @@ -1944,11 +1987,11 @@ so it is left alone. Measured 2026-08-26 against the anonymous oracle `meta.ai/identification` (verdicts read from the settled page text; corpus and full row-level provenance in -`data/contentseal/manifest.csv`). Muse Image carries no C2PA, so -`vendor_for_strength` returns None for it and these images ride the default -resolution-adaptive curve; there is deliberately no `meta` entry in the flat vendor -map, because no provenance signal can establish that cohort automatically and an -unreachable constant would be dead code. +`data/contentseal/manifest.csv`). The original measurement used the +default resolution-adaptive curve. Current routing can infer the Meta cohort +from a retained standalone AI IPTC tag, after checking C2PA vendor evidence +first. Stripped files need explicit `--vendor meta` to select the measured +Meta floor; absence of C2PA alone does not prevent automatic attribution. | source (2.56 MP generation) | detected at | clean from | |---|---|---| @@ -2063,6 +2106,8 @@ profiles were removed (`sdxl-zimage` is a later, surviving profile) it was forced to 0 on every path, so the module, the `--min-resolution` and `--upscaler` options and the `esrgan` extra were all unreachable and went with it. Only the `max_resolution` cap can move geometry now, and it only scales down. +Tiled removal bypasses that cap and processes the native image geometry; +`tile_size` controls the per-tile workload. Regression coverage: @@ -2093,11 +2138,11 @@ Contracts: get the tags spliced into the encoded bytes without a second encode; WebP is re-saved losslessly through Pillow (libwebp hides spliced chunks from readers unless a hand-built VP8X announces them); HEIF writers bake orientation into - the pixels on save. Orientation is re-tagged only when the raster still has the - source's STORED dimensions, decided against the raster's own shape because cv2 - rotates JPEG and (build-dependent) PNG decodes under `IMREAD_COLOR` but never - `IMREAD_UNCHANGED`, which is what the pixel paths read with -- so a decode that - already turned the raster upright is never tagged into a double rotation. + pixels on save. Callers that know the decode state pass + `orientation_applied=False` for raw pixels and `True` for a transposed + raster. Dimensions alone cannot distinguish mirrors or 180-degree rotation. + The omitted value retains the compatibility shape heuristic. Display tags + are captured before encoding, including when source and destination match. The metadata strip is the other half of that contract: Pillow cannot hold 16-bit colour, so `remove_ai_metadata`'s open+save path silently returned a 16-bit PNG at @@ -2116,6 +2161,16 @@ Regression coverage: - [`test_image_io.py`](../tests/test_image_io.py) - [`test_cli_robustness.py`](../tests/test_cli_robustness.py) +## Bounded image audit workers + +`scripts/_isolated_image_workers.py` supervises a fresh Python subprocess for +each image in `visible_positives.py` and `pill_gate_audit.py`. Each child has +its own timeout and is killed and reaped when that bound expires. A native +crash, timeout, or malformed worker reply remains an explicit failed row; +the parent never retries unsafe decoding in its own process. Threads only +supervise children. Recall sampling deduplicates by source content SHA-256, +not by equal detector-confidence vectors. + ## Adding or changing behavior For a new visible mark: diff --git a/docs/photo-classify-hf/README.md b/docs/photo-classify-hf/README.md index 97953fd..ef24e1f 100644 --- a/docs/photo-classify-hf/README.md +++ b/docs/photo-classify-hf/README.md @@ -49,8 +49,9 @@ metadata is gone: photograph from the rest of China's generators (Qwen, Kling, ...) publishes `provider=None` (the residual head abstains) - `label=human` on camera photographs at a low false-positive rate -- `label=unknown` when the detector is only POSSIBLY AI, or when 124-d - features cannot be extracted +- `label=unknown` when the detector is only POSSIBLY AI +- `label=ai, provider=None` when the detector is DEFINITELY AI but 124-d + features cannot be extracted (unless the receipt gate abstains) Do not use it to assert that a file is clean. Do not use it as a legal authorship test. Do not run Model 2 on every image. @@ -140,7 +141,7 @@ no mixed head can honestly name that group, so its win abstains. - Input: 124-d residual vector, or abstain if the image is smaller than 256 px or the extractor refuses - Heads: one-vs-rest focal MLP `124-64-1` per class - (`openai`, `google`, `tc260`, `meta_muse_image`, `no_ai`) + (`openai`, `google`, `bytedance`, `tc260`, `meta_muse_image`, `no_ai`) - Decision: a class wins only if it beats `no_ai` by margin 0.30, then argmax among those that passed - Public name `muse-image` is the Muse Image class. The freeze file still @@ -194,10 +195,10 @@ DEFINITELY is the shipped operating point. | Class | Muse Image hold-out v2 | 85.7% (66/77 listed 79) | | Class | Muse Image hold-out v3 | 89.4% (177/198) | | Class | Muse Image hold-out pooled | 88.4% (243/275 listed 277) | -| Class | meme templates, ungated | 29.1% leak (86 of 97) | +| Class | meme templates, ungated | Withdrawn: recorded percentage and fraction disagree; source row must be recovered | -The ungated meme leak is why Model 2 must not run on every file. Gated on -DEFINITELY, that leak is not a provider attribution. +Model 2 must remain gated on DEFINITELY. The historical ungated meme +measurement is not usable evidence until its contradictory counts are reconciled. Ridge-only Model 1 at the earlier 1% Open Images cut (before the AND with the freeze MLP) is documented in the research page: Kodak 0/24, fresh 1.7%, @@ -259,7 +260,7 @@ silently change the heads. | `clip-l-ft.pt` | HeadedCLIP state dict (CLIP-L plus unused linear head) | | `probe-weights-clip-l-ft.npz` | Ridge mean, scale, weights, `thr_oi_1pct` | | `detector.pt` | Freeze MLP | -| `provider.pt` | Focal heads keyed `openai`, `google`, `tc260`, `meta_muse_image`, `no_ai` | +| `provider.pt` | Focal heads keyed `openai`, `google`, `bytedance`, `tc260`, `meta_muse_image`, `no_ai` | | `operating-point.json` | Seeds, thresholds, margin | The image catalog is not in this repository. diff --git a/docs/photo-classify-training.md b/docs/photo-classify-training.md index 3c7c462..5425276 100644 --- a/docs/photo-classify-training.md +++ b/docs/photo-classify-training.md @@ -77,7 +77,7 @@ construction and fits the same heads from the pack above. `detector_ai_train` plus extras versus `detector_photo_train` plus struct. Threshold is the 1.67% quantile on `detector_photo_dev_oi`. 2. AND that MLP with the frozen ridge in the probe file. DEFINITELY is both. -3. Load 124-d vectors. Fit one-vs-rest focal heads for openai, google, tc260, +3. Load 124-d vectors. Fit one-vs-rest focal heads for openai, google, bytedance, tc260, meta_muse_image, no_ai. Margin 0.30. The public class for the Muse Image head is `muse-image`. `tc260` is the China AIGC label standard, mixed producers, not one company. @@ -96,7 +96,7 @@ cell; the rest of China (Qwen, Yuanbao, Kling, ...) abstains behind the tc260 veto until per-producer train mass holds its own cells (qwen measured at 64 train rows: 27%). The producer metadata is available locally (all catalog tc260 rows parse their producer from the -sha256-verified mirror; the earlier "app database" premise was wrong), +sha256-verified mirror), and the historical harvest keeps adding mass (`tc260-producer-split-2026-09-07/historical-harvest.json`). Microsoft has no pixel class and cannot have one: its products render with other @@ -109,5 +109,6 @@ the shared negative pools (google -10 or meta -6 depending on variant), so drift rows should enter together with enough mass to hold every cell flat, not as a standalone patch. Second, scope: 6 of 12 Aug rows failed before the provider stage, so a drift fix that covers the whole cell needs Model 1 -in the loop. The `ContentProducer` split still needs metadata exported from -the app database. +in the loop. The `ContentProducer` metadata is already available; the +remaining prerequisite is enough independent per-producer training and +evaluation data to establish a stable class without regressing sibling cells. diff --git a/docs/provider-oracles.md b/docs/provider-oracles.md index a22f85e..692aaf3 100644 --- a/docs/provider-oracles.md +++ b/docs/provider-oracles.md @@ -277,6 +277,9 @@ uv run python scripts/provider_oracles.py run-web \ The runner reuses one context and one route for the batch, performs a fresh page navigation before each upload, does not retry, and stores the settled page text. +Each completed verdict is persisted immediately with its own observation +timestamp. A later navigation or upload failure preserves those completed +rows and leaves unobserved rows unresolved. For a selected proxy route it enables certificate handling in that isolated Chromium launch and context; direct launches keep Playwright's normal TLS verification. A 2026-09-07 preflight sent one read-only request through each diff --git a/docs/python-api.md b/docs/python-api.md index e01e3af..b611ad0 100644 --- a/docs/python-api.md +++ b/docs/python-api.md @@ -657,11 +657,12 @@ combines synthetic multi-scale visual matching with temporal consistency, so an isolated lookalike in one frame is not enough to authorize inpainting. `mark="auto"` is the default: it evaluates all providers in one decode pass and selects the first stable match in specificity order (`sora`, `veo`, `seedance`, -`dola`, `hailuo`, `kling`). Provider confidence values are calibrated +`doubao`, `dola`, `hailuo`, `kling`). Provider confidence values are calibrated independently and are not compared across detectors. Pass one of those explicit values to restrict the scan to a single provider. The Veo detector recognizes the current four-point diamond and the -legacy `Veo` text. Seedance recognizes the boxed `AI` label, Dola recognizes +legacy `Veo` text. Seedance recognizes the boxed `AI` label, Doubao recognizes +the fixed `豆包AI生成` label, and Dola recognizes its compact text label, Hailuo AI recognizes the composite MINIMAX/Hailuo AI label, and Kling AI recognizes its bottom-right logo, wordmark, and version suffix. Each variant has an independent synthetic silhouette and calibrated temporal policy. diff --git a/docs/qwen-improvement-research.md b/docs/qwen-improvement-research.md index 9bb3c8e..0a720d7 100644 --- a/docs/qwen-improvement-research.md +++ b/docs/qwen-improvement-research.md @@ -1,5 +1,10 @@ # Qwen-Image improvement research (2026-06-20) +> Metric correction: the OCR metric from `scripts/fidelity_metrics.py` is +> normalized edit distance (NED), edit distance divided by the longer +> normalized string. The historical values are unchanged; the earlier CER +> label incorrectly implied normalization by reference length. + > Research archive. This page records experiments and decisions from the date > above. It may mention prototypes or defaults that were later changed. Use the > user guides and current source code for the supported interface. @@ -15,7 +20,7 @@ The `qwen` pipeline runs base Qwen-Image (20B MMDiT, Apache-2.0) as a low-streng img2img scrub (removal comes from the denoising `strength`). Certified oracle scrub floors: OpenAI 0.10 (seed-robust), Gemini 0.25 (pinned seed). Measured against the SDXL + canny-ControlNet pipeline (`scripts/fidelity_metrics.py`): Qwen preserves -**text** markedly better (incl. CJK and Cyrillic, lower OCR CER) but preserves +**text** markedly better (incl. CJK and Cyrillic, lower OCR NED) but preserves **faces** worse, smoothing skin (Laplacian-variance retention 0.40 vs 0.62, face LPIPS 0.17 vs 0.09, ArcFace identity 0.38 vs 0.55 at the scrub floors). The goal of the research: keep Qwen's text advantage while fixing the face-smoothing, and judge @@ -132,7 +137,7 @@ candidate; it does not upgrade that exact-output result into broad certification A `--pipeline auto` router (Haar+MSER → text→qwen / faces→controlnet / both→mixed) and a faces+text mixed dual-pass (scrub the whole frame on both, graft qwen text regions onto the controlnet base) were built and run on Modal (the abba poster: faces + display text). On that -canonical faces+text case **controlnet won EVERY metric, including text** (CER 0.114 vs qwen +canonical faces+text case **controlnet won EVERY metric, including text** (NED 0.114 vs qwen 0.379; ID 0.64 vs 0.36) — canny holds existing letter shapes, qwen re-renders display text and garbles it, so grafting qwen text only hurts. Qwen beats controlnet on text ONLY for clean body text on a plain background with no faces (openai_1/2), a niche `--pipeline qwen` alone covers; @@ -226,7 +231,7 @@ original bbox and stayed collision-immune. Regression-guarded by re-introducing SynthID? Note: residual transfer from the ORIGINAL risks copying back watermark-carrying high frequencies, so it must be verified against the SynthID oracle. - Head-to-head: does `qwen-zimage` retain its measured ArcFace gain across more portraits - and mixed scenes, match Qwen's text advantage (CJK+Cyrillic CER), and clear SynthID + and mixed scenes, match Qwen's text advantage (CJK+Cyrillic NED), and clear SynthID robustly across content types and seeds? - Can YuNet's serial face workload be bounded to foreground/relevant faces without losing the small faces the upstream YOLO path would process? diff --git a/docs/release-and-distribution.md b/docs/release-and-distribution.md index 09b67f5..a304d61 100644 --- a/docs/release-and-distribution.md +++ b/docs/release-and-distribution.md @@ -149,7 +149,9 @@ After the skill files reach the default branch: ClawHub publishes automatically: the `clawhub` job in `distribute.yml` compares the `SKILL.md` version with the published one on every GitHub Release and -submits an update when they differ. It needs the `CLAWHUB_TOKEN` secret +submits an update when they differ. After its local npm installation, the +job invokes `./node_modules/.bin/clawhub` explicitly. It needs the +`CLAWHUB_TOKEN` secret (create a dedicated token on clawhub.ai, not a device-flow one); without the secret the job skips with a notice. A submission waits for ClawHub security scans before the listing goes public, so a green job means @@ -170,12 +172,22 @@ release must never silently substitute its latest schema when a caller explicitl requests an older supported one. After publication, the `verify-release.yml` workflow runs automatically once -`distribute.yml` completes and checks every surface below. It can also be -dispatched manually with a specific version. Every check fails the job; none of +`distribute.yml` completes and checks every surface below. The automatic +run downloads `distributed-release-*` from the exact triggering distribution +run, verifies the run ID and artifact attempt, and selects the highest recorded +attempt no later than the triggering attempt. This also handles reruns of +failed jobs that reuse an earlier successful version-resolution job. A newer +PyPI release cannot silently change the recorded target. +Manual dispatch accepts an explicit version, or uses PyPI latest when omitted. +Every check fails the job; none of them warns and continues, because a surface nobody can confirm is not a verified surface, with one deliberate exception noted below. The ComfyUI check reads the registry's JSON API and asserts that some published node version declares this -library release. It deliberately does NOT proxy through the sync workflow's run +library release. Dependencies are parsed as packaging requirements: the +canonical package name must be `remove-ai-watermarks`, an exact `>=` floor +must name the target release, and the full constraint must admit it. URL +and environment-conditional requirements do not satisfy that contract. +It deliberately does NOT proxy through the sync workflow's run status: the node repository also runs that workflow on a daily schedule, so a green nightly run says nothing about whether this release was synced. diff --git a/docs/supported-signals.md b/docs/supported-signals.md index eea9cc4..4c9d32e 100644 --- a/docs/supported-signals.md +++ b/docs/supported-signals.md @@ -48,8 +48,8 @@ pill detector; the generic pill shape does not attribute LiblibAI on its own. | --- | --- | --- | --- | | `sora` | Sora 2 mascot and wordmark | Moves among frame positions | Requires a temporally recurring visual match; the older Sora Turbo corner swirl is a different unsupported mark. | | `veo` | Current four-point diamond and legacy `Veo` text | Fixed bottom-right corner | Uses separate silhouettes and requires a recurring match; learned fill is preferable on structured backgrounds. | -| `doubao` | `豆包AI生成` text run | Fixed bottom-right corner | Reuses the image engine's synthetic alpha as the template; a stable 12-frame run above 0.35 is required. | | `seedance` | Boxed `AI` label | Fixed bottom-right corner | Requires an anchored recurring match; the full localized box is filled because a thinner synthetic shape mask leaves the real translucent rim behind. | +| `doubao` | `豆包AI生成` text run | Fixed bottom-right corner | Reuses the image engine's synthetic alpha as the template; a stable run of at least 12 frames at confidence >=0.35 is required. Without Doubao TC260 confirmation, the run also needs a strong anchor at >=0.55. | | `dola` | `Dola AI` text | Fixed bottom-right corner | Requires an anchored recurring match; ByteDance or BytePlus provenance can relax only an existing visual run. | | `hailuo` | `MINIMAX \| hailuo AI` composite label | Fixed lower edge | Uses a synthetic waveform, text, separator, and ring silhouette; the complete recurring label box is filled. A TC260 label naming MiniMax as producer can relax only an existing stable run. | | `kling` | Kling AI swirl, `KLING AI` or `KlingAI`, version, and optional `PRO` suffix | Fixed bottom-right edge | Combines a synthetic logo rescue with font and capitalization variants, an edge gate, a white-label gate, and anchored temporal recurrence. | diff --git a/docs/synthid-classifiers.md b/docs/synthid-classifiers.md index dcd9047..95c3ad5 100644 --- a/docs/synthid-classifiers.md +++ b/docs/synthid-classifiers.md @@ -1,5 +1,12 @@ # SynthID source classifiers (research) +> Audit correction, 2026-09-08: multi-period affine-probe results from +> schema 13 or earlier used confirmation patches during period selection +> (R04). They require rerunning and recalibration under schema 14; the +> frozen runtime detector uses separate code and its thresholds are unchanged. +> External-negative claims admitted without explicit independent evidence +> (R08) also need an evidence audit before reuse. + > Research archive for metadata-free OpenAI/Gemini source finding and > provider-lineage classifiers. These are not SynthID payload decoders and are > not shipped product verdicts. Current behavior: diff --git a/docs/synthid-detector-removal-plan.md b/docs/synthid-detector-removal-plan.md index 21b0252..16d415b 100644 --- a/docs/synthid-detector-removal-plan.md +++ b/docs/synthid-detector-removal-plan.md @@ -1,5 +1,12 @@ # SynthID detector and pixel-only removal research plan +> Audit correction, 2026-09-08: multi-period affine-probe results from +> schema 13 or earlier used confirmation patches during period selection +> (R04). They require rerunning and recalibration under schema 14; the +> frozen runtime detector uses separate code and its thresholds are unchanged. +> External-negative claims admitted without explicit independent evidence +> (R08) also need an evidence audit before reuse. + > Chronological mixed archive of detector, classifier, and removal work. > Not a statement of current product capability. Read the split pages > first: @@ -3689,14 +3696,11 @@ a content-dependent post-hoc neural encoder produces and what the SynthID-Image paper describes. The comb experts work on one provider and not the other because the providers are not doing the same thing. -The Google numbers say something sharper than "positives correlate more". A -single shared pattern present in two classes at different amplitudes gives a -cross-correlation at the geometric mean of the two within-class values, whereas -two distinct components would fall below it. Observed against predicted: -`0.224` against `0.227` for tile16, `0.304` against `0.306` for tile8, ratios of -`0.985` and `0.993`. So it is one pattern, not a watermark carrier layered on a -separate generator fingerprint, and the controls carry that same pattern at -roughly half the amplitude of the positives. +The normalized cross-correlations (`0.224` for tile16 and `0.304` for +tile8) are close to the geometric means of the corresponding within-class +correlations (`0.227` and `0.306`). This is consistent with shared structure, +but normalized correlations discard absolute scale. They do not establish +one unique component or a control-to-positive carrier amplitude ratio. Three readings survive that, and L1 decides between them: the controls are watermarked after all and the label is wrong; the mark modulates the amplitude of @@ -4302,8 +4306,7 @@ A full pass over the register against the recorded evidence, closing what the log already settles: - M2 re-verified independently and settled. `imagen-4.0-generate-001` and - `imagen-3.0-generate-002` return 404 in us-central1 and us-east1 on both GCP - projects (`gen-lang-client-0926942364` and `raiw-cws-publish`); + `imagen-3.0-generate-002` returned 404 in the tested regional API calls; `gemini-2.5-flash-image` rejects `addWatermark` at request parsing (`Unknown name "addWatermark" at 'generation_config': Cannot find field`), and the current Gemini API documentation states for both the Gemini-image @@ -4848,7 +4851,8 @@ Where it is written. Grayscale (PIL `L`, then RGB) stayed `detected`, so the decoder reads luminance: stacking `L,L,L` zeroes chroma, and a chroma-only carrier would have gone silent. JPEG q20 stayed `detected`, so the carrier is not the high-frequency DCT tail that classical DWT-DCT uses. A center -crop keeping half the area stayed `detected`, so the residual is spatially +crop keeping half the width and half the height (one quarter of the area) +stayed `detected`, so the residual is spatially redundant rather than a border or a single tile. A two-pixel crop stayed `detected` while our origin-locked lattice dies, so the written residual is not that lattice. 90-degree rotation and a horizontal flip stayed `detected`, @@ -5920,11 +5924,13 @@ the ordered pixel-file hash list has SHA-256 `651f959689e7043989ded09fa4aece5cea2592e7b69e371cc97c35f581d9f57d`. All 3,000 decoded hashes were unique and none overlapped the first cohort. The unchanged registered-v2 base produced no threshold crossings among 2,996 -supported images, so registered-v3 also produced 0/2,996. Together, the two -Open Images cohorts give registered-v3 0/5,993 false positives after the -confirmation rule was applied, with a one-sided 95% zero-error upper bound of -about 0.050%. They are source-disjoint samples from one source family, not two -independent acquisition mechanisms. +supported images, so registered-v3 also produced 0/2,996. Only this second +cohort was untouched during gate selection. Its one-sided 95% zero-error upper +bound is `1 - 0.05**(1/2996)`, approximately 0.09994%. The first cohort +(2,997 supported images) selected the gates and must not enlarge that +independent denominator. Both cohorts come from one source family. This +historical holdout result does not validate the later corrected selection-only +period search; that implementation needs a fresh evaluation. A second-family challenge used the first 3,000 sorted COCO val2017 files. The newline-delimited file-name list has SHA-256 @@ -5935,9 +5941,10 @@ All 3,000 pixel hashes were unique. The unchanged registered base produced 0/2,366 crossings, with maximum score `0.932964706`; registered-v3 therefore also produced zero. COCO predates this confirmation experiment and is not a fresh acquisition, but it is a distinct natural-image source family. Across -both Open Images cohorts and this COCO challenge, registered-v3 has 0/8,359 -supported-control crossings. This qualifies the precision-first positive route; -it does not turn a miss into proof of absence. +both Open Images cohorts and this COCO challenge, the historical descriptive +total is 0/8,359 supported-control crossings. It includes gate-selection data +and is not an independent qualification denominator. A miss is not proof of +absence, and corrected period selection requires a new evaluation. The fixed `0.28` candidate failed the second holdout and is rejected. It accepted one of 213 supported images, at score `0.322542963`. The runtime keeps diff --git a/docs/synthid-detector-research.md b/docs/synthid-detector-research.md index 417d60f..4a09570 100644 --- a/docs/synthid-detector-research.md +++ b/docs/synthid-detector-research.md @@ -1,5 +1,12 @@ # SynthID local detector research +> Audit correction, 2026-09-08: multi-period affine-probe results from +> schema 13 or earlier used confirmation patches during period selection +> (R04). They require rerunning and recalibration under schema 14; the +> frozen runtime detector uses separate code and its thresholds are unchanged. +> External-negative claims admitted without explicit independent evidence +> (R08) also need an evidence audit before reuse. + > Research archive for the hunt for a local, keyless SynthID pixel detector. > Not a statement of current product capability. Shipped behavior: > [supported signals](supported-signals.md) and @@ -29,9 +36,10 @@ contrast on photographs. S4, 2026-08-15: the two providers are not doing the same thing. Cross-image correlation of the folded residual is high for Google (tile16 pos-pos `+0.326`, chance `0.036`) and at chance for OpenAI (`+0.032`). Google -shares one fixed phase-coherent pattern, also present in its controls at -about half the amplitude. OpenAI shares nothing, which is what a -content-dependent post-hoc encoder produces. Comb experts that work on +has phase-coherent residual structure also correlated with its controls; +normalized correlations do not establish an absolute amplitude ratio. OpenAI +shows no shared residual above chance in this measurement, consistent with a +content-dependent post-hoc encoder. Comb experts that work on Google therefore cannot be reused as an OpenAI watermark detector. M2, 2026-08-15, re-verified 2026-08-16: Google unwatermarked pairs cannot diff --git a/docs/verification-plan.md b/docs/verification-plan.md index b466fbe..9054366 100644 --- a/docs/verification-plan.md +++ b/docs/verification-plan.md @@ -7,6 +7,10 @@ How we convince ourselves the library actually works, across its whole surface, on real data. +The [2026-09-08 code and documentation audit](code-and-documentation-audit.md) +records current reproducible defects, documentation discrepancies, review +coverage, and the checks needed to close them. + This is the pre-release and periodic-audit plan. It is deliberately organized by **oracle strength** rather than by module, because the hard part is never "call the function" -- it is "know what the right answer was". A sweep with no oracle proves only that nothing threw. diff --git a/docs/videoseal-temporal-evaluation.md b/docs/videoseal-temporal-evaluation.md index 4d5bd9c..69b3a15 100644 --- a/docs/videoseal-temporal-evaluation.md +++ b/docs/videoseal-temporal-evaluation.md @@ -1,5 +1,43 @@ # VideoSeal temporal evaluation +> Audit correction, 2026-09-08: The historical 2026-09-07 matrix and its +> conclusions are superseded by the corrected run below. The audit found +> transposed rectangular video decoding (R01) and +> observations measured before encoding rather than on the hashed artifact +> (R12). The table remains an archive, not validated delivered-file evidence. + +## Corrected artifact measurements (2026-09-08) + +The corrected run measured 32 saved artifacts: four clean controls, four +marked controls, and 24 attack outputs. Every observation decoded its saved +MP4, with equal SHA-256 before and after decoding; all 32 recorded hashes +were also checked against the retained files. The four carriers and attack +recipes are the same as described in the historical study below. + +| Carrier | crf18 | crf23 | crf28 | crf32 | scale .75 | fps .5 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| moving_gradient | 0.879 | 0.559 | 0.520 | 0.512 | 0.711 | 1.000 | +| moving_texture | 0.582 | 0.547 | 0.535 | 0.562 | 0.879 | 0.969 | +| real_veo | 1.000 | 0.953 | 0.789 | 0.621 | 0.988 | 1.000 | +| real_sora | 1.000 | 1.000 | 0.992 | 0.879 | 1.000 | 1.000 | + +Under the unchanged average-aggregation threshold of 0.9, frame-rate halving +retains detection on 4/4 carriers, downscale on 2/4, and CRF 32 on 0/4. +These are four fixed carriers, not population survival estimates. Norm-weighted +aggregation changes the moving-texture downscale result from 0.879 to 0.938; +it does not change the kernel's canonical average-aggregation rule. + +This was a local CPU run with two Torch threads, Torch 2.14.0, ffmpeg 9.0.1, +and the existing pinned checkpoint listed below. Model downloads and provider +calls were disabled. Study source SHA-256: +`a440fc95a2712097ef47665eaf8a9e9a370545d7ba05748e961955c5222546a0`. +Corrected case rows SHA-256: +`de8d633f4f63ed45e1bd53cc29d30244cd5367034bf602c21baaadaab5ba85af`. +Artifacts remain outside the repository; this dated verdict and its limits +are the durable record. + +## Historical study (2026-09-07, superseded) + Development-only study, run on 2026-09-07 from `scripts/videoseal_temporal_study.py`, answering the three temporal questions the case-level benchmark verdict aggregates away: how decoded bit accuracy @@ -44,9 +82,10 @@ Readings, not verdicts: the benchmark adapter's rule is 0.9 under plain avg. scored a perfect 1.000 on both real clips - resize was a training augmentation, and the antialias resample appears to help the detector. On the synthetic gradient it still costs 0.31. -- **Frame-rate halving is harmless.** Every carrier kept the message after - dropping half the frames: the 256-bit secret repeats across keyframes, so - temporal redundancy, not frame count, carries it. +- **Frame-rate halving was not uniformly above threshold.** Even the + historical table puts real Veo at 0.855, below the 0.9 rule; three of four + listed carriers exceed it. The corrected artifact-domain run must establish + whether that pattern survives. - **Aggregation choice matters at the margin.** Norm-weighted averaging recovered 2-6 points on damaged arms (moving_texture scale: 0.879 avg versus 0.941 l1/l2, crossing the 0.9 rule) but never rescued a collapsed diff --git a/docs/watermark-forgery-study.md b/docs/watermark-forgery-study.md index d4267f5..ff657fc 100644 --- a/docs/watermark-forgery-study.md +++ b/docs/watermark-forgery-study.md @@ -1,5 +1,44 @@ # Watermark forgery study +> Audit correction, 2026-09-08: The historical VideoSeal results and +> cross-scheme conclusions are superseded by the corrected video run below. +> Historical video observations were taken before encoding +> rather than from the hashed output artifact (R12); rectangular video +> decoding also required correction (R01). AudioSeal observations remain +> separate from those invalidated video comparisons. + +## Corrected VideoSeal measurements (2026-09-08) + +Twelve observations now measure decoded saved artifacts, including controls +and double embeds. All 12 recorded SHA-256 digests match the retained MP4s; +the decoder also checks that each file remains unchanged during measurement. + +| Cell | Gradient vs A | Gradient vs B | Real Sora vs A | Real Sora vs B | +| --- | ---: | ---: | ---: | ---: | +| clean | 0.516 | 0.555 | 0.559 | 0.535 | +| marked A | 1.000 | 0.469 | 1.000 | 0.469 | +| removed CRF 23 | 0.559 | 0.496 | 1.000 | 0.469 | +| forged B on clean | 0.469 | 1.000 | 0.469 | 1.000 | +| forged B on marked A | 0.469 | 1.000 | 0.473 | 0.996 | +| forged B on removed A | 0.469 | 1.000 | 0.469 | 1.000 | + +Both marked controls meet the fixed-message A rule. All six B-embedding +outputs fall below its 0.9 threshold, while matching B at 0.996 or above. +CRF 23 removes the matched A verdict on the gradient but preserves it on +this Sora carrier. These two fixed carriers support this scoped observation; +they do not establish complete erasure of every trace of A or a universal +double-embedding rule. AudioSeal was not rerun in this correction. + +The run used the same pinned checkpoint, Torch 2.14.0, ffmpeg 9.0.1, and +two CPU threads as the corrected [temporal study](videoseal-temporal-evaluation.md#corrected-artifact-measurements-2026-09-08). +No model downloads or provider calls ran. Study source SHA-256: +`031d9e0c58af58ab5f9227ea52a500eb86644fc464f9c37be1e894ce0b6b4935`. +Corrected case rows SHA-256: +`f8c5a65e5853de0f1ff67c2ef0549fbd81df971e13c79539c85c1b0255eb60a8`. +Artifacts remain outside the repository. + +## Historical study (2026-09-07, superseded video results) + Development-only study, run on 2026-09-07 from `scripts/watermark_forgery_study.py`, opening the forgery/remover-forensics direction from the original research map. It measures the four-state model diff --git a/maintain.sh b/maintain.sh index 0a5462f..45c0a5e 100755 --- a/maintain.sh +++ b/maintain.sh @@ -7,20 +7,9 @@ uv sync --all-extras # inside the project env crashes and, with set -e, aborts the whole gate before # ruff/pyright/tests (see CLAUDE.md "Test and lint"). uvx uv-outdated -# uv-secure prints its verdict but can then crash in an internal teardown with a -# NON-ZERO exit -- observed as "annotated-doc raised exception" and later "anyio raised -# exception"; both are bugs in uv-secure's OWN uvx env, not a project vulnerability. With -# set -e that teardown crash aborts the whole gate before ruff/pyright/tests. So gate on -# the VERDICT, not the exit code: capture the output, accept the run when uv-secure -# reported all-safe (even if it then crashed), but still FAIL on a real finding (no -# all-safe line) so a genuine CVE is never masked, and fail loud if it never got a -# verdict at all (so a broken run is never silently skipped). -secure_out="$(uvx uv-secure uv.lock 2>&1)" || true -printf '%s\n' "$secure_out" -if ! grep -qE "No vulnerabilities or maintenance issues detected|All dependencies appear safe" <<<"$secure_out"; then - echo "maintain.sh: uv-secure reported a finding or failed before its verdict -- triage before committing." >&2 - exit 1 -fi +# Findings and scanner failures both stop the gate, even if a success message +# preceded a failure. Scan only this lockfile, not sibling worktree copies. +uvx uv-secure uv.lock uv run python scripts/sync_c2pa_soft_bindings.py --check uv run ruff check --fix uv run ruff format diff --git a/pyproject.toml b/pyproject.toml index 393af81..6b60f6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,8 +141,8 @@ text-draft = [ # because it pulls torch and downloads model weights on first use. identify() # guards the import and skips the TrustMark signal when absent. trustmark = [ - # TrustMark requires NumPy <2, whose last release has no CPython 3.13+ - # wheels. Keep this feature installable only on its real runtime range. + # The supported TrustMark floor includes releases requiring NumPy <2, + # without CPython 3.13+ wheels. Keep the established Python support range. "trustmark>=0.8.0; python_version < '3.13'", ] # Metadata-free photo classifier (Model 1 then gated Model 2). CPU torch is @@ -250,7 +250,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "scripts/**/*.py" = ["G004", "S108", "S310", "T20"] -"tests/*.py" = ["ANN", "S101", "S105", "S106", "S108"] +"tests/*.py" = ["ANN", "S101", "S105", "S106", "S108"] # fixture and mock signatures need not annotate every parameter "src/remove_ai_watermarks/_internal/watermark_remover.py" = ["S603", "S606", "S607"] # nvidia-smi capability probe "src/remove_ai_watermarks/_internal/c2pa.py" = ["S110"] # try-except-pass for corrupt file handling diff --git a/scripts/_isolated_image_workers.py b/scripts/_isolated_image_workers.py new file mode 100644 index 0000000..94cfa5c --- /dev/null +++ b/scripts/_isolated_image_workers.py @@ -0,0 +1,65 @@ +"""Bound native image work in fresh interpreters, with no parent-process fallback.""" + +from __future__ import annotations + +import json +import runpy +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Any + + +def _run_one(script: Path, path: str, timeout: float) -> dict[str, Any]: + command = [sys.executable, str(Path(__file__).resolve()), str(script), path] + try: + result = subprocess.run( # noqa: S603 -- fixed interpreter and local worker entry point + command, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired: + return {"path": path, "keys": [], "status": "timeout"} + except OSError as error: + return {"path": path, "keys": [], "status": f"worker-error:{type(error).__name__}"} + if result.returncode != 0: + return {"path": path, "keys": [], "status": "crashed", "returncode": result.returncode} + try: + row = json.loads(result.stdout) + if not isinstance(row, dict) or row.get("status") is None: + raise ValueError("worker returned an invalid record") + except ValueError: + return {"path": path, "keys": [], "status": "invalid-worker-result"} + return row + + +def run_batch(script: Path, paths: list[str], *, jobs: int, timeout: float) -> list[dict[str, Any]]: + """Run at most jobs child processes; each child gets its own timeout. + + Threads only supervise subprocesses. subprocess.run kills and reaps a timed-out + child before returning, so executor shutdown never waits on unbounded native work. + No fork or multiprocessing spawn imports the parent's OpenCV/Pillow state. + """ + if jobs < 1 or timeout <= 0: + raise ValueError("jobs and timeout must be positive") + with ThreadPoolExecutor(max_workers=jobs) as executor: + return list(executor.map(lambda path: _run_one(script, path, timeout), paths)) + + +def main() -> None: + script = Path(sys.argv[1]).resolve() + sys.path.insert(0, str(script.parent.parent / "src")) + # stdout is the worker protocol, diagnostics from the audit go to stderr. + from contextlib import redirect_stdout + + with redirect_stdout(sys.stderr): + namespace = runpy.run_path(str(script)) + row = namespace["_one"](sys.argv[2]) + sys.stdout.write(json.dumps(row)) + + +if __name__ == "__main__": + main() diff --git a/scripts/_text_eval.py b/scripts/_text_eval.py index 2b044d8..0da0372 100644 --- a/scripts/_text_eval.py +++ b/scripts/_text_eval.py @@ -11,7 +11,7 @@ def normalize_text(text: str) -> str: def levenshtein_normalized(left: str, right: str) -> float: - """Return normalized Levenshtein distance without changing either input.""" + """Return Levenshtein distance divided by the longer input length (not CER).""" if not left and not right: return 0.0 previous = list(range(len(right) + 1)) diff --git a/scripts/audioseal_experiment.py b/scripts/audioseal_experiment.py index 207cbe2..ecd3df7 100644 --- a/scripts/audioseal_experiment.py +++ b/scripts/audioseal_experiment.py @@ -62,6 +62,7 @@ FRAME_RATE = 12.0 FRAME_WIDTH = 840 FRAME_HEIGHT = 480 CARRIER_SEED = 20260907 +NOISE_ATTACK_SEED = CARRIER_SEED + 1 MESSAGE_SEED = 7 @@ -103,12 +104,12 @@ def carrier_seed(name: str) -> int: return CARRIER_SEED + zlib.crc32(name.encode()) % 1000 -def synth_carrier(name: str, seconds: float) -> np.ndarray: +def synth_carrier(name: str, seconds: float, *, seed: int | None = None) -> np.ndarray: """Synthesize one deterministic carrier by registered name.""" factory = CARRIERS.get(name) if factory is None: raise KeyError(f"unknown carrier {name!r}; expected one of {sorted(CARRIERS)}") - master = np.random.default_rng(carrier_seed(name)) + master = np.random.default_rng(carrier_seed(name) if seed is None else seed) return np.asarray(factory(int(seconds * SAMPLE_RATE), master), dtype=np.float32) @@ -383,7 +384,7 @@ def embed_audio(generator: object, samples: np.ndarray, message: list[int]) -> n def numpy_attacks(samples: np.ndarray) -> dict[str, np.ndarray]: - rng = np.random.default_rng(CARRIER_SEED + 1) + rng = np.random.default_rng(NOISE_ATTACK_SEED) out: dict[str, np.ndarray] = {} power = float(np.sum(samples.astype(np.float64) ** 2)) noise = rng.normal(0.0, 1.0, samples.shape[0]).astype(np.float32) diff --git a/scripts/fidelity_metrics.py b/scripts/fidelity_metrics.py index 21f2a42..9e2c723 100644 --- a/scripts/fidelity_metrics.py +++ b/scripts/fidelity_metrics.py @@ -32,10 +32,10 @@ Two subcommands: Run this on the ORIGINALS, hand-verify/correct the file, and it becomes the ground truth for ``compare --ground-truth`` -- the clean way to score text, since OCR-vs-OCR is doubly noisy (errors on both - images + reading-order differences inflate CER even on identical text). + images + reading-order differences inflate NED even on identical text). compare -- Score each VARIANT against the ORIGINAL across four groups: - 1. Text -- character error rate (CER) of the variant's OCR vs the + 1. Text -- normalized edit distance (NED) of the variant's OCR vs the verified ground truth (or the original's OCR if no --ground-truth). 2. Face identity -- insightface (buffalo_l) ArcFace cosine similarity. 3. Face texture -- LPIPS + Laplacian-variance ratio on face crops @@ -88,7 +88,7 @@ def _match_size(variant: np.ndarray, ref: np.ndarray) -> np.ndarray: def _norm(text: str) -> str: - """Normalize for CER: NFC + drop ALL whitespace (segmentation-order agnostic).""" + """Normalize for NED: NFC + drop ALL whitespace (segmentation-order agnostic).""" return "".join(unicodedata.normalize("NFC", text).split()) @@ -167,7 +167,8 @@ def _ocr_lines(bgr: np.ndarray, langs: list[str], min_score: float = 0.5) -> lis return [t for _, t in kept] -def _cer(ref: str, hyp: str) -> float: +def _text_ned(ref: str, hyp: str) -> float: + """Case-sensitive edit distance divided by the longer normalized string.""" return levenshtein_normalized(_norm(ref), _norm(hyp)) @@ -334,7 +335,7 @@ def compare(original: str, variants: tuple[str, ...], ocr_langs: str, ground_tru lp = _lpips_model() # AlexNet LPIPS, loaded once and reused for face crops + whole image # ── text ── - ocr_cer: dict[str, float | None] = {label: None for label, _ in parsed} + ocr_ned: dict[str, float | None] = {label: None for label, _ in parsed} if langs: ref_text: str | None = None if ground_truth: @@ -348,7 +349,7 @@ def compare(original: str, variants: tuple[str, ...], ocr_langs: str, ground_tru if ref_text: console.print(f" OCR variants ({','.join(langs)})...") for label, img in parsed: - ocr_cer[label] = _cer(ref_text, "\n".join(_ocr_lines(img, langs))) + ocr_ned[label] = _text_ned(ref_text, "\n".join(_ocr_lines(img, langs))) # ── faces ── face_stats: dict[str, FaceStats] = {label: FaceStats() for label, _ in parsed} @@ -392,14 +393,14 @@ def compare(original: str, variants: tuple[str, ...], ocr_langs: str, ground_tru # ── report ── table = Table(title=f"Fidelity vs {Path(original).name} (reference)") - for col in ("variant", "text CER↓", "faces", "ID cos↑", "face LPIPS↓", "lapvar↑", "img LPIPS↓", "SSIM↑", "PSNR↑"): + for col in ("variant", "text NED↓", "faces", "ID cos↑", "face LPIPS↓", "lapvar↑", "img LPIPS↓", "SSIM↑", "PSNR↑"): table.add_column(col) for label, _ in parsed: st = face_stats[label] wl, ws, wp = whole[label] table.add_row( label, - _fmt(ocr_cer[label]), + _fmt(ocr_ned[label]), str(st.n_faces), _fmt(_mean(st.identity)), _fmt(_mean(st.lpips)), @@ -410,7 +411,7 @@ def compare(original: str, variants: tuple[str, ...], ocr_langs: str, ground_tru ) console.print(table) console.print( - " Legend: CER lower=better; ID cos higher=better; face LPIPS lower=better; " + " Legend: NED lower=better; ID cos higher=better; face LPIPS lower=better; " "lapvar ratio ~1=detail kept, <1=smoothed/plastic; img LPIPS lower=better; SSIM/PSNR higher=closer." ) diff --git a/scripts/pill_gate_audit.py b/scripts/pill_gate_audit.py index 1bc66bc..95e04e9 100644 --- a/scripts/pill_gate_audit.py +++ b/scripts/pill_gate_audit.py @@ -32,12 +32,12 @@ import json import math import os import sys -from concurrent.futures import ProcessPoolExecutor, as_completed -from concurrent.futures import TimeoutError as FutureTimeout -from concurrent.futures.process import BrokenProcessPool from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) +sys.path.insert(0, str(Path(__file__).parent)) + +from _isolated_image_workers import run_batch REPO = Path(__file__).resolve().parents[1] POSITIVES = REPO / ".local-eval" / "visible-positives.jsonl" @@ -95,18 +95,8 @@ def _one(path_str: str) -> dict[str, object]: def _batch(paths: list[str], jobs: int, timeout: int) -> list[dict[str, object]]: - try: - with ProcessPoolExecutor(max_workers=jobs) as ex: - futs = [ex.submit(_one, p) for p in paths] - return [f.result() for f in as_completed(futs, timeout=timeout)] - except (FutureTimeout, BrokenProcessPool, OSError, RuntimeError): - out = [] - for p in paths: - try: - out.append(_one(p)) - except BaseException: # a native crash costs one file, not the sweep - out.append({"path": Path(p).name, "status": "crashed"}) - return out + """Bound each image in an independent interpreter, including native failures.""" + return run_batch(Path(__file__), paths, jobs=jobs, timeout=timeout) def report(rows: list[dict[str, object]]) -> None: diff --git a/scripts/provider_oracle_web.py b/scripts/provider_oracle_web.py index 6155253..e5d05c2 100644 --- a/scripts/provider_oracle_web.py +++ b/scripts/provider_oracle_web.py @@ -14,7 +14,7 @@ from urllib.parse import unquote, urlsplit import provider_oracles as oracles if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Iterator, Mapping from pathlib import Path from playwright.sync_api import Browser, BrowserContext, BrowserType, Page, ProxySettings @@ -241,11 +241,10 @@ def _submit_batch( *, headed: bool, timeout_seconds: float, -) -> list[WebVerdict]: +) -> Iterator[WebVerdict]: """Submit every row in one isolated session without retries or route changes.""" from playwright.sync_api import sync_playwright - verdicts: list[WebVerdict] = [] with sync_playwright() as playwright: browser = _launch_browser(playwright.chromium, proxy, headed=headed) context = _new_context(browser, proxy) @@ -256,13 +255,12 @@ def _submit_batch( log.info("Submitting %s to %s", upload.name, oracles.SURFACES[surface].url) verdict = _check_page(page, surface, upload, timeout_seconds) log.info("Provider response for %s: %s", upload.name, verdict.raw_response) - verdicts.append(verdict) + yield verdict finally: page.close() finally: context.close() browser.close() - return verdicts def run_web_batch( @@ -305,10 +303,8 @@ def run_web_batch( headed=headed, timeout_seconds=timeout_seconds, ) - if len(verdicts) != len(pending): - raise RuntimeError(f"Playwright returned {len(verdicts)} verdicts for {len(pending)} uploads") - checked_at = datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") for (manifest_row, _result), verdict in zip(pending, verdicts, strict=True): + checked_at = datetime.now(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z") oracles.record_result( manifest_path, artifact_id=manifest_row["artifact_id"], diff --git a/scripts/robustness_suite.py b/scripts/robustness_suite.py index 0b6748e..b04dfdf 100644 --- a/scripts/robustness_suite.py +++ b/scripts/robustness_suite.py @@ -95,6 +95,9 @@ def graceful(res: Results, case: str, cmd: str, args: list[str], timeout: int = if timed_out: res.add(case, cmd, False, f"HUNG (> {timeout}s)", out) return + if code not in (0, 1, 2): + res.add(case, cmd, False, f"unexpected process exit {code}", out) + return crashed = next((m for m in _CRASH_MARKERS if m in out), None) if crashed: res.add(case, cmd, False, f"unhandled {crashed} (exit {code})", out) diff --git a/scripts/smoke_matrix.py b/scripts/smoke_matrix.py index 7326b95..b6d0c83 100644 --- a/scripts/smoke_matrix.py +++ b/scripts/smoke_matrix.py @@ -49,6 +49,8 @@ import tempfile from dataclasses import dataclass, field from pathlib import Path +from robustness_suite import _CRASH_MARKERS + REPO = Path(__file__).resolve().parents[1] SAMPLES = REPO / "data" / "fixtures" / "provenance" CORPUS = REPO / ".local-eval" / "originals" @@ -61,6 +63,8 @@ def _capture(args: list[str]) -> str: p = subprocess.run( # noqa: S603 [exe, "run", "remove-ai-watermarks", *args], capture_output=True, text=True, cwd=REPO, check=False ) + if p.returncode != 0: + raise RuntimeError(f"CLI capture failed (exit {p.returncode}): {p.stderr}") return p.stdout @@ -77,7 +81,7 @@ class Runner: tmp: Path results: list[Result] = field(default_factory=list) - def run(self, name: str, args: list[str], *, expect_exit: int | None = 0, timeout: int = 180) -> Result: + def run(self, name: str, args: list[str], *, expect_exit: int | tuple[int, ...] = 0, timeout: int = 180) -> Result: exe = shutil.which("uv") or "uv" cmd = [exe, "run", "remove-ai-watermarks", *args] try: @@ -88,7 +92,10 @@ class Runner: r = Result(name, "FAIL", f"timeout after {timeout}s", " ".join(args)) self.results.append(r) return r - ok = expect_exit is None or p.returncode == expect_exit + acceptable = (expect_exit,) if isinstance(expect_exit, int) else expect_exit + output = p.stdout + p.stderr + crashed = any(marker in output for marker in _CRASH_MARKERS) + ok = p.returncode in acceptable and not crashed detail = "" if ok else f"exit {p.returncode} (want {expect_exit}): {(p.stderr or p.stdout).strip()[-200:]}" r = Result(name, "pass" if ok else "FAIL", detail, " ".join(args)) self.results.append(r) @@ -293,7 +300,7 @@ def main() -> None: r.run( f"batch --mode {mode}", ["batch", str(bd), "--mode", mode, "-o", str(tmp / f"batch_{mode}")], - expect_exit=None, + expect_exit=0, ) # ---- diffusion: argument handling always; the model body under --diffusion ---- @@ -319,8 +326,8 @@ def main() -> None: r.run( f"visible auto real {label}", ["visible", str(p), "-o", str(tmp / f"r_{label}_{p.stem[:8]}.png")], - expect_exit=None, - ) # 0 or 2 are both correct; a CRASH is not + expect_exit=(0, EXIT_NO_VISIBLE_MARK), + ) # Successful removal or the documented no-mark outcome. # unicode + misnamed extension + truncated: the documented real-world traps if picks: @@ -333,7 +340,7 @@ def main() -> None: r.run("misnamed extension", ["identify", str(mis), "--json"]) trunc = tmp / "truncated.png" trunc.write_bytes((SAMPLES / "chatgpt-1.png").read_bytes()[:4096]) - r.run("truncated file does not crash", ["identify", str(trunc), "--json"], expect_exit=None) + r.run("truncated file does not crash", ["identify", str(trunc), "--json"], expect_exit=(0, 1)) # ---- report -------------------------------------------------------------- bad = [x for x in r.results if x.status == "FAIL"] @@ -470,9 +477,12 @@ def _media_rows(r: Runner, tmp: Path) -> None: r.skip(f"{name} metadata strip", "ffmpeg could not synthesize the fixture") continue out = tmp / f"media_clean.{name}" - r.run(f"{name} metadata strip runs", ["metadata", str(src), "--remove", "-o", str(out)], expect_exit=None) - if out.exists(): - r.check(f"{name} strip produced a non-empty file", out.stat().st_size > 0, "empty output") + r.run(f"{name} metadata strip runs", ["metadata", str(src), "--remove", "-o", str(out)], expect_exit=0) + r.check( + f"{name} strip produced a non-empty file", + out.is_file() and out.stat().st_size > 0, + "missing or empty output", + ) def _sdxl_watermark_bits(img: object) -> float: @@ -557,7 +567,7 @@ def _diffusion_rows(r: Runner, tmp: Path, doubao: Path) -> None: res = r.run( "batch --mode invisible runs (512px)", ["batch", str(bd), "--mode", "invisible", "-o", str(bout), *small], - expect_exit=None, + expect_exit=0, timeout=3600, ) if res.status == "pass": @@ -569,7 +579,7 @@ def _diffusion_rows(r: Runner, tmp: Path, doubao: Path) -> None: r.run( "batch --mode all runs (512px)", ["batch", str(bd), "--mode", "all", "-o", str(aout), *small], - expect_exit=None, + expect_exit=0, timeout=3600, ) diff --git a/scripts/synthid_affine_lattice_probe.py b/scripts/synthid_affine_lattice_probe.py index 683649c..37367e7 100644 --- a/scripts/synthid_affine_lattice_probe.py +++ b/scripts/synthid_affine_lattice_probe.py @@ -1003,7 +1003,7 @@ def score_lattice( for index in rerank_candidates ] rerank_scores = [ - min(*whitened_scores[index], amplitude.selection, amplitude.confirmation) + min(whitened_scores[index][0], amplitude.selection) for index, amplitude in zip(rerank_candidates, rerank_amplitudes, strict=True) ] rerank_winner = int(np.argmax(rerank_scores)) @@ -1603,7 +1603,8 @@ def main( report_out.write_text( json.dumps( { - "schema_version": 13, + "schema_version": 14, + "period_selection": "selection-only-whitened-amplitude-v1", "template": str(template_path), "periods": [float(value) for value in periods], "rotations_degrees": [float(value) for value in rotations], diff --git a/scripts/synthid_conformal_cascade.py b/scripts/synthid_conformal_cascade.py index f68a846..87414ca 100644 --- a/scripts/synthid_conformal_cascade.py +++ b/scripts/synthid_conformal_cascade.py @@ -284,7 +284,7 @@ def load_config(path: Path) -> CascadeConfig: def load_observation_records(path: Path) -> list[tuple[str, tuple[ExpertObservation, ...]]]: """Load named score records with explicit support for every expert.""" payload = _mapping(json.loads(path.read_text(encoding="utf-8")), "observation manifest") - if payload.get("schema_version") != 1: + if payload.get("schema_version") not in {1, 2}: raise ValueError("unsupported observation manifest schema") records: list[tuple[str, tuple[ExpertObservation, ...]]] = [] for record_index, raw_record in enumerate(_sequence(payload.get("records"), "records")): @@ -311,6 +311,11 @@ def load_observation_records(path: Path) -> list[tuple[str, tuple[ExpertObservat else _number(raw_score, f"records[{record_index}].observations[{observation_index}].score"), ) ) + if payload.get("schema_version") == 1: + from synthid_runtime.synthid_detector import REGISTERED_DETECTOR_ID + + if any(observation.name == REGISTERED_DETECTOR_ID for observation in observations): + raise ValueError("legacy runtime observations require rescoring to preserve fallback expert identity") records.append((record_id, tuple(observations))) return records diff --git a/scripts/synthid_periodic_tile_probe.py b/scripts/synthid_periodic_tile_probe.py index c33d4c9..008e078 100644 --- a/scripts/synthid_periodic_tile_probe.py +++ b/scripts/synthid_periodic_tile_probe.py @@ -120,8 +120,8 @@ def score_pixels( path=path, score=score, active_support=min(norm / (model.expected_norm + 1e-12), 1.0), - row_shift=row_shift, - column_shift=column_shift, + row_shift=int(row_shift), + column_shift=int(column_shift), repeat_count=(model.height // model.tile_height) * (model.width // model.tile_width), ) @@ -172,8 +172,8 @@ def load_model(path: Path) -> PeriodicTileModel: ) if model.height < 1 or model.width < 1 or model.tile_height < 1 or model.tile_width < 1: raise ValueError("invalid periodic-tile geometry") - if model.height % model.tile_height or model.width % model.tile_width: - raise ValueError("image geometry is not divisible by periodic-tile geometry") + if model.height < model.tile_height or model.width < model.tile_width: + raise ValueError("image geometry must be at least as large as the tile geometry") if model.template.shape != (model.tile_height, model.tile_width, 3): raise ValueError("invalid periodic-tile template shape") if not np.all(np.isfinite(model.template)) or not np.isclose(np.linalg.norm(model.template), 1.0): diff --git a/scripts/synthid_pixel_probe.py b/scripts/synthid_pixel_probe.py index a55b110..00092c9 100644 --- a/scripts/synthid_pixel_probe.py +++ b/scripts/synthid_pixel_probe.py @@ -62,7 +62,9 @@ def carrier(gray: NDArray[np.float64]) -> NDArray[np.float64]: def ncc(a: NDArray[np.float64], b: NDArray[np.float64]) -> float: """Normalized cross-correlation of two carriers (unit-norm zero-mean vectors).""" - if a.shape != b.shape or a.size == 0: + if a.shape != b.shape: + raise ValueError("carrier geometry does not match") + if a.size == 0: return 0.0 return float(np.dot(a, b)) @@ -88,14 +90,13 @@ def random_baseline(shape: tuple[int, ...], n: int, *, seed: int = 0) -> float: def _load_carriers(paths: tuple[str, ...]) -> list[NDArray[np.float64]]: - """Load carriers for same-shaped images; warn and skip mismatched shapes.""" + """Load carriers only when every image has the same comparable geometry.""" grays = [(p, load_gray(p)) for p in paths] shape = grays[0][1].shape carriers: list[NDArray[np.float64]] = [] for p, g in grays: if g.shape != shape: - console.print(f" [yellow]skip[/] {p}: shape {g.shape} != {shape}") - continue + raise click.ClickException(f"{p}: geometry {g.shape} does not match {shape}") carriers.append(carrier(g)) return carriers @@ -129,8 +130,9 @@ def consistency(images: tuple[str, ...]) -> None: ) def removal(pos: tuple[str, ...], cleaned: tuple[str, ...]) -> None: """Does the pipeline drop the carrier correlation toward the random baseline?""" - pos_carriers = _load_carriers(pos) - cleaned_carriers = _load_carriers(cleaned) + carriers = _load_carriers(pos + cleaned) + pos_carriers = carriers[: len(pos)] + cleaned_carriers = carriers[len(pos) :] if not pos_carriers or not cleaned_carriers: console.print("[red]Need at least one positive and one cleaned fill of matching shape.[/]") raise SystemExit(1) diff --git a/scripts/synthid_research_manifest.py b/scripts/synthid_research_manifest.py index c5b0779..b7825e1 100644 --- a/scripts/synthid_research_manifest.py +++ b/scripts/synthid_research_manifest.py @@ -208,6 +208,13 @@ def _row_errors(row: dict[str, str], index: int) -> list[str]: errors.append(f"{prefix}: source-evidence cannot establish a same-provider negative") if verifier == "source-evidence" and not row.get("evidence_reference", "").strip(): errors.append(f"{prefix}: source-evidence requires evidence_reference") + if ( + synthid == "not_detected" + and source != target + and verifier not in matching + and not row.get("evidence_reference", "").strip() + ): + errors.append(f"{prefix}: an external negative without a matching verifier requires evidence_reference") if verifier in matching and not row.get("oracle_session", "").strip(): errors.append(f"{prefix}: provider verification requires oracle_session") if oracle_role == "source_control" and synthid != "detected": diff --git a/scripts/synthid_routed_detector.py b/scripts/synthid_routed_detector.py index 02081cb..e8bd4e6 100644 --- a/scripts/synthid_routed_detector.py +++ b/scripts/synthid_routed_detector.py @@ -53,7 +53,7 @@ def main(images: tuple[Path, ...], report_out: Path) -> None: report_out.write_text( json.dumps( { - "schema_version": 1, + "schema_version": 2, "counts": {"detected": detected, "abstain": len(records) - detected}, "records": records, }, diff --git a/scripts/synthid_routed_expert_bank.py b/scripts/synthid_routed_expert_bank.py index d7c4f2a..08fb11e 100644 --- a/scripts/synthid_routed_expert_bank.py +++ b/scripts/synthid_routed_expert_bank.py @@ -27,6 +27,7 @@ from synthid_conformal_cascade import ( # noqa: E402 ) from synthid_research_manifest import artifact_sha256 # noqa: E402 from synthid_runtime import synthid_detector # noqa: E402 +from synthid_runtime_expert_scores import EXPERT_NAMES # noqa: E402 log = logging.getLogger(__name__) @@ -46,18 +47,18 @@ class RoutedBankResult: registered_score: float | None large_supported: bool large_score: float | None + opponent_supported: bool + opponent_score: float | None + fine_opponent_supported: bool + fine_opponent_score: float | None def classify_routed(observations: tuple[ExpertObservation, ...]) -> RoutedBankResult: - """Route explicit fixed, registered, and large observations without an OR rule.""" + """Route fixed, large, and the active registered expert without an OR rule.""" by_name = {observation.name: observation for observation in observations} if len(by_name) != len(observations): raise ValueError("observation expert names must be unique") - expected = { - synthid_detector.DETECTOR_ID, - synthid_detector.REGISTERED_DETECTOR_ID, - synthid_detector.LARGE_DETECTOR_ID, - } + expected = set(EXPERT_NAMES) if by_name.keys() != expected: missing = sorted(expected - by_name.keys()) unknown = sorted(by_name.keys() - expected) @@ -66,6 +67,17 @@ def classify_routed(observations: tuple[ExpertObservation, ...]) -> RoutedBankRe fixed = by_name[synthid_detector.DETECTOR_ID] registered = by_name[synthid_detector.REGISTERED_DETECTOR_ID] large = by_name[synthid_detector.LARGE_DETECTOR_ID] + opponent = by_name[synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID] + fine_opponent = by_name[synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID] + active_registered = [item for item in (registered, opponent, fine_opponent) if item.supported] + if len(active_registered) > 1: + raise ValueError("registered route must contain at most one active expert") + routed_registered = active_registered[0] if active_registered else registered + registered_thresholds = { + registered.name: synthid_detector.REGISTERED_THRESHOLD, + opponent.name: synthid_detector.OPPONENT_REGISTERED_THRESHOLD, + fine_opponent.name: synthid_detector.FINE_OPPONENT_REGISTERED_THRESHOLD, + } if large.supported: if large.score is None: raise RuntimeError("validated large observation lost its score") @@ -77,13 +89,13 @@ def classify_routed(observations: tuple[ExpertObservation, ...]) -> RoutedBankRe verdict = "abstain" reason = "large_below_threshold" selected_expert = None - elif registered.supported: - if registered.score is None: + elif routed_registered.supported: + if routed_registered.score is None: raise RuntimeError("validated registered observation lost its score") - if registered.score >= synthid_detector.REGISTERED_THRESHOLD: + if routed_registered.score >= registered_thresholds[routed_registered.name]: verdict = "detected" reason = "registered_threshold_crossed" - selected_expert = registered.name + selected_expert = routed_registered.name else: verdict = "abstain" reason = ( @@ -115,6 +127,10 @@ def classify_routed(observations: tuple[ExpertObservation, ...]) -> RoutedBankRe registered_score=registered.score, large_supported=large.supported, large_score=large.score, + opponent_supported=opponent.supported, + opponent_score=opponent.score, + fine_opponent_supported=fine_opponent.supported, + fine_opponent_score=fine_opponent.score, ) @@ -122,7 +138,7 @@ def classify_routed(observations: tuple[ExpertObservation, ...]) -> RoutedBankRe @click.argument("observation_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option("--report-out", type=click.Path(dir_okay=False, path_type=Path), required=True) def main(observation_path: Path, report_out: Path) -> None: - """Route a three-expert pixel score manifest from OBSERVATION_PATH.""" + """Route a versioned runtime expert score manifest from OBSERVATION_PATH.""" logging.basicConfig(level=logging.INFO, format="%(message)s") counts: dict[RoutedVerdict, int] = {"detected": 0, "abstain": 0} rows: list[dict[str, object]] = [] @@ -134,7 +150,7 @@ def main(observation_path: Path, report_out: Path) -> None: report_out.write_text( json.dumps( { - "schema_version": 1, + "schema_version": 2, "observation_sha256": artifact_sha256(observation_path), "counts": counts, "records": rows, diff --git a/scripts/synthid_runtime_expert_scores.py b/scripts/synthid_runtime_expert_scores.py index d65ea5c..fb6ef6b 100644 --- a/scripts/synthid_runtime_expert_scores.py +++ b/scripts/synthid_runtime_expert_scores.py @@ -1,4 +1,4 @@ -"""Export fixed, large, and scale-registered SynthID observations for images. +"""Export correctly named fixed, large, and routed registered observations. The output is an input manifest for ``synthid_conformal_cascade.py``. All experts consume decoded RGB pixels only. Unsupported geometry is recorded @@ -32,6 +32,15 @@ log = logging.getLogger(__name__) FIXED_EXPERT_NAME = synthid_detector.DETECTOR_ID REGISTERED_EXPERT_NAME = synthid_detector.REGISTERED_DETECTOR_ID LARGE_EXPERT_NAME = synthid_detector.LARGE_DETECTOR_ID +OPPONENT_EXPERT_NAME = synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID +FINE_OPPONENT_EXPERT_NAME = synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID +EXPERT_NAMES = ( + FIXED_EXPERT_NAME, + REGISTERED_EXPERT_NAME, + LARGE_EXPERT_NAME, + OPPONENT_EXPERT_NAME, + FINE_OPPONENT_EXPERT_NAME, +) class ExpertScore(TypedDict): @@ -57,26 +66,21 @@ def _observation(name: str, supported: bool, score: float | None) -> ExpertScore def score_pixels(pixels: NDArray[np.uint8]) -> list[ExpertScore]: - """Return explicit fixed, registered, and large observations for RGB PIXELS.""" + """Return five named slots; inactive registered fallbacks remain unsupported.""" if pixels.ndim != 3 or pixels.shape[2] != 3 or pixels.dtype != np.uint8: raise ValueError("pixels must be an RGB uint8 array") bgr_pixels = np.ascontiguousarray(pixels[:, :, ::-1]) native = synthid_detector.detect_synthid("decoded-image", image=bgr_pixels, register_scale=False) registered = synthid_detector.detect_synthid("decoded-image", image=bgr_pixels, register_scale=True) - fixed = _observation(FIXED_EXPERT_NAME, False, None) - large = _observation(LARGE_EXPERT_NAME, False, None) - native_observation = _observation(native.detector, native.status != "unsupported", native.score) - if native.detector == FIXED_EXPERT_NAME: - fixed = native_observation - elif native.detector == LARGE_EXPERT_NAME: - large = native_observation - else: - raise RuntimeError(f"unexpected default SynthID expert: {native.detector}") - return [ - fixed, - _observation(REGISTERED_EXPERT_NAME, registered.status != "unsupported", registered.score), - large, - ] + observations = {name: _observation(name, False, None) for name in EXPERT_NAMES} + for result, expected in ( + (native, {FIXED_EXPERT_NAME, LARGE_EXPERT_NAME}), + (registered, {REGISTERED_EXPERT_NAME, OPPONENT_EXPERT_NAME, FINE_OPPONENT_EXPERT_NAME}), + ): + if result.detector not in expected: + raise RuntimeError(f"unexpected SynthID expert: {result.detector}") + observations[result.detector] = _observation(result.detector, result.status != "unsupported", result.score) + return list(observations.values()) def score_path(path: Path) -> ScoredImage: @@ -103,8 +107,8 @@ def main(images: tuple[Path, ...], report_out: Path) -> None: report_out.write_text( json.dumps( { - "schema_version": 1, - "experts": [FIXED_EXPERT_NAME, REGISTERED_EXPERT_NAME, LARGE_EXPERT_NAME], + "schema_version": 2, + "experts": list(EXPERT_NAMES), "records": records, }, indent=2, @@ -112,7 +116,7 @@ def main(images: tuple[Path, ...], report_out: Path) -> None: + "\n", encoding="utf-8", ) - log.info("Wrote %d three-expert score records: %s", len(records), report_out) + log.info("Wrote %d runtime expert score records: %s", len(records), report_out) if __name__ == "__main__": diff --git a/scripts/videoseal_temporal_study.py b/scripts/videoseal_temporal_study.py index 465ef0d..17226e8 100644 --- a/scripts/videoseal_temporal_study.py +++ b/scripts/videoseal_temporal_study.py @@ -39,7 +39,7 @@ sys.path.insert(0, str(REPO / "scripts")) import videoseal_oracle # noqa: E402 from audioseal_experiment import ffmpeg_version # noqa: E402 from videoseal_oracle import read, read_aggregation_matrix # noqa: E402 -from watermark_benchmark import _decode_video, sha256_file # noqa: E402 +from watermark_benchmark import _decode_video, decode_video_artifact, sha256_file # noqa: E402 from watermark_benchmark_video_cohort import ( # noqa: E402 FRAME_RATE, encode_clip, @@ -205,21 +205,25 @@ def main() -> int: workdir = out_dir / name workdir.mkdir() clean_path = encode_clip(ffmpeg, workdir / f"{name}-clean.mp4", clean_frames) + clean_frames, clean_digest = decode_video_artifact(clean_path) record( "carrier_clean", carrier=name, - sha256=sha256_file(clean_path), + sha256=clean_digest, + measurement_domain="decoded_artifact", aggregation_matrix=read_aggregation_matrix(model, clean_frames), **provenance, ) marked_frames = np.asarray(videoseal_oracle.embed(model, clean_frames, message), dtype=np.float32) marked_path = encode_clip(ffmpeg, workdir / f"{name}-marked.mp4", marked_frames) + marked_frames, marked_digest = decode_video_artifact(marked_path) marked_read = read(model, marked_frames) record( "carrier_marked", carrier=name, - sha256=sha256_file(marked_path), + sha256=marked_digest, + measurement_domain="decoded_artifact", bit_accuracy_avg=marked_read.bit_accuracy, aggregation_matrix=read_aggregation_matrix(model, marked_frames), per_frame_bit_accuracy=[round(v, 4) for v in marked_read.per_frame_bit_accuracy], @@ -243,15 +247,14 @@ def main() -> int: ("fps_half", apply_fps_half(ffmpeg, marked_path, workdir)), ] for attack, path in attacked: - frames = _decode_video(path) - if frames is None: - raise SystemExit(f"attack artifact failed to decode: {path}") + frames, digest = decode_video_artifact(path) reading = read(model, frames) record( "attack", carrier=name, attack=attack, - sha256=sha256_file(path), + sha256=digest, + measurement_domain="decoded_artifact", frames=frames.shape[0], bit_accuracy_avg=reading.bit_accuracy, aggregation_matrix=read_aggregation_matrix(model, frames), diff --git a/scripts/visible_positives.py b/scripts/visible_positives.py index 8cba624..366312f 100644 --- a/scripts/visible_positives.py +++ b/scripts/visible_positives.py @@ -13,8 +13,8 @@ CRASH TOLERANCE IS NOT OPTIONAL AT THIS SCALE because results were only written at the end). So this script: * writes every result to JSONL as it arrives -- a kill never costs more than a batch; * is resumable -- an interrupted run skips what it already recorded; - * runs a FRESH pool per batch with a timeout, so one poisoned file costs one batch, - and that batch is retried serially to find and record the offender. + * runs each image in a fresh interpreter with a per-image timeout and bounded + concurrency; a poisoned file is recorded without retrying it in the parent. Treat input datasets as sensitive and read-only, and keep output gitignored. @@ -29,12 +29,12 @@ import glob import json import os import sys -from concurrent.futures import ProcessPoolExecutor, as_completed -from concurrent.futures import TimeoutError as FutureTimeout -from concurrent.futures.process import BrokenProcessPool from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) +sys.path.insert(0, str(Path(__file__).parent)) + +from _isolated_image_workers import run_batch # The package's own format set. An inlined copy here silently skipped .heif, which # CLAUDE.md documents as supported. @@ -60,20 +60,8 @@ def _one(path: str) -> dict[str, object]: def _run_batch(batch: list[str], jobs: int, timeout: int) -> list[dict[str, object]]: - """One batch in a fresh pool. On a native worker crash or timeout, retry serially so - the poisoned file is identified and recorded instead of stalling the whole sweep.""" - try: - with ProcessPoolExecutor(max_workers=jobs) as ex: - futs = {ex.submit(_one, p): p for p in batch} - return [f.result() for f in as_completed(futs, timeout=timeout)] - except (FutureTimeout, BrokenProcessPool, OSError, RuntimeError): - out: list[dict[str, object]] = [] - for p in batch: - try: - out.append(_one(p)) - except BaseException: # a native crash here kills only this file, not the sweep - out.append({"path": p, "keys": [], "status": "crashed"}) - return out + """Bound each image in an independent interpreter, including native failures.""" + return run_batch(Path(__file__), batch, jobs=jobs, timeout=timeout) def main() -> None: @@ -81,7 +69,7 @@ def main() -> None: ap.add_argument("--jobs", type=int, default=max(1, (os.cpu_count() or 4) - 2)) ap.add_argument("--limit", type=int, default=0) ap.add_argument("--batch", type=int, default=200) - ap.add_argument("--timeout", type=int, default=600, help="seconds per batch before falling back to serial") + ap.add_argument("--timeout", type=int, default=600, help="seconds per image before terminating its isolated worker") ap.add_argument("--out", type=Path, default=OUT) ap.add_argument("--restart", action="store_true") a = ap.parse_args() diff --git a/scripts/visible_recall_sample.py b/scripts/visible_recall_sample.py index 21547f2..54c0edc 100644 --- a/scripts/visible_recall_sample.py +++ b/scripts/visible_recall_sample.py @@ -24,6 +24,7 @@ from __future__ import annotations import argparse import csv +import hashlib import json import random from pathlib import Path @@ -64,6 +65,16 @@ def corner_strip(img: NDArray[Any]) -> NDArray[Any] | None: return strip +def unique_content_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Keep one row per file digest, independently of detector observations.""" + unique: dict[str, dict[str, Any]] = {} + for row in rows: + with Path(row["path"]).open("rb") as source: + digest = hashlib.file_digest(source, "sha256").hexdigest() + unique.setdefault(digest, row) + return list(unique.values()) + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("scan", type=Path, help="JSONL corpus scan produced by the visible evaluation harness") @@ -78,10 +89,7 @@ def main() -> None: out.mkdir(parents=True, exist_ok=True) recs = [json.loads(line) for line in scan.open() if '"marks"' in line] - seen: dict[tuple, dict] = {} - for r in recs: # exact-duplicate uploads share the whole NCC vector - seen.setdefault(tuple(r.get("shape", ())) + tuple(sorted((k, m["conf"]) for k, m in r["marks"].items())), r) - uniq = list(seen.values()) + uniq = unique_content_rows(recs) tc = [r for r in uniq if r["cls"] == "tc260"] goog = [r for r in uniq if r["cls"] == "neg" and "Google" in r.get("platform", "")] diff --git a/scripts/watermark_benchmark.py b/scripts/watermark_benchmark.py index cfdc21f..f965549 100644 --- a/scripts/watermark_benchmark.py +++ b/scripts/watermark_benchmark.py @@ -523,7 +523,7 @@ def _decode_video(path: Path) -> NDArray[Any] | None: if probe.returncode != 0: return None try: - height, width = (int(value) for value in probe.stdout.strip().split(",")) + width, height = (int(value) for value in probe.stdout.strip().split(",")) except ValueError: return None result = subprocess.run( # noqa: S603 - resolved ffmpeg with fixed arguments @@ -546,6 +546,8 @@ def _decode_video(path: Path) -> NDArray[Any] | None: ) if result.returncode != 0 or not result.stdout: return None + if height <= 0 or width <= 0: + return None frame_size = height * width * 3 if len(result.stdout) % frame_size != 0: return None @@ -553,6 +555,17 @@ def _decode_video(path: Path) -> NDArray[Any] | None: return frames.astype(np.float32) / 255.0 +def decode_video_artifact(path: Path) -> tuple[NDArray[Any], str]: + """Decode saved bytes and bind the measurement to their unchanged digest.""" + digest = sha256_file(path) + frames = _decode_video(path) + if frames is None: + raise ValueError(f"video artifact failed to decode: {path}") + if sha256_file(path) != digest: + raise ValueError(f"video artifact changed during decoding: {path}") + return frames, digest + + def _detect_audioseal(path: Path, samples: NDArray[Any]) -> str | None: """Read the pinned AudioSeal oracle; the label is the decoded message.""" from audioseal_oracle import detect_sample_array diff --git a/scripts/watermark_benchmark_audio_cohort.py b/scripts/watermark_benchmark_audio_cohort.py index 4b36f8e..8ba2592 100755 --- a/scripts/watermark_benchmark_audio_cohort.py +++ b/scripts/watermark_benchmark_audio_cohort.py @@ -37,7 +37,10 @@ sys.path.insert(0, str(REPO / "scripts")) import audioseal_oracle # noqa: E402 from audioseal_experiment import ( # noqa: E402 FFMPEG_ATTACKS, + MESSAGE_SEED, + NOISE_ATTACK_SEED, SAMPLE_RATE, + carrier_seed, decode_audio_f32, ffmpeg_attack, ffmpeg_version, @@ -52,6 +55,7 @@ log = logging.getLogger("watermark_benchmark_audio_cohort") RECIPE_VERSION = "watermark-benchmark-audio-cohort-v2" FORGED_MESSAGE_SEED = 8 +HARD_NEGATIVE_SEED = 20260907 + 991 # Carriers the 10 dB additive-noise transform measurably takes below the # audioseal decision rule; the synthetic noise carriers survive it. REMOVED_BY_NOISE: tuple[str, ...] = ("tone_stack",) @@ -102,7 +106,7 @@ def benchmark_row( transform_name: str, transform_revision: str, parameters: dict[str, object], - seed: int, + seed: int | None, expected: str, ) -> dict[str, object]: """Build one strict benchmark-manifest row for a generated artifact.""" @@ -176,7 +180,8 @@ def build_cohort( rows: list[dict[str, object]] = [] for name in carriers: - clean = synth_carrier(name, duration_s) + executed_seed = carrier_seed(name) + clean = synth_carrier(name, duration_s, seed=executed_seed) clean_path = artifacts / f"{name}-clean.wav" clean_path.write_bytes(wav_pcm16_bytes(clean, SAMPLE_RATE)) @@ -201,7 +206,7 @@ def build_cohort( "sample_rate": SAMPLE_RATE, "dependencies": dependencies, }, - seed=20260907, + seed=executed_seed, expected="not_detected", ) ) @@ -222,7 +227,7 @@ def build_cohort( "alpha": 1.0, "dependencies": dependencies, }, - seed=7, + seed=MESSAGE_SEED, expected="detected", ) ) @@ -241,7 +246,7 @@ def build_cohort( transform_name=f"attack-{attack}", transform_revision=RECIPE_VERSION, parameters=attack_parameters(attack, dependencies), - seed=7, + seed=NOISE_ATTACK_SEED if attack == "noise_snr10" else None, expected="unresolved", ) ) @@ -262,14 +267,13 @@ def build_cohort( transform_name="remove-noise-snr10", transform_revision=RECIPE_VERSION, parameters=attack_parameters("noise_snr10", dependencies), - seed=7, + seed=NOISE_ATTACK_SEED, expected="not_detected", ) ) # The forged row embeds a different message into the same clean - # carrier: a watermark is present, just not the oracle's, and the - # matched verifier's correct answer is not_detected. + # carrier: a watermark is present, with a foreign decoded payload. forged_message = message_bits(FORGED_MESSAGE_SEED) forged_samples = np.asarray(audioseal_oracle.embed(generator, clean, forged_message), dtype=np.float32) forged_path = artifacts / f"{name}-forged.wav" @@ -321,7 +325,7 @@ def build_cohort( "sample_rate": SAMPLE_RATE, "dependencies": dependencies, }, - seed=20260907 + 991, + seed=HARD_NEGATIVE_SEED, expected="not_detected", ) ) @@ -390,7 +394,7 @@ def _append_speech_rows( transform_name="synthesize-speech", transform_revision=RECIPE_VERSION, parameters={"tts": tts, "sample_rate": SAMPLE_RATE, "dependencies": dependencies}, - seed=20260907, + seed=None, expected="not_detected", ) ) @@ -412,7 +416,7 @@ def _append_speech_rows( "tts": tts, "dependencies": dependencies, }, - seed=7, + seed=MESSAGE_SEED, expected="detected", ) ) @@ -430,7 +434,7 @@ def _append_speech_rows( transform_name=f"attack-{attack}", transform_revision=RECIPE_VERSION, parameters={**attack_parameters(attack, dependencies), "tts": tts}, - seed=7, + seed=NOISE_ATTACK_SEED if attack == "noise_snr10" else None, expected="unresolved", ) ) @@ -438,7 +442,7 @@ def _append_speech_rows( def synth_carrier_hard_negative(duration_s: float) -> np.ndarray: """A never-embedded white-noise carrier keyed off a distinct seed.""" - rng = np.random.default_rng(20260907 + 991) + rng = np.random.default_rng(HARD_NEGATIVE_SEED) n = int(duration_s * SAMPLE_RATE) return (rng.normal(0.0, 0.1, n)).astype(np.float32) diff --git a/scripts/watermark_benchmark_report.py b/scripts/watermark_benchmark_report.py index 80f654b..fa16a5b 100755 --- a/scripts/watermark_benchmark_report.py +++ b/scripts/watermark_benchmark_report.py @@ -411,6 +411,7 @@ def _fidelity_groups(records: Sequence[ResultRecord]) -> list[dict[str, Any]]: counts = {"measured": 0, "incomparable": 0, "not_measured": 0, "inconsistent": 0} identical = 0 psnr_values: list[float] = [] + mean_psnr_values: list[float] = [] snr_values: list[float] = [] for key, (_, value) in pairs.items(): if key in inconsistent_pairs: @@ -426,6 +427,9 @@ def _fidelity_groups(records: Sequence[ResultRecord]) -> list[dict[str, Any]]: psnr = value.get("psnr_db") if isinstance(psnr, (int, float)) and not isinstance(psnr, bool) and math.isfinite(psnr): psnr_values.append(float(psnr)) + mean_psnr = value.get("mean_psnr_db") + if isinstance(mean_psnr, (int, float)) and not isinstance(mean_psnr, bool) and math.isfinite(mean_psnr): + mean_psnr_values.append(float(mean_psnr)) snr = value.get("snr_db") if isinstance(snr, (int, float)) and not isinstance(snr, bool) and math.isfinite(snr): snr_values.append(float(snr)) @@ -438,6 +442,8 @@ def _fidelity_groups(records: Sequence[ResultRecord]) -> list[dict[str, Any]]: "identical": identical, "finite_psnr": len(psnr_values), "p50_psnr_db": nearest_rank(sorted(psnr_values), 0.50) if psnr_values else None, + "finite_mean_psnr": len(mean_psnr_values), + "p50_mean_psnr_db": nearest_rank(sorted(mean_psnr_values), 0.50) if mean_psnr_values else None, "finite_snr": len(snr_values), "p50_snr_db": nearest_rank(sorted(snr_values), 0.50) if snr_values else None, } @@ -743,6 +749,8 @@ def render_markdown(summary: Mapping[str, Any], *, title: str = "Watermark bench ("inconsistent", "Inconsistent"), ("finite_psnr", "Finite PSNR"), ("p50_psnr_db", "p50 PSNR dB"), + ("finite_mean_psnr", "Finite video mean PSNR"), + ("p50_mean_psnr_db", "p50 video mean PSNR dB"), ("finite_snr", "Finite SNR"), ("p50_snr_db", "p50 SNR dB"), ), diff --git a/scripts/watermark_benchmark_video_cohort.py b/scripts/watermark_benchmark_video_cohort.py index db036c9..b66a659 100755 --- a/scripts/watermark_benchmark_video_cohort.py +++ b/scripts/watermark_benchmark_video_cohort.py @@ -89,9 +89,9 @@ def _carrier_seed(name: str) -> int: return CARRIER_SEED + zlib.crc32(name.encode()) % 1000 -def synth_carrier(name: str, *, seed_offset: int = 0) -> np.ndarray: +def synth_carrier(name: str, *, seed_offset: int = 0, seed: int | None = None) -> np.ndarray: """Render one deterministic clip as float RGB frames in [0, 1].""" - rng = np.random.default_rng(_carrier_seed(name) + seed_offset) + rng = np.random.default_rng(_carrier_seed(name) + seed_offset if seed is None else seed) frames = np.empty((FRAME_COUNT, HEIGHT, WIDTH, 3), dtype=np.float32) axis = np.arange(WIDTH, dtype=np.float32)[None, :] rows = np.arange(HEIGHT, dtype=np.float32)[:, None] @@ -192,7 +192,7 @@ def benchmark_row( transform_name: str, transform_revision: str, parameters: dict[str, object], - seed: int, + seed: int | None, expected: str, ) -> dict[str, object]: return { @@ -242,7 +242,8 @@ def build_cohort( rows: list[dict[str, object]] = [] for name in carriers: - clean = synth_carrier(name) + executed_seed = _carrier_seed(name) + clean = synth_carrier(name, seed=executed_seed) clean_path = encode_clip(ffmpeg, artifacts / f"{name}-clean.mp4", clean) marked_frames = np.asarray(videoseal_oracle.embed(model, clean, message), dtype=np.float32) marked_path = encode_clip(ffmpeg, artifacts / f"{name}-marked.mp4", marked_frames) @@ -260,7 +261,7 @@ def build_cohort( transform_name="synthesize-clip", transform_revision=RECIPE_VERSION, parameters={"carrier": name, **geometry, "dependencies": dependencies}, - seed=_carrier_seed(name), + seed=executed_seed, expected="not_detected", ) ) @@ -281,7 +282,7 @@ def build_cohort( **geometry, "dependencies": dependencies, }, - seed=7, + seed=videoseal_oracle.MESSAGE_SEED, expected="detected", ) ) @@ -299,7 +300,7 @@ def build_cohort( transform_name=f"attack-{attack}", transform_revision=RECIPE_VERSION, parameters=attack_parameters(attack, dependencies), - seed=7, + seed=None, expected="unresolved", ) ) @@ -320,7 +321,7 @@ def build_cohort( transform_name="remove-h264-crf23", transform_revision=RECIPE_VERSION, parameters=attack_parameters("h264_crf23", dependencies), - seed=7, + seed=None, expected="not_detected", ) ) @@ -351,7 +352,8 @@ def build_cohort( ) ) - hard = synth_carrier("moving_texture", seed_offset=991) + hard_seed = _carrier_seed("moving_texture") + 991 + hard = synth_carrier("moving_texture", seed=hard_seed) hard_path = encode_clip(ffmpeg, artifacts / f"{HARD_NEGATIVE_CARRIER}.mp4", hard) rows.append( benchmark_row( @@ -365,7 +367,7 @@ def build_cohort( transform_name="synthesize-clip", transform_revision=RECIPE_VERSION, parameters={"carrier": "moving_texture", "seed_offset": 991, "dependencies": dependencies}, - seed=_carrier_seed(HARD_NEGATIVE_CARRIER) + 991, + seed=hard_seed, expected="not_detected", ) ) diff --git a/scripts/watermark_forgery_study.py b/scripts/watermark_forgery_study.py index f1339fc..6e431d1 100644 --- a/scripts/watermark_forgery_study.py +++ b/scripts/watermark_forgery_study.py @@ -134,6 +134,7 @@ def audio_cells(out_dir: Path) -> list[dict[str, object]]: def video_cells(out_dir: Path, ffmpeg: str) -> list[dict[str, object]]: + from watermark_benchmark import decode_video_artifact from watermark_benchmark_video_cohort import encode_clip model = videoseal_oracle.load_model() @@ -143,7 +144,8 @@ def video_cells(out_dir: Path, ffmpeg: str) -> list[dict[str, object]]: workdir = out_dir / "videoseal" workdir.mkdir() - def measure(label: str, frames: np.ndarray, path: Path) -> dict[str, object]: + def measure(label: str, path: Path) -> np.ndarray: + frames, digest = decode_video_artifact(path) reading = videoseal_oracle.read(model, frames, message=message_a) row: dict[str, object] = { "scheme": "videoseal", @@ -151,11 +153,12 @@ def video_cells(out_dir: Path, ffmpeg: str) -> list[dict[str, object]]: "bit_accuracy_vs_a": round(reading.bit_accuracy, 6), "bit_accuracy_vs_b": round(bit_accuracy(reading.decoded_bits, message_b), 6), "detected_by_matched_rule": reading.detected, - "sha256": sha256_file(path), + "sha256": digest, + "measurement_domain": "decoded_artifact", } rows.append(row) log.info("%s", json.dumps(row, sort_keys=True)) - return row + return frames for carrier_name, clean in ( ("moving_gradient", synth_video_carrier("moving_gradient")[:CLIP_FRAMES]), @@ -163,32 +166,27 @@ def video_cells(out_dir: Path, ffmpeg: str) -> list[dict[str, object]]: ): prefix = f"{carrier_name}-" clean_path = encode_clip(ffmpeg, workdir / f"{prefix}clean.mp4", clean) - measure(f"{carrier_name}/clean", clean, clean_path) + clean = measure(f"{carrier_name}/clean", clean_path) marked_a = np.asarray(videoseal_oracle.embed(model, clean, message_a), dtype=np.float32) marked_a_path = encode_clip(ffmpeg, workdir / f"{prefix}marked_a.mp4", marked_a) - measure(f"{carrier_name}/marked_a", marked_a, marked_a_path) + marked_a = measure(f"{carrier_name}/marked_a", marked_a_path) removed_path = apply_crf(ffmpeg, marked_a_path, 23, workdir) - from watermark_benchmark import _decode_video - - removed_frames = _decode_video(removed_path) - if removed_frames is None: - raise SystemExit(f"removed artifact failed to decode: {removed_path}") - measure(f"{carrier_name}/removed_crf23", removed_frames, removed_path) + removed_frames = measure(f"{carrier_name}/removed_crf23", removed_path) forged_on_clean = np.asarray(videoseal_oracle.embed(model, clean, message_b), dtype=np.float32) path = encode_clip(ffmpeg, workdir / f"{prefix}forged_b_on_clean.mp4", forged_on_clean) - measure(f"{carrier_name}/forged_b_on_clean", forged_on_clean, path) + measure(f"{carrier_name}/forged_b_on_clean", path) forged_on_marked = np.asarray(videoseal_oracle.embed(model, marked_a, message_b), dtype=np.float32) path = encode_clip(ffmpeg, workdir / f"{prefix}forged_b_on_marked.mp4", forged_on_marked) - measure(f"{carrier_name}/forged_b_on_marked_a", forged_on_marked, path) + measure(f"{carrier_name}/forged_b_on_marked_a", path) if removed_frames.shape[0] == marked_a.shape[0]: forged_on_removed = np.asarray(videoseal_oracle.embed(model, removed_frames, message_b), dtype=np.float32) path = encode_clip(ffmpeg, workdir / f"{prefix}forged_b_on_removed.mp4", forged_on_removed) - measure(f"{carrier_name}/forged_b_on_removed_a", forged_on_removed, path) + measure(f"{carrier_name}/forged_b_on_removed_a", path) rows.append( { diff --git a/skills/.claude-plugin/plugin.json b/skills/.claude-plugin/plugin.json index 5da7846..374d525 100644 --- a/skills/.claude-plugin/plugin.json +++ b/skills/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "remove-ai-watermarks", "displayName": "Remove AI Watermarks", - "version": "1.0.4", + "version": "1.0.5", "description": "Identify and remove AI provenance marks from images and video the user generated or edited, using the remove-ai-watermarks CLI.", "author": { "name": "wiltodelta", diff --git a/skills/remove-ai-watermarks/SKILL.md b/skills/remove-ai-watermarks/SKILL.md index 1cb8fac..af2034a 100644 --- a/skills/remove-ai-watermarks/SKILL.md +++ b/skills/remove-ai-watermarks/SKILL.md @@ -5,7 +5,7 @@ license: Apache-2.0 compatibility: Python 3.11-3.14. Invisible image removal needs NVIDIA CUDA. Some video writes need ffmpeg. Installer may be uv, pipx, or pip. metadata: author: wiltodelta - version: "1.0.4" + version: "1.0.5" homepage: https://raiw.cc repository: https://github.com/wiltodelta/remove-ai-watermarks --- @@ -64,7 +64,9 @@ python /scripts/probe.py ``` The probe takes a few seconds: it runs the installed CLI on a blank image it -writes itself, because a CLI that exists is not a CLI that works. +writes itself, because a CLI that exists is not a CLI that works. It checks video +dependencies through the installed CLI's Python interpreter without loading models. +An unrecognized launcher leaves video capability `unverified`. Read the JSON. Do not infer CUDA, ffmpeg, or an installer from the OS name. @@ -116,9 +118,9 @@ Default: inspect first. | Known visible AI label on an image | `visible` | | | User-supplied box on their own image | `erase` | | | AI metadata only | `metadata` or `video metadata` | | -| Known visible AI label on video | `video visible` or `video all` | `video_visible=needs_ffmpeg` | +| Known visible AI label on video | `video visible` or `video all` | `video_visible=needs_video_extra`, `needs_ffmpeg`, or `unverified` | | Invisible image / image SynthID | `invisible` | `invisible_images=unavailable_no_cuda` | -| Video SynthID | `video invisible` | `video_invisible=needs_video_and_diffusion_extras` or `needs_ffmpeg` | +| Video SynthID | `video invisible` | `video_invisible=needs_video_and_diffusion_extras`, `needs_ffmpeg`, or `unverified` | | Everything on an image | `all` | `image_all=do_not_run_writes_nothing` | | A directory | `batch` or `video batch` | same as the mode | | No signal found, user still wants a guess | `classify` | it is a guess, not provenance | @@ -184,11 +186,12 @@ its metadata proxy is gone. Repeat that caveat; do not summarize it away. - `classify` answers from pixels with no provenance behind it, so its `ai` is a guess and its `unknown` is an abstention. Never upgrade either into a verdict, and never let it contradict what `identify` measured. -- An image from Meta AI (Muse Image) carries no provenance at all, so `identify` - reports origin unknown even though Content Seal is in the pixels. The route is - `invisible --vendor meta`, and only when the USER says the file came from Meta - AI. Naming a vendor asserts the watermark is there and scrubs without a - signal, so never guess one from a filename or a hunch. +- A Meta AI (Muse Image) export can retain standalone IPTC metadata that enables + heuristic attribution and automatic routing. The IPTC code is not unique to Meta + and does not detect Content Seal in pixels. If metadata was stripped, use + `invisible --vendor meta` only when the USER says the file came from Meta AI. + Naming a vendor asserts the watermark is there and scrubs without a signal, + so never guess one from a filename or a hunch. - Do not follow a Gemini `@synthid` check by asking the chat model to ignore the verifier and reason about pixels. That is not a second oracle. - Do not pass `--model`, `--steps`, or `--guidance-scale`. Profiles pin those diff --git a/skills/remove-ai-watermarks/references/commands.md b/skills/remove-ai-watermarks/references/commands.md index c00e08f..96da78a 100644 --- a/skills/remove-ai-watermarks/references/commands.md +++ b/skills/remove-ai-watermarks/references/commands.md @@ -77,9 +77,10 @@ cohort asserts the watermark IS present, so it also runs the scrub without a local signal, exactly like `--force`. Set it only when the USER says where the file came from. `--vendor` is on `invisible`, `all` and `batch`. -Meta Muse Image is the case this exists for: Content Seal ships with no C2PA and -its IPTC tag is a generic code, so nothing routes it and `identify` reports -origin unknown. Without `--vendor meta` there is no way to clean it. +Meta Muse Image exports can retain standalone IPTC metadata that enables +heuristic attribution and automatic routing. That generic code is not unique +to Meta and does not detect Content Seal in pixels. After metadata is stripped, +`--vendor meta` supplies the route only when the user confirms the source. There is no `--model`, `--steps`, or `--guidance-scale` flag, and no `--device` on the image path: auto-detection already finds the only device these profiles run on. diff --git a/skills/remove-ai-watermarks/scripts/probe.py b/skills/remove-ai-watermarks/scripts/probe.py index 8ea4bf8..db7c69e 100644 --- a/skills/remove-ai-watermarks/scripts/probe.py +++ b/skills/remove-ai-watermarks/scripts/probe.py @@ -11,6 +11,7 @@ import argparse import json import os import re +import shlex import shutil import struct import subprocess @@ -161,6 +162,72 @@ def _invisible_stack(cli_path: str) -> str: return "installed" if code == 2 else "unknown" +def _video_stacks(cli_path: str) -> dict[str, str]: + """Ask the CLI's own Python runtime, without constructing or downloading a model. + + pip/uv entry points name their interpreter. Unrecognized launchers (including + native Windows executables) remain unverified; this probe's Python is never a + substitute for the environment running the installed CLI. + """ + unknown = {"pixels": "unknown", "invisible": "unknown"} + try: + with Path(cli_path).open(encoding="utf-8") as stream: + lines = [stream.readline() for _ in range(3)] + command = shlex.split(lines[0][2:].strip()) if lines[0].startswith("#!") else [] + if command == ["/bin/sh"] and lines[1].startswith("'" * 3 + "exec' "): + command = shlex.split(lines[1][len("'" * 3 + "exec' ") :]) + if command[-2:] != ["$0", "$@"]: + return unknown + command = command[:-2] + if ( + len(command) != 1 + or not Path(command[0]).is_absolute() + or not re.fullmatch(r"python(?:[0-9]+(?:\.[0-9]+)?)?", Path(command[0]).name) + ): + return unknown + except (OSError, UnicodeError, ValueError): + return unknown + code, stdout, _stderr = _run([command[0], "-c", _VIDEO_RUNTIME_CHECK], timeout=60.0) + if code != 0: + return unknown + try: + result = json.loads(stdout) + except ValueError: + return unknown + if not isinstance(result, dict): + return unknown + return { + "pixels": result.get("pixels") if result.get("pixels") in {"ok", "missing"} else "unknown", + "invisible": result.get("invisible") if result.get("invisible") in {"installed", "missing"} else "unknown", + } + + +_VIDEO_RUNTIME_CHECK = """ +import json +import sys +result = {"pixels": "unknown", "invisible": "unknown"} +try: + from remove_ai_watermarks.video import _require_video_runtime + _require_video_runtime() + import av + result["pixels"] = "ok" +except RuntimeError: + result["pixels"] = "missing" +except ImportError: + result["pixels"] = "missing" +try: + from remove_ai_watermarks.video_invisible import is_available + if is_available(): + from diffusers import AutoencoderKL + result["invisible"] = "installed" + else: + result["invisible"] = "missing" +except ImportError: + result["invisible"] = "missing" +sys.stdout.write(json.dumps(result)) +""" + + def _has_cuda() -> bool: exe = _which("nvidia-smi") if exe is None: @@ -208,6 +275,7 @@ def _advice( installer: str | None, outdated: bool | None, invisible: str, + video: dict[str, str], ) -> dict[str, str]: host = "https://raiw.cc" if not cli_found: @@ -228,18 +296,30 @@ def _advice( # video routes. Reporting pixel commands as "ok" is what sent an agent into a # missing-cv2 crash on a machine the probe had just called ready. no_pixels = pixels == "missing" + if no_pixels or video["pixels"] == "missing": + video_visible = "needs_video_extra" + elif video["pixels"] != "ok": + video_visible = "unverified" + else: + video_visible = "ok" if ffmpeg else "needs_ffmpeg" + + if no_pixels or "missing" in video.values(): + video_invisible = "needs_video_and_diffusion_extras" + elif video != {"pixels": "ok", "invisible": "installed"}: + video_invisible = "unverified" + else: + video_invisible = "ok_cpu_or_gpu" if ffmpeg else "needs_ffmpeg" + advice = { "identify": "metadata_only_no_pixels" if no_pixels else "ok", "visible": "needs_visible_extra" if no_pixels else "ok", "metadata": "ok", "video_identify": "metadata_only_no_pixels" if no_pixels else "ok", "video_metadata": "ok", - "video_visible": "needs_video_extra" if no_pixels else ("ok" if ffmpeg else "needs_ffmpeg"), + "video_visible": video_visible, "invisible_images": "unavailable_no_cuda" if not cuda else ("needs_visible_extra" if no_pixels else "ok"), "image_all": _image_all(no_pixels=no_pixels, cuda=cuda, invisible=invisible), - "video_invisible": ( - "needs_video_and_diffusion_extras" if no_pixels else ("ok_cpu_or_gpu" if ffmpeg else "needs_ffmpeg") - ), + "video_invisible": video_invisible, "no_cuda_fallback": host, } if no_pixels: @@ -271,6 +351,7 @@ def build_report() -> dict[str, Any]: ffmpeg = _which("ffmpeg") is not None pixels = _pixel_stack(str(cli["path"])) if cli["found"] else "unknown" invisible = _invisible_stack(str(cli["path"])) if cli["found"] else "unknown" + video = _video_stacks(str(cli["path"])) if cli["found"] else {"pixels": "unknown", "invisible": "unknown"} return { "os": os.name, "platform": sys.platform, @@ -280,6 +361,7 @@ def build_report() -> dict[str, Any]: "ffmpeg": ffmpeg, "pixel_stack": pixels, "invisible_stack": invisible, + "video_stacks": video, "installers": {name: path is not None for name, path in installers.items()}, "preferred_installer": installer, "advice": _advice( @@ -290,6 +372,7 @@ def build_report() -> dict[str, Any]: installer=installer, outdated=cli["outdated"], invisible=invisible, + video=video, ), } diff --git a/src/remove_ai_watermarks/_internal/c2pa.py b/src/remove_ai_watermarks/_internal/c2pa.py index 52708de..4eae085 100644 --- a/src/remove_ai_watermarks/_internal/c2pa.py +++ b/src/remove_ai_watermarks/_internal/c2pa.py @@ -88,7 +88,7 @@ def reader_available() -> bool: return _C2PA_READER_AVAILABLE -def _manifest_json_uncached(path: str) -> str | None: +def _manifest_json_uncached(path: str, *, strict: bool = False) -> str | None: """The manifest store as JSON, or None when this file has no readable manifest. Two outcomes are routine and stay at debug: a file with no manifest (``try_create`` @@ -105,6 +105,8 @@ def _manifest_json_uncached(path: str) -> str | None: return None except Exception as error: logger.warning("C2PA reader failed to open %s: %s: %s", path, type(error).__name__, error) + if strict: + raise return None if reader is None: return None @@ -115,23 +117,30 @@ def _manifest_json_uncached(path: str) -> str | None: # The reader opened the file, so a manifest is there; failing to serialize it # is never routine. logger.warning("C2PA reader could not serialize %s: %s: %s", path, type(error).__name__, error) + if strict: + raise return None @functools.lru_cache(maxsize=8) -def _manifest_json_cached(path: str, _mtime_ns: int) -> str | None: - return _manifest_json_uncached(path) +def _manifest_json_cached(path: str, _mtime_ns: int, *, strict: bool = False) -> str | None: + return _manifest_json_uncached(path, strict=strict) -def read_manifest_store_json(image_path: Path) -> str | None: - """Read the complete manifest-store JSON, caching it until the file changes.""" +def read_manifest_store_json(image_path: Path, *, strict: bool = False) -> str | None: + """Read the manifest store, optionally raising unexpected reader failures. + + Strict reads have a separate cache key: a tolerant failure cached as ``None`` + must never become evidence that a later strict collection completed. + """ if not reader_available(): return None path = str(image_path) try: - return _manifest_json_cached(path, image_path.stat().st_mtime_ns) + mtime_ns = image_path.stat().st_mtime_ns except OSError: - return _manifest_json_uncached(path) + return _manifest_json_uncached(path, strict=strict) + return _manifest_json_cached(path, mtime_ns, strict=strict) def _find_c2pa_chunk(path: Path) -> _PngChunk | None: diff --git a/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py b/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py index 48e8710..47dcf87 100644 --- a/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py @@ -7,10 +7,10 @@ change there cannot silently diverge between the profiles. Pieces bound to this architecture: the diffusers ``ChromaImg2ImgPipeline`` (no DiffSynth here), the neutral faithful-regeneration prompt the floors were -calibrated with, guidance 5.0, and the step-count compensation -- diffusers -truncates the step COUNT (``int(steps * strength)``), so the requested count is -scaled to always spend four effective denoising steps, the same semantics as -``sdxl_zimage_pipeline.requested_steps``. Strength is bound to it too: the flat +calibrated with, guidance 5.0, and ``ceil(4 / strength)`` requested steps. Chroma +truncates the skipped interval, so this calibrated schedule executes four or +five steps depending on strength; SDXL rounds differently. Strength is bound +to the request schedule too: the flat vendor floors in ``watermark_profiles`` come from the 2026-08-29/30 oracle calibration (docs/chroma1-engine-research.md) and do not transfer to another prompt, guidance, or effective step count. @@ -25,7 +25,6 @@ img2img pass, so that is the calibrated path. from __future__ import annotations import logging -import math from dataclasses import dataclass from typing import Any, ClassVar @@ -36,17 +35,19 @@ from remove_ai_watermarks._internal.two_stage_pipeline import ( _load_prompt_payload, _prompt_cache_path, _store_prompt_payload, + _target_size, edge_pad_to_grid, + requested_steps, ) log = logging.getLogger(__name__) CHROMA_MODEL_ID = "lodestones/Chroma1-HD" -# The Chroma1 global stage spends four effective denoising steps at every -# strength. The calibration ladder (docs/chroma1-engine-research.md) held this -# fixed; a different count is a different engine. -CHROMA_STEPS = 4 +# Calibration used ceil(4 / strength) requested steps. Chroma truncates the +# skipped count, so some strengths execute five steps rather than four. +# Preserve this request schedule: changing it invalidates the measured floors. +CHROMA_NOMINAL_STEPS = 4 CHROMA_GUIDANCE = 5.0 # The neutral faithful-regeneration prompt the floors were measured with. It is @@ -67,21 +68,9 @@ _CHROMA_PROMPT_CACHE_KEY = (CHROMA_MODEL_ID, ("chroma1-prompt-embeds-v1",), CHRO _LATENT_GRID = 16 -def requested_steps(effective_steps: int, strength: float) -> int: - """Spend ``effective_steps`` regardless of Diffusers' step-count truncation. - - Diffusers img2img truncates the step COUNT (``init_timestep = int(steps * - strength)``), so at the floors this profile uses a naive request would run - zero or one steps. Scale the request so the effective count is exact. - """ - return max(1, math.ceil(effective_steps / max(float(strength), 1e-6))) - - def chroma_target_size(width: int, height: int) -> tuple[int, int]: """Floor dimensions to the latent grid without changing aspect.""" - return max(_LATENT_GRID, (width // _LATENT_GRID) * _LATENT_GRID), max( - _LATENT_GRID, (height // _LATENT_GRID) * _LATENT_GRID - ) + return _target_size(width, height, _LATENT_GRID) @dataclass @@ -161,8 +150,8 @@ class ChromaZImagePipeline(TwoStageZImagePipeline): pipe = self._load_global() target = chroma_target_size(image.width, image.height) prepared = image if image.size == target else image.resize(target, Image.Resampling.LANCZOS) - steps = requested_steps(CHROMA_STEPS, strength) - self._progress(f"Running Chroma1 pass: strength={strength:.4f}, steps={CHROMA_STEPS} of {steps}...") + steps = requested_steps(CHROMA_NOMINAL_STEPS, strength) + self._progress(f"Running Chroma1 pass: strength={strength:.4f}, requested steps={steps}...") generator = torch.Generator(device=self.device).manual_seed(seed) if seed is not None else None # Pass cached embeddings when available to skip the T5 inference. diff --git a/src/remove_ai_watermarks/_internal/isobmff.py b/src/remove_ai_watermarks/_internal/isobmff.py index 86f440f..c8848b3 100644 --- a/src/remove_ai_watermarks/_internal/isobmff.py +++ b/src/remove_ai_watermarks/_internal/isobmff.py @@ -300,8 +300,8 @@ def _tc260_aigc_regions( return regions -def tc260_aigc_payloads(path: str | Path) -> tuple[bytes, ...]: - """Read native TC260 ``AIGC`` JSON values from an MP4/MOV container.""" +def tc260_aigc_payloads(path: str | Path, *, strict: bool = False) -> tuple[bytes, ...]: + """Read native TC260 ``AIGC`` values, optionally propagating I/O failures.""" try: with open(path, "rb") as stream: if not is_isobmff(stream.read(8)): @@ -310,6 +310,8 @@ def tc260_aigc_payloads(path: str | Path) -> tuple[bytes, ...]: file_size = stream.tell() return tuple(region[3] for region in _tc260_aigc_regions(stream, file_size)) except OSError: + if strict: + raise return () @@ -342,7 +344,7 @@ def is_isobmff(data: bytes) -> bool: return len(data) >= 8 and data[4:8] == b"ftyp" -def scan_c2pa_region(path: str | Path, *, max_total: int = 4 * 1024 * 1024) -> bytes: +def scan_c2pa_region(path: str | Path, *, max_total: int = 4 * 1024 * 1024, strict: bool = False) -> bytes: """Concatenated payloads of top-level ``uuid`` / ``jumb`` boxes in an ISOBMFF file, found by seeking past other boxes (``mdat`` etc.) by size. @@ -352,7 +354,7 @@ def scan_c2pa_region(path: str | Path, *, max_total: int = 4 * 1024 * 1024) -> b walks box headers (8-16 bytes each) and seeks past payloads it does not need, so it never loads ``mdat`` into memory and works on multi-GB files. Returns the relevant box payloads (capped at ``max_total``), or ``b""`` for a - non-ISOBMFF file or on any read error. + non-ISOBMFF file or on a read error unless ``strict=True``. """ collected = bytearray() try: @@ -392,6 +394,8 @@ def scan_c2pa_region(path: str | Path, *, max_total: int = 4 * 1024 * 1024) -> b collected += f.read(to_read) pos += size except OSError: + if strict: + raise return b"" return bytes(collected) diff --git a/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py b/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py index e85e21d..f0b3b37 100644 --- a/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py @@ -17,7 +17,6 @@ at the strength Qwen needs. See ``watermark_profiles.SDXL_ZIMAGE_OPENAI_STRENGTH from __future__ import annotations import logging -import math from dataclasses import dataclass from typing import Any, ClassVar @@ -27,7 +26,9 @@ from remove_ai_watermarks._internal.two_stage_pipeline import ( _GLOBAL_NEGATIVE, _GLOBAL_PROMPT, TwoStageZImagePipeline, + _target_size, build_canny_control_image, + requested_steps, ) from remove_ai_watermarks._internal.watermark_profiles import ( CONTROLNET_CANNY_MODEL, @@ -49,22 +50,7 @@ SDXL_STEPS = 4 def sdxl_target_size(width: int, height: int) -> tuple[int, int]: """Floor dimensions to SDXL's latent grid without changing aspect.""" - return max(_LATENT_GRID, (width // _LATENT_GRID) * _LATENT_GRID), max( - _LATENT_GRID, (height // _LATENT_GRID) * _LATENT_GRID - ) - - -def requested_steps(effective_steps: int, strength: float) -> int: - """Translate "spend N denoising steps" into what Diffusers has to be asked for. - - The two runtimes truncate differently and it is easy to port this wrong. - DiffSynth sets ``sigma_start = denoising_strength`` and then runs *every* - requested step across the shortened sigma range. Diffusers img2img instead - truncates the step *count* (``init_timestep = int(steps * strength)``), so - asking it for four steps at strength 0.15 runs **zero** and returns nothing but - a VAE round-trip. Ask for enough that ``effective_steps`` actually execute. - """ - return max(1, math.ceil(effective_steps / max(float(strength), 1e-6))) + return _target_size(width, height, _LATENT_GRID) @dataclass diff --git a/src/remove_ai_watermarks/_internal/two_stage_pipeline.py b/src/remove_ai_watermarks/_internal/two_stage_pipeline.py index 4150d7e..aa5ce9b 100644 --- a/src/remove_ai_watermarks/_internal/two_stage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/two_stage_pipeline.py @@ -15,6 +15,7 @@ from __future__ import annotations import contextlib import hashlib import logging +import math import os import tempfile import urllib.request @@ -300,9 +301,21 @@ def largest_face_denoise( return _clamp(scaled, denoise_min, denoise_max) -def _target_size(width: int, height: int) -> tuple[int, int]: - """Floor image dimensions to the /16 latent grid without changing aspect.""" - return max(16, (width // 16) * 16), max(16, (height // 16) * 16) +def requested_steps(effective_steps: int, strength: float) -> int: + """Scale a nominal denoising budget by strength for Diffusers img2img. + + SDXL truncates ``steps * strength``. Chroma truncates the skipped count + instead, so the same request can execute one additional step. Both profiles + were calibrated with this request formula; it is not an exact count across + schedulers. DiffSynth instead executes every requested step over a shortened + sigma range and does not use this compensation. + """ + return max(1, math.ceil(effective_steps / max(float(strength), 1e-6))) + + +def _target_size(width: int, height: int, grid: int = 16) -> tuple[int, int]: + """Floor image dimensions to the profile's latent grid.""" + return max(grid, (width // grid) * grid), max(grid, (height // grid) * grid) def _resize_to_target(image: Image.Image) -> Image.Image: diff --git a/src/remove_ai_watermarks/_internal/watermark_profiles.py b/src/remove_ai_watermarks/_internal/watermark_profiles.py index 22150e3..870e1c2 100644 --- a/src/remove_ai_watermarks/_internal/watermark_profiles.py +++ b/src/remove_ai_watermarks/_internal/watermark_profiles.py @@ -113,11 +113,10 @@ QWEN_ZIMAGE_OPENAI_STRENGTH = 0.07675 # measured corpus margin, not a universal InvisMark threshold. QWEN_ZIMAGE_MICROSOFT_STRENGTH = 0.15 -# Meta Muse Image stamps every output with Content Seal, but no provenance signal -# survives to route it: the outputs carry no C2PA, and their IPTC -# trainedAlgorithmicMedia companion tag is a standard code many platforms use, so -# it cannot key this cohort the way an issuer keys the others. The floor is -# therefore selected by an explicit --vendor meta override, never by detection. +# Meta Muse Image's retained standalone IPTC AI tag selects this floor through +# vendor_for_strength. That shared standard tag is a routing heuristic, not a +# vendor-unique signature or a Content Seal pixel detection. Stripped outputs +# need an explicit --vendor meta override when their origin is known. # Derivation (oracle meta.ai/identification, 2026-08-26/27, corpus in # data/contentseal/): five independent 2.56 MP generations bracketed at # lighthouse (0.0525, 0.06], fox (0.03, 0.0375], night_city (0.03, 0.0375], @@ -146,7 +145,7 @@ _SDXL_ZIMAGE_STRENGTH_BY_VENDOR: dict[str, float] = { # chroma-zimage floors, from the 2026-08-29/30 four-cohort oracle calibration # (docs/chroma1-engine-research.md; ChromaImg2ImgPipeline, neutral prompt, -# guidance 5.0, four effective steps, seed 0). Flat per-vendor values derived +# guidance 5.0, ceil(4 / strength) requested steps, seed 0). Flat per-vendor values derived # by the same worst-clean-plus-one-spread rule as the other profiles: # # - OpenAI: the initial three fixtures first cleared at 0.06 / 0.06 / 0.075, diff --git a/src/remove_ai_watermarks/_text_mark_engine.py b/src/remove_ai_watermarks/_text_mark_engine.py index 2ad671b..8ea72fe 100644 --- a/src/remove_ai_watermarks/_text_mark_engine.py +++ b/src/remove_ai_watermarks/_text_mark_engine.py @@ -235,11 +235,12 @@ def glyph_silhouette(asset_name: str) -> NDArray[Any] | None: _RIVAL_MODULES = { "doubao_alpha.png": "remove_ai_watermarks.doubao_engine", "jimeng_alpha.png": "remove_ai_watermarks.jimeng_engine", + "qwen_alpha.png": "remove_ai_watermarks.qwen_engine", "samsung_alpha.png": "remove_ai_watermarks.samsung_engine", } -def _rival_config(asset_name: str, fallback: TextMarkConfig) -> TextMarkConfig: +def _rival_config(asset_name: str) -> TextMarkConfig | None: """The rival mark's own config, for scoring its template on a shared blob. Looked up LAZILY by asset name: a rival's template geometry @@ -249,14 +250,14 @@ def _rival_config(asset_name: str, fallback: TextMarkConfig) -> TextMarkConfig: """ mod_path = _RIVAL_MODULES.get(asset_name) if mod_path is None: - return fallback + return None from importlib import import_module try: return import_module(mod_path)._CONFIG except Exception: # a missing/renamed engine must not break detection logger.debug("rival config %s unavailable; skipping its margin check.", asset_name) - return fallback + return None def _template_match_best( @@ -335,7 +336,9 @@ class TextMarkEngine: if not c.rivals: return True for rival_asset in c.rivals: - rival = _rival_config(rival_asset, c) + rival = _rival_config(rival_asset) + if rival is None: + continue if score - template_match_score(box_mask, scale_base, rival) < c.rival_margin: logger.debug("%s detect: loses the %s rival margin; rejecting.", c.name, rival_asset) return False diff --git a/src/remove_ai_watermarks/api.py b/src/remove_ai_watermarks/api.py index 3351343..29f29c8 100644 --- a/src/remove_ai_watermarks/api.py +++ b/src/remove_ai_watermarks/api.py @@ -152,7 +152,9 @@ def _write_visible_result( # and surfaced as a confusing "cannot read image " naming the OUTPUT path # (Tier E, 2026-07-20). Raise here so a library caller and the CLI both get an # accurate message about the write. - if not image_io.write_bgr_with_alpha(out_path, result, loaded.alpha, display_tags_from=source_path): + if not image_io.write_bgr_with_alpha( + out_path, result, loaded.alpha, display_tags_from=source_path, orientation_applied=False + ): raise OSError(f"failed to write output (is the destination writable?): {out_path}") if strip_metadata: @@ -495,12 +497,25 @@ def remove_all( result, removed = visible.image, visible.labels visible_label = ", ".join(removed) if removed else None say("visible", visible_label or "") - if not image_io.write_bgr_with_alpha(staged, result, alpha): + if not image_io.write_bgr_with_alpha(staged, result, alpha, display_tags_from=src, orientation_applied=False): raise OSError(f"failed to write the staged intermediate: {staged}") # ── 2. Invisible watermark ── outcome = _run_invisible(src, staged, staged, opts, engine, say, evidence, force) + if alpha is not None and outcome == "removed": + # Diffusion bakes EXIF into its RGB pixels. Apply the same transform to + # the original alpha before recombining, including mirrored images. + import numpy as np + from PIL import Image, ImageOps + + with Image.open(src) as original: + orientation = original.getexif().get(274, 1) + if orientation in range(2, 9): + alpha_image = Image.fromarray(alpha) + alpha_image.getexif()[274] = orientation + alpha = np.asarray(ImageOps.exif_transpose(alpha_image)) + # ── 3. AI metadata ── # Read the pristine ORIGINAL for provenance above and the STAGED file here: # the visible pass has already dropped this file's C2PA. @@ -511,12 +526,14 @@ def remove_all( say("metadata", "stripped") # The invisible stage (and the cv2.IMREAD_COLOR paths under it) drops alpha, so - # re-attach the ORIGINAL alpha plane unchanged for transparent formats. + # re-attach the original alpha in the same orientation for transparent formats. final_bgr, _ = image_io.read_bgr_and_alpha(staged) if final_bgr is None: raise OSError(f"failed to read back the staged intermediate: {staged}") out.parent.mkdir(parents=True, exist_ok=True) - if not image_io.write_bgr_with_alpha(out, final_bgr, alpha): + if not image_io.write_bgr_with_alpha( + out, final_bgr, alpha, display_tags_from=staged, orientation_applied=False + ): raise OSError(f"failed to write output (is the destination writable?): {out}") finally: if staged.exists(): @@ -668,7 +685,6 @@ def remove_batch( if progress is not None: progress(path, stage, detail) - processed = failed = 0 unavailable: list[Path] = [] errors: list[tuple[Path, str]] = [] items: list[BatchItemResult] = [] @@ -683,13 +699,11 @@ def remove_batch( # it out so the caller reports it once, with the extra named. if isinstance(exc, ImportError) and not pixels_available(): raise - failed += 1 message = str(exc) errors.append((img_path, message)) items.append(BatchItemResult(img_path, None, mode, error=message)) say(img_path, "failed", message) continue - processed += 1 items.append(result) if result.invisible == "unavailable": unavailable.append(img_path) @@ -697,7 +711,7 @@ def remove_batch( # advances on this and nothing else. Keying it off a mode-specific stage line # left `visible` and `metadata` runs sitting at 0% for the whole batch. say(img_path, "done", result.invisible or "") - return BatchSummary(processed, failed, unavailable, errors, tuple(items)) + return BatchSummary(len(items) - len(errors), len(errors), unavailable, errors, tuple(items)) def _run_batch_one( @@ -757,7 +771,9 @@ def _run_batch_one( provenance=loaded.provenance, backend=backend, ) - if not image_io.write_bgr_with_alpha(out_path, visible.image, loaded.alpha): + if not image_io.write_bgr_with_alpha( + out_path, visible.image, loaded.alpha, display_tags_from=img_path, orientation_applied=False + ): raise OSError(f"failed to write output (is the destination writable?): {out_path}") return BatchItemResult(img_path, out_path, mode, visible.status, visible.marks) @@ -767,8 +783,9 @@ def _run_batch_one( raise MetadataStripIncomplete(set(leftover)) return BatchItemResult(img_path, out_path, mode) - # invisible-only: no preceding visible pass, so out_path does not exist yet, and - # the input IS the pristine original for both the gate and the vendor probe. + # Invisible-only has no preceding visible pass. An output from an earlier run + # may already exist; only the current outcome decides whether to copy through. + # The input IS the pristine original for both the gate and the vendor probe. outcome = _run_invisible( img_path, img_path, @@ -779,10 +796,12 @@ def _run_batch_one( _SourceEvidence(img_path), force, ) - if not out_path.exists(): + if outcome != "removed" and out_path.resolve() != img_path.resolve(): # Keep the output directory COMPLETE even when the pixels are deliberately # left alone; a hole the caller cannot see is worse than an unchanged copy. src_bgr, src_alpha = image_io.read_bgr_and_alpha(img_path) - if src_bgr is None or not image_io.write_bgr_with_alpha(out_path, src_bgr, src_alpha): + if src_bgr is None or not image_io.write_bgr_with_alpha( + out_path, src_bgr, src_alpha, display_tags_from=img_path, orientation_applied=False + ): raise OSError(f"failed to copy input through to output: {out_path}") return BatchItemResult(img_path, out_path, mode, invisible=outcome) diff --git a/src/remove_ai_watermarks/cli.py b/src/remove_ai_watermarks/cli.py index 9b96e56..22a183b 100644 --- a/src/remove_ai_watermarks/cli.py +++ b/src/remove_ai_watermarks/cli.py @@ -75,31 +75,6 @@ class _Table: return "\n".join(f" {line}" for line in lines) -class _Progress: - """No-op stand-in for rich.Progress; results are printed directly instead.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass - - def __enter__(self) -> _Progress: - return self - - def __exit__(self, *exc: object) -> Literal[False]: - # Literal[False], not bool: a plain `bool` tells a type checker this context - # manager MAY suppress an exception, which makes every name bound inside a - # `with` block conditionally bound afterwards. It never suppresses. - return False - - def add_task(self, *args: Any, **kwargs: Any) -> int: - return 0 - - def advance(self, *args: Any, **kwargs: Any) -> None: - pass - - def update(self, *args: Any, **kwargs: Any) -> None: - pass - - class _Console: """Minimal plain-text replacement for rich.Console.""" @@ -117,14 +92,8 @@ def _panel(text: str = "", *args: Any, **kwargs: Any) -> str: return text -def _column(*args: Any, **kwargs: Any) -> None: - return None - - Panel = _panel Table = _Table -Progress = _Progress -SpinnerColumn = BarColumn = TextColumn = TimeElapsedColumn = _column console = _Console() @@ -1881,39 +1850,21 @@ def cmd_batch( tile_overlap=tile_overlap, ) - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeElapsedColumn(), - console=console, - ) as progress: - task = progress.add_task("Processing...", total=len(images)) - done: set[str] = set() + def on_progress(img: Path, stage: str, detail: str) -> None: + if ctx.obj.get("verbose"): + console.print(f" {img.name}: {stage}{f' {detail}' if detail else ''}") - def on_progress(img: Path, stage: str, detail: str) -> None: - # `remove_batch` emits exactly one terminal stage per image in EVERY mode, - # so the bar advances on that and never on a mode-specific line. - progress.update(task, description=img.name) - if stage in ("done", "failed") and img.name not in done: - done.add(img.name) - progress.advance(task) - if ctx.obj.get("verbose"): - console.print(f" {img.name}: {stage}{f' {detail}' if detail else ''}") - - summary = api_remove_batch( - directory, - output_dir, - mode=mode, # type: ignore[arg-type] - backend=backend, # type: ignore[arg-type] - sensitivity=_parse_sensitivity(sensitivity), - invisible=invisible_options, - force=force, - engine=_batch_engine(mode, invisible_options), - progress=on_progress, - ) - progress.update(task, completed=len(images)) + summary = api_remove_batch( + directory, + output_dir, + mode=mode, # type: ignore[arg-type] + backend=backend, # type: ignore[arg-type] + sensitivity=_parse_sensitivity(sensitivity), + invisible=invisible_options, + force=force, + engine=_batch_engine(mode, invisible_options), + progress=on_progress, + ) processed, errors = summary.processed, summary.failed synthid_skipped_count = len(summary.invisible_unavailable) diff --git a/src/remove_ai_watermarks/image_io.py b/src/remove_ai_watermarks/image_io.py index 4eab43a..0fa669b 100644 --- a/src/remove_ai_watermarks/image_io.py +++ b/src/remove_ai_watermarks/image_io.py @@ -137,7 +137,8 @@ def _register_heif() -> None: with contextlib.suppress(Exception): import pillow_heif # pyright: ignore[reportMissingImports] - pillow_heif.register_heif_opener() + # Documented public API; pillow-heif does not mark its typed re-export. + pillow_heif.register_heif_opener() # pyright: ignore[reportPrivateImportUsage] # ── Display-tag carry across re-encodes (issue #98) ───────────────────────── @@ -175,7 +176,9 @@ def _orientation_exif(orient: int | None, *, prefixed: bool) -> bytes | None: return dumped[6:] if dumped.startswith(b"Exif\x00\x00") else dumped -def _read_display_tags(source: Path, raster_shape: tuple[int, int]) -> tuple[bytes | None, int | None]: +def _read_display_tags( + source: Path, raster_shape: tuple[int, int], *, orientation_applied: bool | None = None +) -> tuple[bytes | None, int | None]: """Extract ``(icc_profile, orientation)`` from ``source`` for a raster of ``raster_shape``. The contract is that ``source`` is the file whose decode produced the pixels @@ -183,8 +186,9 @@ def _read_display_tags(source: Path, raster_shape: tuple[int, int]) -> tuple[byt decode turned the raster upright cannot be assumed from the container: cv2 applies EXIF orientation for JPEG and (build-dependent) PNG -- but only under ``IMREAD_COLOR``, never ``IMREAD_UNCHANGED``, which is what the pixel paths read - with -- and the reporter of issue #98 ran a build that left PNG flat too. So the - decision is made against the ground truth of the raster itself: for the + with -- and the reporter of issue #98 ran a build that left PNG flat too. + ``orientation_applied`` carries that decode state explicitly when known. + Otherwise the legacy inference uses raster geometry: for the transposing orientations (5-8) the tag is carried exactly when the raster still has the source's STORED dimensions, and dropped when it has the upright ones; anything else (a resized pipeline output, the rare mirror/180 values) drops the @@ -206,7 +210,11 @@ def _read_display_tags(source: Path, raster_shape: tuple[int, int]) -> tuple[byt return None, None if not (isinstance(icc, bytes) and icc): icc = None - if orient in _TRANSPOSING_ORIENTATIONS: + if orientation_applied is True: + orient = None + elif orientation_applied is False: + orient = orient if isinstance(orient, int) and 1 <= orient <= 8 else None + elif orient in _TRANSPOSING_ORIENTATIONS: rows, cols = raster_shape # Upright dimensions are the stored ones swapped; only a raster still in # stored orientation may carry the tag. @@ -426,7 +434,13 @@ def _pil_write( return False -def imwrite(path: str | Path, img: NDArray[Any], *, display_tags_from: str | Path | None = None) -> bool: +def imwrite( + path: str | Path, + img: NDArray[Any], + *, + display_tags_from: str | Path | None = None, + orientation_applied: bool | None = None, +) -> bool: """Unicode-safe image write that PRESERVES the input format at max quality. Format is taken from the path extension. HEIC/AVIF (which cv2 cannot encode) go @@ -439,14 +453,19 @@ def imwrite(path: str | Path, img: NDArray[Any], *, display_tags_from: str | Pat the source's ICC profile and EXIF orientation are carried into the re-encoded output (issue #98): cv2's encoders write no container metadata, and colour and geometry are display fidelity, not provenance. The tags are read BEFORE the write - so an in-place rewrite (source == output) still finds them, and orientation is - re-tagged only when the raster still has the source's stored dimensions (see - :func:`_read_display_tags`), so a decode that already turned the pixels upright - is never tagged into a second rotation.""" + so an in-place rewrite (source == output) still finds them. + Set ``orientation_applied=False`` for an unchanged/raw decode, or ``True`` + when EXIF has already been applied, including mirrors and square images. + Without that explicit state, orientation is re-tagged only when the raster + still has the source's stored dimensions, using the legacy heuristic in + :func:`_read_display_tags`. Pass the known decode state to avoid ambiguous + geometry, especially square images and mirrors.""" import cv2 icc, orient = ( - _read_display_tags(Path(display_tags_from), (img.shape[0], img.shape[1])) + _read_display_tags( + Path(display_tags_from), (img.shape[0], img.shape[1]), orientation_applied=orientation_applied + ) if display_tags_from is not None else (None, None) ) @@ -496,6 +515,7 @@ def write_bgr_with_alpha( alpha: NDArray[Any] | None, *, display_tags_from: str | Path | None = None, + orientation_applied: bool | None = None, ) -> bool: """Write BGR (with optional alpha) to ``path``. Returns ``imwrite``'s success flag. @@ -515,5 +535,7 @@ def write_bgr_with_alpha( import numpy as np if alpha is None or Path(path).suffix.lower() not in ALPHA_FORMATS: - return imwrite(path, bgr, display_tags_from=display_tags_from) - return imwrite(path, np.dstack([bgr, alpha]), display_tags_from=display_tags_from) + return imwrite(path, bgr, display_tags_from=display_tags_from, orientation_applied=orientation_applied) + return imwrite( + path, np.dstack([bgr, alpha]), display_tags_from=display_tags_from, orientation_applied=orientation_applied + ) diff --git a/src/remove_ai_watermarks/invisible_engine.py b/src/remove_ai_watermarks/invisible_engine.py index fa57a32..1a92301 100644 --- a/src/remove_ai_watermarks/invisible_engine.py +++ b/src/remove_ai_watermarks/invisible_engine.py @@ -297,9 +297,10 @@ class InvisibleEngine: # what would otherwise register it, so a bare Image.open would fail on HEIC. from remove_ai_watermarks import image_io + output_path = Path(output_path) if output_path is not None else Path(image_path) image_io._register_heif() - image = Image.open(image_path) - image = ImageOps.exif_transpose(image) + with Image.open(image_path) as opened: + image = ImageOps.exif_transpose(opened) orig_size = image.size # (width, height) # Full-res original, kept for the adaptive-polish detail target (image is # reassigned to the resized copy below; PIL resize returns a new object). @@ -312,7 +313,7 @@ class InvisibleEngine: # All profiles run at the input's native geometry, so only the explicit max # cap can move it, and it can only ever scale down. - target = _target_size(image.width, image.height, max_resolution) + target = None if tile else _target_size(image.width, image.height, max_resolution) if target is not None: if self._progress_callback: self._progress_callback( @@ -330,11 +331,11 @@ class InvisibleEngine: _tmp_path = Path(_tmp_str) # Convert to RGB before the PNG temp: the diffusion pass is RGB anyway, and a # non-RGB source mode (e.g. a CMYK JPEG) cannot be written as PNG and would raise. - image.convert("RGB").save(_tmp_path) os.close(_tmp_fd) image_path = _tmp_path try: + image.convert("RGB").save(_tmp_path, icc_profile=image.info.get("icc_profile")) out_path = self._remover.remove_watermark( image_path=image_path, output_path=output_path, @@ -380,7 +381,8 @@ class InvisibleEngine: progress=self._progress_callback, ) - image_io.imwrite(out_path, out_cv, display_tags_from=image_path) + if not image_io.imwrite(out_path, out_cv, display_tags_from=image_path, orientation_applied=True): + raise OSError(f"failed to write postprocessed output: {out_path}") return out_path finally: diff --git a/src/remove_ai_watermarks/metadata.py b/src/remove_ai_watermarks/metadata.py index bdccfdb..97d33f9 100644 --- a/src/remove_ai_watermarks/metadata.py +++ b/src/remove_ai_watermarks/metadata.py @@ -244,7 +244,7 @@ def _is_ai_value(value: str) -> bool: return provenance is not None or generator is not None or any(token in value_lower for token in AI_GENERATOR_TOKENS) -def _png_late_metadata(image_path: Path, window: int) -> bytes: +def _png_late_metadata(image_path: Path, window: int, *, strict: bool = False) -> bytes: """Payloads of PNG metadata chunks that start *beyond* the first ``window`` bytes, found by seeking past the (large) ``IDAT`` pixel stream. @@ -253,6 +253,7 @@ def _png_late_metadata(image_path: Path, window: int) -> bytes: ``iTXt`` chunk at ~2.7 MB). This is the PNG analogue of the ISOBMFF late-box scan in :func:`scan_head`. Returns only chunks past ``window`` so bytes already in the head are not duplicated; empty when there are none. + ``strict=True`` propagates I/O failures to a collection-status owner. """ out = bytearray() try: @@ -283,12 +284,16 @@ def _png_late_metadata(image_path: Path, window: int) -> bytes: # (which would silently skip a genuine AI-label chunk after it). pos = data_start + safe_length + 4 # data + CRC except OSError as exc: + if strict: + raise logger.debug("PNG late-metadata scan failed on %s: %s", image_path, exc) return b"" return bytes(out) -def _riff_late_metadata(image_path: Path, window: int, *, max_total: int = 4 * 1024 * 1024) -> bytes: +def _riff_late_metadata( + image_path: Path, window: int, *, max_total: int = 4 * 1024 * 1024, strict: bool = False +) -> bytes: """Payloads of RIFF metadata chunks that start *beyond* the first ``window`` bytes, found by stepping over the (large) coded-image chunk. @@ -332,6 +337,8 @@ def _riff_late_metadata(image_path: Path, window: int, *, max_total: int = 4 * 1 out += f.read(min(safe_length, max_total - len(out))) position = start + safe_length + (safe_length & 1) # chunks are word-aligned except OSError as exc: + if strict: + raise logger.debug("RIFF late-metadata scan failed on %s: %s", image_path, exc) return b"" return bytes(out) @@ -358,11 +365,10 @@ def scan_head(image_path: Path, size: int = 1024 * 1024) -> bytes: past large boxes like ``mdat``) and PNG ``tEXt`` / ``iTXt`` / ``eXIf`` chunks (seeking past ``IDAT``). - A file at least ``size`` bytes long additionally gets the metadata text its - decoder can reach but a raw read cannot (:func:`_decoder_visible_text`): a - compressed PNG ``zTXt`` packet, or a chunk past the window in a container with no - late-chunk reader here. A file that fits inside ``size`` is exactly - ``f.read(size)``, since the raw read already holds every byte. + Decoder-visible text is appended regardless of file size: even a complete + raw buffer cannot expose a compressed PNG ``zTXt`` packet without inflation. + The decoder also reaches chunks past the window in containers with no + late-chunk reader here. This is the shared input for every C2PA / AIGC / IPTC byte scan. The extensions catch a manifest or XMP packet placed AFTER the media data -- a @@ -406,8 +412,7 @@ def _scan_head_impl(image_path: Path, size: int) -> bytes: head += _png_late_metadata(image_path, size) elif head[:4] == b"RIFF" and head[8:12] == b"WEBP" and len(head) == size: head += _riff_late_metadata(image_path, size) - if len(head) >= size: - head += _decoder_visible_text(image_path, head) + head += _decoder_visible_text(image_path, head) return head @@ -435,8 +440,8 @@ def _decoder_visible_text(image_path: Path, head: bytes) -> bytes: structural reader here knows about yet. Only text ALREADY MISSING from ``head`` is appended, so the common case adds - nothing and no detector sees a value twice. Skipped entirely when the file fits - inside the window, since then the raw read already holds every byte. + nothing and no detector sees a value twice. Complete small buffers still need + this step because compressed metadata does not contain its decoded text. """ try: from PIL import Image @@ -722,8 +727,8 @@ _SAMSUNG_GENAI_RE = re.compile(rb'genAIType"\s*:\s*(-?\d+)') _SAMSUNG_EDITOR_MARKER = b"PhotoEditor_Re_Edit_Data" -def _read_file_tail(image_path: Path, size: int) -> bytes: - """Return the last ``size`` bytes of the file (or the whole file if smaller).""" +def _read_file_tail(image_path: Path, size: int, *, strict: bool = False) -> bytes: + """Read the last ``size`` bytes; optionally propagate I/O failures.""" try: file_size = image_path.stat().st_size with open(image_path, "rb") as f: @@ -731,6 +736,8 @@ def _read_file_tail(image_path: Path, size: int) -> bytes: f.seek(file_size - size) return f.read() except OSError: + if strict: + raise return b"" @@ -1249,7 +1256,8 @@ def _strip_with_ffmpeg(source_path: Path, output_path: Path) -> Path: f"ffmpeg is required to strip metadata from {source_path.suffix} files but was not found on " "PATH; install ffmpeg (e.g. `brew install ffmpeg`) or re-encode the file with another tool" ) - output_path.parent.mkdir(parents=True, exist_ok=True) + from remove_ai_watermarks.video_encoding import atomic_video_output + cmd = [ ffmpeg, "-y", @@ -1257,17 +1265,21 @@ def _strip_with_ffmpeg(source_path: Path, output_path: Path) -> Path: "error", "-i", str(source_path), + "-map", + "0", "-map_metadata", "-1", "-map_chapters", "-1", "-c", "copy", - str(output_path), ] - result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603 - if result.returncode != 0: - raise RuntimeError(f"ffmpeg failed to strip metadata from {source_path}: {result.stderr.strip()[:300]}") + with atomic_video_output(output_path) as temporary_output: + result = subprocess.run( # noqa: S603 + [*cmd, str(temporary_output)], capture_output=True, text=True, check=False + ) + if result.returncode != 0: + raise RuntimeError(f"ffmpeg failed to strip metadata from {source_path}: {result.stderr.strip()[:300]}") logger.info("Stripped container metadata via ffmpeg -> %s", output_path) return output_path @@ -1762,16 +1774,12 @@ def remove_ai_metadata( # im.info fallback (PNG has one; the others are not contractual), so # the default strip keeps the image's colour for every output format. save_kwargs["icc_profile"] = img.info["icc_profile"] - if ( - exif_data - and save_kwargs["format"] in ("JPEG", "PNG", "WEBP") - and (save_kwargs["format"] == "JPEG" or keep_standard) - ): + if exif_data and save_kwargs["format"] in ("JPEG", "PNG", "WEBP") and keep_standard: # Scrub AI-provenance EXIF tags (xAI/Grok signature, generator tokens) # while keeping genuine camera/editor EXIF, orientation included. PNG # and WebP outputs used to drop EXIF entirely, which took the display # orientation with it: a portrait came back tagged horizontal (issue - # #98). --remove-all still drops EXIF wholesale on those two. + # #98). --remove-all drops EXIF wholesale in every output format. if removed := _scrub_ai_exif(exif_data): logger.info("Scrubbed AI EXIF tag(s): %s", ", ".join(removed)) with contextlib.suppress(Exception): diff --git a/src/remove_ai_watermarks/metadata_record.py b/src/remove_ai_watermarks/metadata_record.py index f289ce2..9b3c291 100644 --- a/src/remove_ai_watermarks/metadata_record.py +++ b/src/remove_ai_watermarks/metadata_record.py @@ -173,15 +173,9 @@ def _isobmff_regions(image_path: Path, head: bytes) -> bytes: from remove_ai_watermarks._internal.isobmff import scan_c2pa_region, tc260_aigc_payloads out = bytearray(head[:HEAD_WINDOW]) - try: - out += scan_c2pa_region(image_path) - except Exception as exc: - logger.debug("ISOBMFF C2PA region scan failed on %s: %s", image_path, exc) - try: - for payload in tc260_aigc_payloads(image_path): - out += payload - except Exception as exc: - logger.debug("ISOBMFF TC260 scan failed on %s: %s", image_path, exc) + out += scan_c2pa_region(image_path, strict=True) + for payload in tc260_aigc_payloads(image_path, strict=True): + out += payload return bytes(out) @@ -201,9 +195,9 @@ def _container_regions(image_path: Path, head: bytes) -> tuple[str, bytes]: if head.startswith(PNG_SIGNATURE): # Chunks placed after the pixel stream (an XMP packet at 2.7 MB, say) are # past the window; the same seek-past-IDAT reader the file path uses gets them. - return "png", _png_regions(head) + png_late_metadata(image_path, HEAD_WINDOW) + return "png", _png_regions(head) + png_late_metadata(image_path, HEAD_WINDOW, strict=True) if head.startswith(b"RIFF") and head[8:12] == b"WEBP": - return "webp", _riff_regions(head) + riff_late_metadata(image_path, HEAD_WINDOW) + return "webp", _riff_regions(head) + riff_late_metadata(image_path, HEAD_WINDOW, strict=True) if is_isobmff(head): return "isobmff", _isobmff_regions(image_path, head) return "unknown", head @@ -211,12 +205,8 @@ def _container_regions(image_path: Path, head: bytes) -> tuple[str, bytes]: def _raw_head(image_path: Path) -> bytes: """The file's first bytes, unmodified -- the input every structural walk needs.""" - try: - with open(image_path, "rb") as handle: - return handle.read(HEAD_WINDOW) - except OSError as exc: - logger.debug("head read failed for %s: %s", image_path, exc) - return b"" + with open(image_path, "rb") as handle: + return handle.read(HEAD_WINDOW) def _trailer(image_path: Path, container: str) -> bytes: @@ -232,28 +222,24 @@ def _trailer(image_path: Path, container: str) -> bytes: # RIFF declares its structural end in bytes 4..8. A fixed tail window is # normally the last animation/frame payload, not a trailer, so preserve # only bytes appended after the declared RIFF container. - try: - with open(image_path, "rb") as handle: - header = handle.read(12) - if len(header) < 12 or not header.startswith(b"RIFF"): - return b"" - declared_end = 8 + struct.unpack("= file_size: - return b"" - handle.seek(declared_end) - return handle.read(min(file_size - declared_end, UNKNOWN_TRAILER_WINDOW)) - except OSError as exc: - logger.debug("RIFF trailer read failed for %s: %s", image_path, exc) - return b"" + with open(image_path, "rb") as handle: + header = handle.read(12) + if len(header) < 12 or not header.startswith(b"RIFF"): + return b"" + declared_end = 8 + struct.unpack("= file_size: + return b"" + handle.seek(declared_end) + return handle.read(min(file_size - declared_end, UNKNOWN_TRAILER_WINDOW)) if container == "isobmff": # ISOBMFF has no out-of-container trailer convention. Its bounded box # walkers already collect late provenance while skipping ``mdat``; keeping # a blind tail here would carry coded media bytes. return b"" - tail = read_file_tail(image_path, TAIL_WINDOW) + tail = read_file_tail(image_path, TAIL_WINDOW, strict=True) if SAMSUNG_EDITOR_MARKER in tail: # Galaxy AI splits its evidence: the marker sits in the post-EOI trailer, but # the `genAIType` value it is gated on can sit INSIDE the entropy-coded scan. @@ -275,14 +261,14 @@ def _decoder_info(image_path: Path) -> dict[str, Any]: (the text keys, and the raw EXIF blob), and opening twice repeats the container header parse and, for a PNG carrying ``zTXt``, the zlib inflate with it. """ - try: - from PIL import Image + from PIL import Image, UnidentifiedImageError + try: with Image.open(image_path) as img: # PIL types this mapping with a non-string key union (a DPI tuple key # exists), so the keys are normalized here rather than assumed. return {str(key): value for key, value in img.info.items()} - except Exception as exc: # a container PIL cannot open + except (UnidentifiedImageError, ImportError) as exc: # unsupported optional decoder logger.debug("PIL info unavailable for %s: %s", image_path, exc) return {} @@ -299,15 +285,11 @@ def _exif_pairs(info: dict[str, Any]) -> dict[str, str]: exif_bytes = info.get("exif") if not exif_bytes: return {} - try: - import piexif + import piexif - loaded = piexif.load(exif_bytes) - tags = loaded.get("0th", {}) - exif_tags = loaded.get("Exif", {}) - except Exception as exc: # malformed EXIF - logger.debug("EXIF parse failed: %s", exc) - return {} + loaded = piexif.load(exif_bytes) + tags = loaded.get("0th", {}) + exif_tags = loaded.get("Exif", {}) pairs = { name: text @@ -375,18 +357,53 @@ def collect_metadata_record( from remove_ai_watermarks._internal.c2pa import read_manifest_store_json + status = "complete" + issues: list[dict[str, str]] = [] + container, regions, tail = "unknown", b"", b"" + info: dict[str, Any] = {} + exif: dict[str, str] = {} + store = None try: image_path.stat() - status = "complete" - issues: list[dict[str, str]] = [] except OSError as exc: logger.debug("metadata source unavailable for %s: %s", image_path, exc) status = "error" - issues = [{"stage": "source", "code": "unavailable"}] + issues.append({"stage": "source", "code": "unavailable"}) - container, regions = _container_regions(image_path, _raw_head(image_path)) + if status == "complete": + try: + head = _raw_head(image_path) + except OSError as exc: + logger.debug("metadata head read failed for %s: %s", image_path, exc) + status = "error" + issues.append({"stage": "head", "code": "read-failed"}) + else: + # Each stage reports its own failure. Successful earlier regions stay + # available for inspection, but the verdict rejects an incomplete record. + try: + container, regions = _container_regions(image_path, head) + except Exception as exc: + logger.debug("metadata region collection failed for %s: %s", image_path, exc) + issues.append({"stage": "regions", "code": "collection-failed"}) + try: + tail = _trailer(image_path, container) + except Exception as exc: + logger.debug("metadata trailer collection failed for %s: %s", image_path, exc) + issues.append({"stage": "trailer", "code": "collection-failed"}) + try: + info = _decoder_info(image_path) + exif = _exif_pairs(info) + except Exception as exc: + logger.debug("metadata decoder collection failed for %s: %s", image_path, exc) + issues.append({"stage": "decoder", "code": "collection-failed"}) + try: + store = read_manifest_store_json(image_path, strict=True) + except Exception as exc: + logger.debug("C2PA collection failed for %s: %s", image_path, exc) + issues.append({"stage": "c2pa", "code": "collection-failed"}) + if issues: + status = "partial" - info = _decoder_info(image_path) record: dict[str, Any] = { "schema_version": schema_version, "record_type": METADATA_RECORD_TYPE, @@ -397,13 +414,12 @@ def collect_metadata_record( "metadata_base64": base64.b64encode(regions).decode("ascii"), # Always collected: Samsung's Galaxy AI marker is a post-EOI trailer, and a # record without it loses that verdict outright. - "tail_base64": base64.b64encode(_trailer(image_path, container)).decode("ascii"), + "tail_base64": base64.b64encode(tail).decode("ascii"), # PIL info BEFORE exif: the file path prefers a PNG text tag over an EXIF # one, and the normalizer walks the record in insertion order. "pil": _pil_info(info), - "exif": _exif_pairs(info), + "exif": exif, } - store = read_manifest_store_json(image_path) if store is not None: record["c2pa_store"] = store return record diff --git a/src/remove_ai_watermarks/text_draft.py b/src/remove_ai_watermarks/text_draft.py index 5a39c29..3350b8c 100644 --- a/src/remove_ai_watermarks/text_draft.py +++ b/src/remove_ai_watermarks/text_draft.py @@ -138,12 +138,12 @@ def _recognition_box( ) -> tuple[int, int, int, int]: x1, y1, x2, y2 = box line_height = y2 - y1 + pad_y = max(8, line_height // 3) if vertical_pad_ratio is None else max(1, round(line_height * vertical_pad_ratio)) if script == "cjk": left_pad = max(16, round(line_height * 0.2)) right_pad = max(16, round(line_height * 0.6)) - return max(0, x1 - left_pad), y1, min(width, x2 + right_pad), y2 + return max(0, x1 - left_pad), max(0, y1 - pad_y), min(width, x2 + right_pad), min(height, y2 + pad_y) pad_x = max(16, line_height) - pad_y = max(8, line_height // 3) if vertical_pad_ratio is None else max(8, round(line_height * vertical_pad_ratio)) return max(0, x1 - pad_x), max(0, y1 - pad_y), min(width, x2 + pad_x), min(height, y2 + pad_y) diff --git a/src/remove_ai_watermarks/video.py b/src/remove_ai_watermarks/video.py index 8127d88..5a2ac62 100644 --- a/src/remove_ai_watermarks/video.py +++ b/src/remove_ai_watermarks/video.py @@ -280,6 +280,7 @@ def _visible_removal_plan( from remove_ai_watermarks.video_visible import ( VISIBLE_MARK_POLICIES, has_bytedance_video_provenance, + has_doubao_video_provenance, has_hailuo_video_provenance, has_sora_provenance, has_veo_provenance, @@ -290,6 +291,7 @@ def _visible_removal_plan( "sora": has_sora_provenance, "veo": has_veo_provenance, "seedance": has_bytedance_video_provenance, + "doubao": has_doubao_video_provenance, "dola": has_bytedance_video_provenance, "hailuo": has_hailuo_video_provenance, }.get(selected_mark) diff --git a/src/remove_ai_watermarks/video_visible.py b/src/remove_ai_watermarks/video_visible.py index f3dffb5..6304c40 100644 --- a/src/remove_ai_watermarks/video_visible.py +++ b/src/remove_ai_watermarks/video_visible.py @@ -1534,6 +1534,15 @@ def has_bytedance_video_provenance(markers: dict[str, str]) -> bool: return ("bytedance" in identity or "byteplus" in identity) and "trainedalgorithmicmedia" in source_type +def has_doubao_video_provenance(markers: dict[str, str]) -> bool: + """Whether the structural TC260 producer identifies registered Doubao provenance.""" + from remove_ai_watermarks.metadata import uscc_of + from remove_ai_watermarks.watermark_registry import get_mark + + producer = uscc_of(markers.get("aigc_producer", "").strip()).casefold() + return producer in {code.casefold() for code in get_mark("doubao").tc260_producer_codes} + + def has_hailuo_video_provenance(markers: dict[str, str]) -> bool: """Whether a TC260 label names MiniMax, Hailuo's maker, as the producer. diff --git a/tests/test_agent_skill.py b/tests/test_agent_skill.py index 18d21db..519ab35 100644 --- a/tests/test_agent_skill.py +++ b/tests/test_agent_skill.py @@ -604,3 +604,99 @@ def test_every_cli_command_is_named_somewhere_in_the_skill() -> None: missing = sorted(" ".join(path) for path in _cli_commands() if " ".join(path) not in text) assert not missing, f"the CLI grew commands the skill never mentions: {missing}" + + +@pytest.mark.parametrize( + ("video", "invisible", "visible_advice", "invisible_advice"), + [ + ("missing", "installed", "needs_video_extra", "needs_video_and_diffusion_extras"), + ("ok", "missing", "ok", "needs_video_and_diffusion_extras"), + ("ok", "installed", "ok", "ok_cpu_or_gpu"), + ("unknown", "unknown", "unverified", "unverified"), + ], +) +def test_probe_video_advice_uses_the_installed_video_runtime( + video: str, + invisible: str, + visible_advice: str, + invisible_advice: str, +) -> None: + probe = _probe_module() + probe._video_stacks = lambda _cli: {"pixels": video, "invisible": invisible} + probe._which = lambda _name: "/synthetic/executable" + probe._has_cuda = lambda: False + probe._cli = lambda: {"found": True, "path": "/synthetic/cli", "outdated": False} + probe._pixel_stack = lambda _cli: "ok" + probe._invisible_stack = lambda _cli: "missing" + report = probe.build_report() + assert report["advice"]["video_visible"] == visible_advice + assert report["advice"]["video_invisible"] == invisible_advice + + +@pytest.mark.parametrize("wrapper", [False, True]) +def test_video_probe_runs_the_cli_interpreter_without_loading_models(tmp_path: Path, wrapper: bool) -> None: + probe = _probe_module() + cli = tmp_path / "remove-ai-watermarks" + interpreter = "/synthetic/venv/bin/python3" + cli.write_text(f"#!/bin/sh\n'''exec' {interpreter} \"$0\" \"$@\"\n' '''\n" if wrapper else f"#!{interpreter}\n") + calls: list[list[str]] = [] + + def run(argv: list[str], timeout: float = 8) -> tuple[int, str, str]: + calls.append(argv) + return 0, json.dumps({"pixels": "ok", "invisible": "installed"}), "" + + probe._run = run + assert probe._video_stacks(str(cli)) == {"pixels": "ok", "invisible": "installed"} + assert len(calls) == 1 + assert calls[0][:2] == [interpreter, "-c"] + # Execute the exact child code against the real package's availability seams. + # The loader is forbidden even when the optional stack is present. + from types import SimpleNamespace + from unittest.mock import patch + + from remove_ai_watermarks import video_invisible + + with ( + patch.dict(sys.modules, {"av": SimpleNamespace(), "diffusers": SimpleNamespace(AutoencoderKL=object)}), + patch("remove_ai_watermarks.video._require_video_runtime"), + patch.object(video_invisible, "is_available", return_value=True), + patch.object(video_invisible, "load_video_vae_runtime", side_effect=AssertionError("model load")), + ): + exec(calls[0][2], {}) # noqa: S102 - Execute the probe's own child code at the real seam. + + +@pytest.mark.parametrize(("video_missing", "diffusion_missing"), [(True, False), (False, True), (False, False)]) +def test_video_runtime_probe_reports_both_real_availability_seams( + video_missing: bool, + diffusion_missing: bool, + capsys: pytest.CaptureFixture[str], +) -> None: + from types import SimpleNamespace + from unittest.mock import patch + + from remove_ai_watermarks import video, video_invisible + + probe = _probe_module() + guard = RuntimeError("missing video") if video_missing else None + with ( + patch.dict(sys.modules, {"av": SimpleNamespace(), "diffusers": SimpleNamespace(AutoencoderKL=object)}), + patch.object(video, "_require_video_runtime", side_effect=guard) as checked_video, + patch.object(video_invisible, "is_available", return_value=not diffusion_missing) as checked_diffusion, + patch.object(video_invisible, "load_video_vae_runtime", side_effect=AssertionError("model load")), + ): + exec(probe._VIDEO_RUNTIME_CHECK, {}) # noqa: S102 - Execute repository-owned probe code. + result = json.loads(capsys.readouterr().out) + checked_video.assert_called_once() + checked_diffusion.assert_called_once() + assert result == { + "pixels": "missing" if video_missing else "ok", + "invisible": "missing" if diffusion_missing else "installed", + } + + +def test_unrecognized_cli_launcher_is_unverified(tmp_path: Path) -> None: + probe = _probe_module() + cli = tmp_path / "launcher" + cli.write_text('#!/bin/sh\nexec unrecognized-wrapper "$@"\n') + probe._run = lambda *_a, **_kw: pytest.fail("unknown launcher must not run") + assert probe._video_stacks(str(cli)) == {"pixels": "unknown", "invisible": "unknown"} diff --git a/tests/test_audit_image_contracts.py b/tests/test_audit_image_contracts.py new file mode 100644 index 0000000..54ee81e --- /dev/null +++ b/tests/test_audit_image_contracts.py @@ -0,0 +1,228 @@ +"""Regression coverage for image output lifetimes and display fidelity.""" + +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pytest +from PIL import Image, ImageCms, ImageOps + +from remove_ai_watermarks import api, image_io, invisible_engine, watermark_registry +from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover + + +def _source(path, orientation=1, alpha=False, size=(96, 64)): + width, height = size + pixels = np.zeros((height, width, 4 if alpha else 3), dtype=np.uint8) + pixels[:, : width // 3, :3] = (240, 20, 30) + pixels[: height // 2, width // 3 :, :3] = (10, 200, 40) + if alpha: + pixels[:, :, 3] = np.arange(width, dtype=np.uint8)[None, :] + 2 * np.arange(height, dtype=np.uint8)[:, None] + image = Image.fromarray(pixels) + exif = Image.Exif() + exif[274] = orientation + icc = ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes() + image.save(path, exif=exif, icc_profile=icc) + with Image.open(path) as reopened: + upright = np.array(ImageOps.exif_transpose(reopened)) + return icc, upright + + +def _engine(run=None): + engine = object.__new__(invisible_engine.InvisibleEngine) + engine._progress_callback = None + remover = object.__new__(WatermarkRemover) + remover.model_profile = "qwen-zimage" + remover._auto = False + remover._qwen_zimage_pipeline = SimpleNamespace(run=run or (lambda source, **kwargs: source)) + engine._remover = remover + return engine + + +def _no_visible(monkeypatch): + monkeypatch.setattr( + watermark_registry, + "remove_auto_marks_detailed", + lambda image, **kwargs: watermark_registry.VisibleRemovalResult(image, ()), + ) + + +@pytest.mark.parametrize("available", [False, True]) +@pytest.mark.parametrize("in_place", [False, True]) +def test_invisible_batch_passthrough_uses_current_source(tmp_path, monkeypatch, available, in_place): + source_dir = tmp_path / "in" + source_dir.mkdir() + out_dir = source_dir if in_place else tmp_path / "out" + out_dir.mkdir(exist_ok=True) + source = source_dir / "image.png" + icc, expected = _source(source, orientation=3) + original_bytes = source.read_bytes() + destination = out_dir / source.name + if not in_place: + Image.new("RGB", (96, 64), "blue").save(destination) + monkeypatch.setattr(invisible_engine, "is_available", lambda: available) + monkeypatch.setattr(api._SourceEvidence, "has_invisible_target", lambda self: False) + + summary = api.remove_batch(source_dir, out_dir, mode="invisible") + + assert summary.processed == 1 + assert summary.failed == 0 + assert summary.items[0].invisible == ("no-signal" if available else "unavailable") + if in_place: + assert source.read_bytes() == original_bytes + with Image.open(destination) as result: + np.testing.assert_array_equal(np.array(ImageOps.exif_transpose(result)), expected) + assert result.info["icc_profile"] == icc + + +@pytest.mark.parametrize("mode", ["visible", "all"]) +@pytest.mark.parametrize("orientation", [1, 2, 3, 4, 6, 8]) +@pytest.mark.parametrize("run_invisible", [False, True]) +def test_batch_display_tags_and_alpha_survive_repeated_in_place_writes( + tmp_path, monkeypatch, mode, orientation, run_invisible +): + source = tmp_path / "image.png" + icc, expected = _source(source, orientation=orientation, alpha=True) + _no_visible(monkeypatch) + monkeypatch.setattr(invisible_engine, "is_available", lambda: True) + calls = [] + + def run(image, **kwargs): + calls.append(image.size) + return image + + engine = _engine(run) + for _ in range(2): + summary = api.remove_batch(tmp_path, tmp_path, mode=mode, force=run_invisible, engine=engine) + assert summary.failed == 0, summary.errors + with Image.open(source) as result: + assert result.info.get("icc_profile") == icc + np.testing.assert_array_equal(np.array(ImageOps.exif_transpose(result)), expected) + assert len(calls) == (2 if mode == "all" and run_invisible else 0) + + +@pytest.mark.parametrize("explicit", [False, True]) +@pytest.mark.parametrize("polish", [False, True]) +def test_invisible_engine_overwrites_original_and_retains_display(tmp_path, explicit, polish): + source = tmp_path / "source.png" + icc, expected = _source(source, orientation=6) + engine = _engine() + output = engine.remove_watermark(source, source if explicit else None, adaptive_polish=polish) + assert output == source + assert output.exists() + with Image.open(output) as result: + assert result.info.get("icc_profile") == icc + assert result.getexif().get(274, 1) == 1 + assert result.size == (64, 96) + if not polish: + np.testing.assert_array_equal(np.array(result), expected) + + +@pytest.mark.parametrize(("tile", "expected"), [(False, (512, 256)), (True, (2048, 1024))]) +def test_tiling_bypasses_resolution_cap(tmp_path, tile, expected): + source = tmp_path / "source.png" + _source(source, size=(2048, 1024)) + seen = [] + + def run(image, **kwargs): + seen.append((image.size, kwargs["tile"])) + return image + + _engine(run).remove_watermark(source, tmp_path / "out.png", tile=tile, max_resolution=512) + assert seen == [(expected, tile)] + + +def test_invisible_postprocessing_write_failure_raises(tmp_path, monkeypatch): + source = tmp_path / "source.png" + _source(source) + original_writer = image_io.imwrite + + def write(path, pixels, **kwargs): + # The model's write succeeds, the final public postprocessing write fails. + if isinstance(path, Path): + return False + return original_writer(path, pixels, **kwargs) + + monkeypatch.setattr(image_io, "imwrite", write) + with pytest.raises(OSError, match="write"): + _engine().remove_watermark(source, tmp_path / "out.png", adaptive_polish=True) + + +@pytest.mark.parametrize("orientation", [2, 3, 4, 5, 6, 7, 8]) +def test_explicit_upright_writer_does_not_reapply_square_image_orientation(tmp_path, orientation): + source = tmp_path / "source.png" + icc, upright = _source(source, orientation=orientation, size=(64, 64)) + assert image_io.imwrite(source, upright[:, :, ::-1], display_tags_from=source, orientation_applied=True) + with Image.open(source) as result: + assert result.info.get("icc_profile") == icc + np.testing.assert_array_equal(np.array(ImageOps.exif_transpose(result)), upright) + + +@pytest.mark.parametrize("failure", [None, "save", "model"]) +def test_invisible_temporary_input_is_cleaned_after_success_and_failure(tmp_path, monkeypatch, failure): + import tempfile + + source = tmp_path / "source.png" + _source(source) + original_bytes = source.read_bytes() + temporary_paths = [] + original_mkstemp = tempfile.mkstemp + + def make_temp(**kwargs): + fd, name = original_mkstemp(dir=tmp_path, **kwargs) + temporary_paths.append(Path(name)) + return fd, name + + monkeypatch.setattr(tempfile, "mkstemp", make_temp) + original_save = Image.Image.save + + def save(image, fp, **kwargs): + if failure == "save" and Path(fp) in temporary_paths: + raise OSError("synthetic temp save failure") + return original_save(image, fp, **kwargs) + + monkeypatch.setattr(Image.Image, "save", save) + + def run(image, **kwargs): + if failure == "model": + raise OSError("synthetic model failure") + return image + + engine = _engine(run) + destination = tmp_path / "out.png" + if failure: + with pytest.raises(OSError, match="synthetic"): + engine.remove_watermark(source, destination) + assert not destination.exists() + else: + assert engine.remove_watermark(source, destination) == destination + assert destination.exists() + assert source.read_bytes() == original_bytes + assert len(temporary_paths) == 1 + assert not temporary_paths[0].exists() + + +def test_invisible_batch_preserves_new_model_result(tmp_path, monkeypatch): + source_dir = tmp_path / "in" + source_dir.mkdir() + source = source_dir / "image.png" + _source(source) + monkeypatch.setattr(invisible_engine, "is_available", lambda: True) + engine = _engine(lambda image, **kwargs: Image.new("RGB", image.size, (20, 30, 220))) + summary = api.remove_batch(source_dir, tmp_path / "out", mode="invisible", force=True, engine=engine) + assert summary.failed == 0 + assert summary.items[0].invisible == "removed" + with Image.open(tmp_path / "out" / source.name) as output: + assert output.getpixel((0, 0)) == (20, 30, 220) + + +@pytest.mark.parametrize("orientation", [3, 6]) +def test_single_visible_format_conversion_preserves_display_tags(tmp_path, monkeypatch, orientation): + source = tmp_path / "source.png" + icc, expected = _source(source, orientation=orientation, size=(64, 64)) + _no_visible(monkeypatch) + destination = tmp_path / "out.webp" + api.remove_visible(source, destination, strip_metadata=False) + with Image.open(destination) as result: + assert result.info.get("icc_profile") == icc + np.testing.assert_array_equal(np.array(ImageOps.exif_transpose(result)), expected) diff --git a/tests/test_classify.py b/tests/test_classify.py index 13ff3ba..7778696 100644 --- a/tests/test_classify.py +++ b/tests/test_classify.py @@ -67,7 +67,7 @@ def test_runtime_requests_the_exported_allow_patterns(monkeypatch: pytest.Monkey """ from remove_ai_watermarks.classify import _WEIGHT_FILES, _weights_dir - captured: dict[str, object] = [] + captured: list[dict[str, object]] = [] def fake_snapshot_download(**kwargs: object) -> str: captured.append(kwargs) diff --git a/tests/test_evaluation_audit_regressions.py b/tests/test_evaluation_audit_regressions.py new file mode 100644 index 0000000..4b87ed8 --- /dev/null +++ b/tests/test_evaluation_audit_regressions.py @@ -0,0 +1,452 @@ +"""Regressions for evaluation evidence and process outcome handling.""" + +from __future__ import annotations + +import importlib +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) +sys.path.insert(0, str(ROOT / "scripts")) + + +def test_archive_imports(): + import remove_ai_watermarks + + assert Path(remove_ai_watermarks.__file__).is_relative_to(ROOT) + + +def test_rectangular_video_decode_preserves_pixel_layout(monkeypatch, tmp_path): + import watermark_benchmark as benchmark + + pixels = np.arange(2 * 4 * 3, dtype=np.uint8).reshape(1, 2, 4, 3) + monkeypatch.setattr(benchmark.shutil, "which", lambda name: name) + responses = iter( + [SimpleNamespace(returncode=0, stdout="4,2"), SimpleNamespace(returncode=0, stdout=pixels.tobytes())] + ) + monkeypatch.setattr(subprocess, "run", lambda *a, **k: next(responses)) + decoded = benchmark._decode_video(tmp_path / "clip.mp4") + assert decoded.shape == pixels.shape + assert np.array_equal(decoded, pixels.astype(np.float32) / 255) + + +@pytest.mark.parametrize("code", [-11, 127, 137, 1]) +def test_smoke_rejects_unexpected_process_outcomes(monkeypatch, tmp_path, code): + import smoke_matrix as smoke + + monkeypatch.setattr(smoke.subprocess, "run", lambda *a, **k: SimpleNamespace(returncode=code, stdout="", stderr="")) + assert smoke.Runner(tmp_path).run("visible", ["visible", "input.png"], expect_exit=(0, 2)).status == "FAIL" + + +@pytest.mark.parametrize("code", [-11, 127, 137]) +def test_robustness_rejects_abnormal_exit_without_text(monkeypatch, code): + import robustness_suite as robustness + + monkeypatch.setattr(robustness, "run", lambda *a, **k: (code, "", False)) + result = robustness.Results() + robustness.graceful(result, "malformed", "identify", []) + assert result.rows[0][2] is False + + +def test_video_psnr_is_reported_with_explicit_semantics(): + import watermark_benchmark_report as report + + record = SimpleNamespace( + data={ + "adapter": "videoseal", + "state": "attacked", + "artifact": {"sha256": "a"}, + "reference": {"sha256": "b"}, + "fidelity": {"status": "measured", "mean_psnr_db": 40.0}, + } + ) + group = report._fidelity_groups([record])[0] + assert group["finite_mean_psnr"] == 1 + assert group["p50_mean_psnr_db"] == 40.0 + assert group["finite_psnr"] == 0 + + +def test_web_results_survive_later_browser_failure(monkeypatch, tmp_path): + import provider_oracle_web as web + + manifest = { + "surface": "meta-web", + "slot": {}, + "rows": [{"artifact_id": "first", "upload_path": "one"}, {"artifact_id": "second", "upload_path": "two"}], + } + results = {"rows": [{"watermark_result": None}, {"watermark_result": None}]} + saved = [] + monkeypatch.setattr(web.oracles, "verify_batch", lambda *a, **k: {}) + monkeypatch.setattr(web.oracles, "load_batch", lambda *a: (manifest, results)) + monkeypatch.setattr(web, "proxy_settings", lambda *a: None) + monkeypatch.setattr(web.oracles, "record_result", lambda *a, **k: saved.append(k)) + + def submit(*args, **kwargs): + yield web.WebVerdict("detected", "unavailable", "first settled") + assert len(saved) == 1 + raise RuntimeError("second page failed") + + monkeypatch.setattr(web, "_submit_batch", submit) + with pytest.raises(RuntimeError, match="second page failed"): + web.run_web_batch(tmp_path / "manifest.json", acknowledge_uploads=True) + assert saved[0]["artifact_id"] == "first" + assert saved[0]["checked_at"].endswith("Z") + + +def test_normalized_edit_distance_retains_historical_value(): + import fidelity_metrics as fidelity + + assert fidelity._text_ned("a", "aaaa") == 0.75 + assert fidelity._text_ned("A", "a") == 1.0 + + +def test_recall_deduplicates_bytes_independently_of_detector(tmp_path): + import visible_recall_sample as recall + + paths = [tmp_path / name for name in ("a", "b", "copy")] + for path, content in zip(paths, (b"a", b"b", b"a"), strict=True): + path.write_bytes(content) + rows = [{"path": str(path), "shape": [4, 4], "marks": {"x": {"conf": 0.5}}} for path in paths] + assert recall.unique_content_rows(rows) == rows[:2] + + +def test_temporal_study_measures_only_saved_decodes(monkeypatch, tmp_path): + import videoseal_temporal_study as study + + raw = np.zeros((2, 4, 4, 3), dtype=np.float32) + decoded = {} + measured = [] + + def encode(_ffmpeg, path, frames): + path.write_bytes(path.name.encode()) + decoded[path] = np.full_like(raw, len(decoded) / 20 + 0.1) + return path + + def read(_model, frames): + assert any(frames is value for value in decoded.values()) + measured.append(frames) + return SimpleNamespace(bit_accuracy=0.75, per_frame_bit_accuracy=[0.75, 0.75]) + + def matrix(_model, frames): + assert any(frames is value for value in decoded.values()) + measured.append(frames) + return {"avg": 0.75} + + monkeypatch.setattr(study, "require_tools", lambda: "fake") + monkeypatch.setattr(study, "ffmpeg_version", lambda _: "fake") + monkeypatch.setattr(study.videoseal_oracle, "load_model", lambda: object()) + monkeypatch.setattr(study.videoseal_oracle, "embed", lambda *_: raw) + monkeypatch.setattr(study, "synth_carrier", lambda _: raw) + monkeypatch.setattr(study, "REAL_CARRIERS", ()) + monkeypatch.setattr(study, "CRF_SWEEP", ()) + monkeypatch.setattr(study, "encode_clip", encode) + monkeypatch.setattr(study, "apply_scale", lambda _f, _p, d: encode(_f, d / "scale.mp4", raw)) + monkeypatch.setattr(study, "apply_fps_half", lambda _f, _p, d: encode(_f, d / "fps.mp4", raw)) + import watermark_benchmark as benchmark + + monkeypatch.setattr(benchmark, "_decode_video", lambda path: decoded[path]) + monkeypatch.setattr(study, "_decode_video", lambda path: decoded[path]) + monkeypatch.setattr(study, "read", read) + monkeypatch.setattr(study, "read_aggregation_matrix", matrix) + monkeypatch.setattr(study, "render_matrix", lambda rows: "") + monkeypatch.setattr(sys, "argv", ["study", "--output-dir", str(tmp_path / "out")]) + assert study.main() == 0 + assert len(measured) == 14 + + +def test_forgery_study_measures_only_saved_decodes(monkeypatch, tmp_path): + import watermark_benchmark as benchmark + import watermark_benchmark_video_cohort as cohort + import watermark_forgery_study as study + + raw = np.zeros((2, 4, 4, 3), dtype=np.float32) + decoded = {} + measured = [] + + def encode(_ffmpeg, path, frames): + path.write_bytes(path.name.encode()) + decoded[path] = np.full_like(raw, len(decoded) / 30 + 0.1) + return path + + def read(_model, frames, **kwargs): + assert any(frames is value for value in decoded.values()) + measured.append(frames) + return SimpleNamespace(bit_accuracy=0.75, decoded_bits=study.videoseal_oracle.message_bits(), detected=True) + + monkeypatch.setattr(study.videoseal_oracle, "load_model", lambda: object()) + monkeypatch.setattr(study.videoseal_oracle, "embed", lambda *_: raw) + monkeypatch.setattr(study.videoseal_oracle, "read", read) + monkeypatch.setattr(study, "synth_video_carrier", lambda _: raw) + monkeypatch.setattr(study, "real_carrier_frames", lambda _: (raw, {})) + monkeypatch.setattr(cohort, "encode_clip", encode) + monkeypatch.setattr(study, "apply_crf", lambda _f, _p, _c, d: encode(_f, d / "removed.mp4", raw)) + monkeypatch.setattr(benchmark, "_decode_video", lambda path: decoded[path]) + rows = study.video_cells(tmp_path, "fake") + assert len(measured) == 12 + assert len(rows) == 13 + + +def test_isolated_workers_contain_process_exit(tmp_path): + import _isolated_image_workers as workers + + script = tmp_path / "worker.py" + script.write_text( + "import os\ndef _one(path):\n if path == 'bad':\n os._exit(7)\n" + " return {'path': path, 'status': 'ok'}\n" + ) + rows = workers.run_batch(script, ["good", "bad", "last"], jobs=2, timeout=10) + assert [row["status"] for row in rows] == ["ok", "crashed", "ok"] + assert rows[1]["returncode"] == 7 + + +def test_isolated_worker_timeout_has_no_parent_fallback(monkeypatch, tmp_path): + import _isolated_image_workers as workers + + calls = [] + + def run(command, **kwargs): + calls.append((command, kwargs)) + raise subprocess.TimeoutExpired(command, kwargs["timeout"]) + + monkeypatch.setattr(workers.subprocess, "run", run) + rows = workers.run_batch(tmp_path / "never-import-this.py", ["one", "two"], jobs=2, timeout=3) + assert [row["status"] for row in rows] == ["timeout", "timeout"] + assert len(calls) == 2 + assert all(kwargs["timeout"] == 3 for _, kwargs in calls) + + +@pytest.mark.parametrize(("name", "function"), [("visible_positives", "_run_batch"), ("pill_gate_audit", "_batch")]) +def test_audit_uses_isolated_runner(monkeypatch, name, function): + module = importlib.import_module(name) + calls = [] + + def run(script, paths, *, jobs, timeout): + calls.append((script, paths, jobs, timeout)) + return [{"path": "one", "status": "timeout"}] + + monkeypatch.setattr(module, "run_batch", run) + assert getattr(module, function)(["one"], 2, 3)[0]["status"] == "timeout" + assert calls == [(Path(module.__file__), ["one"], 2, 3)] + + +def test_audio_manifest_records_executed_carrier_seeds(monkeypatch, tmp_path): + import json + + import audioseal_experiment as experiment + import watermark_benchmark_audio_cohort as cohort + + observed = [] + original = np.random.default_rng + + def rng(seed): + observed.append(seed) + return original(seed) + + monkeypatch.setattr(experiment.np.random, "default_rng", rng) + monkeypatch.setattr(cohort.audioseal_oracle, "available", lambda: True) + monkeypatch.setattr(cohort.audioseal_oracle, "load_pinned_models", lambda: (None, None)) + monkeypatch.setattr(cohort.audioseal_oracle, "embed", lambda _m, data, _msg: data + 0.01) + monkeypatch.setattr(cohort, "require_tools", lambda: "fake") + monkeypatch.setattr(cohort, "oracle_dependencies", lambda: {"audioseal": "test"}) + manifest = cohort.build_cohort(tmp_path / "audio", carriers=("white_noise",), attacks=(), duration_s=0.01) + rows = [json.loads(line) for line in manifest.read_text().splitlines()] + clean = next(row for row in rows if row["case_id"] == "white_noise-clean") + hard = next(row for row in rows if row["arm"] == "hard_negative") + assert clean["seed"] == experiment.carrier_seed("white_noise") + assert clean["seed"] in observed + assert hard["seed"] in observed + + +def test_video_hard_negative_manifest_records_executed_seed(monkeypatch, tmp_path): + import json + + import watermark_benchmark_video_cohort as cohort + + observed = [] + original = np.random.default_rng + + def rng(seed): + observed.append(seed) + return original(seed) + + monkeypatch.setattr(cohort.np.random, "default_rng", rng) + monkeypatch.setattr(cohort, "FRAME_COUNT", 1) + monkeypatch.setattr(cohort, "HEIGHT", 8) + monkeypatch.setattr(cohort, "WIDTH", 128) + monkeypatch.setattr(cohort.videoseal_oracle, "load_model", lambda: None) + monkeypatch.setattr(cohort, "require_tools", lambda: "fake") + monkeypatch.setattr(cohort, "oracle_dependencies", dict) + + def encode(_f, path, frames): + path.write_bytes(frames.tobytes()) + return path + + monkeypatch.setattr(cohort, "encode_clip", encode) + manifest = cohort.build_cohort(tmp_path / "video", carriers=(), attacks=()) + rows = [json.loads(line) for line in manifest.read_text().splitlines()] + assert rows[0]["seed"] == observed[-1] + + +def test_video_artifact_rejects_a_change_during_decode(monkeypatch, tmp_path): + import watermark_benchmark as benchmark + + path = tmp_path / "clip" + path.write_bytes(b"initial") + + def decode(source): + source.write_bytes(b"changed") + return np.zeros((1, 2, 2, 3)) + + monkeypatch.setattr(benchmark, "_decode_video", decode) + with pytest.raises(ValueError, match="changed during decoding"): + benchmark.decode_video_artifact(path) + + +@pytest.mark.parametrize("code", [0, 2]) +def test_smoke_accepts_only_declared_outcomes(monkeypatch, tmp_path, code): + import smoke_matrix as smoke + + monkeypatch.setattr(smoke.subprocess, "run", lambda *a, **k: SimpleNamespace(returncode=code, stdout="", stderr="")) + assert smoke.Runner(tmp_path).run("visible", ["visible", "input.png"], expect_exit=(0, 2)).status == "pass" + + +def test_web_real_iterator_persists_before_late_page_failure(monkeypatch, tmp_path): + import json + from contextlib import nullcontext + + import playwright.sync_api as playwright + import provider_oracle_web as web + import provider_oracles as oracles + from PIL import Image + + sources = [] + for name, color in [("one", "red"), ("two", "blue")]: + path = tmp_path / f"{name}.png" + Image.new("RGB", (8, 8), color).save(path) + sources.append(path) + slot = { + "name": "meta-direct", + "surface": "meta-web", + "account_label": None, + "browser_profile": None, + "google_account_index": None, + "network_label": "direct", + "proxy_url_env": None, + } + manifest = oracles.prepare_batch( + "meta-web", sources, output_dir=tmp_path / "batch", repository_root=ROOT, slot=slot + ) + pages = [] + + def new_page(): + if pages: + saved = json.loads((manifest.parent / "results.json").read_text()) + assert saved["rows"][0]["watermark_result"] == "detected" + raise RuntimeError("later page failed") + page = SimpleNamespace(close=lambda: None) + pages.append(page) + return page + + context = SimpleNamespace(new_page=new_page, close=lambda: None) + browser = SimpleNamespace(close=lambda: None) + monkeypatch.setattr(playwright, "sync_playwright", lambda: nullcontext(SimpleNamespace(chromium=None))) + monkeypatch.setattr(web, "_launch_browser", lambda *a, **k: browser) + monkeypatch.setattr(web, "_new_context", lambda *a, **k: context) + monkeypatch.setattr(web, "_check_page", lambda *a: web.WebVerdict("detected", "unavailable", "settled response")) + with pytest.raises(RuntimeError, match="later page failed"): + web.run_web_batch(manifest, acknowledge_uploads=True, environ={}) + saved = json.loads((manifest.parent / "results.json").read_text()) + assert saved["rows"][0]["watermark_result"] == "detected" + assert saved["rows"][1]["watermark_result"] is None + submitted = [] + + def remaining(surface, uploads, *a, **k): + submitted.extend(uploads) + return [web.WebVerdict("not_detected", "unavailable", "second settled")] + + monkeypatch.setattr(web, "_submit_batch", remaining) + web.run_web_batch(manifest, acknowledge_uploads=True, environ={}) + assert len(submitted) == 1 + + +def test_recall_main_keeps_distinct_content_with_identical_observations(monkeypatch, tmp_path): + import csv + import json + + import visible_recall_sample as recall + from PIL import Image + + rows = [] + for index, color in enumerate(("red", "blue", "red")): + path = tmp_path / f"{index}.png" + Image.new("RGB", (32, 32), color).save(path) + rows.append( + { + "path": str(path), + "shape": [32, 32], + "uid": str(index), + "cls": "tc260", + "marks": {"x": {"conf": 0.5, "strict": False}}, + } + ) + scan = tmp_path / "scan.jsonl" + scan.write_text("".join(json.dumps(row) + "\n" for row in rows)) + out = tmp_path / "out" + monkeypatch.setattr(sys, "argv", ["sample", str(scan), str(out), "--tc260", "10"]) + recall.main() + with (out / "MANIFEST_DO_NOT_OPEN.csv").open() as source: + sampled = list(csv.DictReader(source)) + assert {row["uid"] for row in sampled} == {"0", "1"} + + +def test_real_rectangular_video_decode_preserves_pixels(tmp_path): + import shutil + + import watermark_benchmark as benchmark + + ffmpeg = shutil.which("ffmpeg") + if ffmpeg is None or shutil.which("ffprobe") is None: + pytest.skip("ffmpeg and ffprobe required") + pixels = np.arange(8 * 16 * 3, dtype=np.uint8).reshape(1, 8, 16, 3) + path = tmp_path / "rectangular.mkv" + subprocess.run( # noqa: S603 -- resolved ffmpeg and generated input + [ + ffmpeg, + "-loglevel", + "error", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-s", + "16x8", + "-i", + "pipe:0", + "-frames:v", + "1", + "-c:v", + "ffv1", + str(path), + ], + input=pixels.tobytes(), + capture_output=True, + check=True, + ) + decoded = benchmark._decode_video(path) + assert decoded.shape == pixels.shape + assert np.array_equal(decoded, pixels.astype(np.float32) / 255) + + +@pytest.mark.parametrize(("name", "function"), [("visible_positives", "_run_batch"), ("pill_gate_audit", "_batch")]) +def test_real_audit_worker_entry_points(name, function, tmp_path): + module = importlib.import_module(name) + # A missing synthetic path still reaches real imread inside the isolated child. + path = str(tmp_path / "missing.png") + rows = getattr(module, function)([path], 1, 10) + assert rows[0]["status"] == "unreadable" diff --git a/tests/test_fidelity_matching.py b/tests/test_fidelity_matching.py index a0f1d6e..a432952 100644 --- a/tests/test_fidelity_matching.py +++ b/tests/test_fidelity_matching.py @@ -42,8 +42,8 @@ def _load_assign(): return _load_module().assign_faces_one_to_one -def test_cer_remains_case_sensitive() -> None: - assert _load_module()._cer("A", "a") == 1.0 +def test_text_ned_remains_case_sensitive() -> None: + assert _load_module()._text_ned("A", "a") == 1.0 def test_distinct_faces_match_nearest() -> None: diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py new file mode 100644 index 0000000..ee1e586 --- /dev/null +++ b/tests/test_maintenance.py @@ -0,0 +1,67 @@ +"""The maintenance gate must stop when the security scanner fails.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +GATE = Path(__file__).resolve().parents[1] / "maintain.sh" +BASH = shutil.which("bash") +pytestmark = pytest.mark.skipif(os.name != "posix" or BASH is None, reason="The maintenance entry point requires Bash") + + +@pytest.mark.parametrize( + ("scanner_output", "scanner_status"), + [ + ("No vulnerabilities or maintenance issues detected", 0), + ("Vulnerabilities detected!", 2), + ("Scanner failed before producing a verdict", 1), + ("No vulnerabilities or maintenance issues detected\nScanner teardown failed", 1), + ("All dependencies appear safe\nScanner teardown failed", 1), + ], +) +def test_security_exit_status_controls_the_gate(tmp_path, scanner_output, scanner_status): + commands = tmp_path / "commands.log" + for name, body in { + "uv": 'printf "uv %s\\n" "$*" >> "$GATE_TEST_COMMANDS"\n', + "uvx": ( + 'printf "uvx %s\\n" "$*" >> "$GATE_TEST_COMMANDS"\n' + 'if [ "$1" = uv-secure ]; then\n' + ' printf "%s\\n" "$GATE_TEST_OUTPUT"\n' + ' exit "$GATE_TEST_STATUS"\n' + "fi\n" + ), + }.items(): + executable = tmp_path / name + executable.write_text("#!/bin/sh\n" + body) + executable.chmod(0o755) + + assert BASH is not None + result = subprocess.run( # noqa: S603 -- fixed repository script, isolated command stubs + [BASH, str(GATE)], + cwd=tmp_path, + env={ + **os.environ, + "PATH": str(tmp_path) + os.pathsep + os.environ.get("PATH", ""), + "GATE_TEST_COMMANDS": str(commands), + "GATE_TEST_OUTPUT": scanner_output, + "GATE_TEST_STATUS": str(scanner_status), + }, + capture_output=True, + text=True, + check=False, + ) + + calls = commands.read_text().splitlines() + assert calls[:3] == ["uv sync --all-extras", "uvx uv-outdated", "uvx uv-secure uv.lock"] + assert scanner_output in result.stdout + if scanner_status: + assert result.returncode != 0 + assert len(calls) == 3 + else: + assert result.returncode == 0 + assert calls[-1] == "uv run pytest -n auto" diff --git a/tests/test_metadata_audit_regressions.py b/tests/test_metadata_audit_regressions.py new file mode 100644 index 0000000..52f7c8e --- /dev/null +++ b/tests/test_metadata_audit_regressions.py @@ -0,0 +1,267 @@ +"""Regression tests for container preservation and collection completeness.""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import piexif +import pytest +from PIL import Image +from PIL.PngImagePlugin import PngInfo + +from remove_ai_watermarks.identify import identify, identify_metadata_record +from remove_ai_watermarks.metadata import remove_ai_metadata +from remove_ai_watermarks.metadata_record import collect_metadata_record + + +def test_small_compressed_xmp_matches_portable_verdict(tmp_path): + source = tmp_path / "compressed.png" + info = PngInfo() + info.add_text("XML:com.adobe.xmp", "ChatGPT", zip=True) + Image.new("RGB", (16, 16)).save(source, pnginfo=info) + assert source.stat().st_size < 1024 + assert b"ChatGPT" not in source.read_bytes() + direct = identify(source, check_visible=False, check_invisible=False) + portable = identify_metadata_record(collect_metadata_record(source), path=source) + assert portable.platform is not None + assert direct.to_dict() == portable.to_dict() + + +def test_jpeg_remove_all_drops_standard_exif(tmp_path): + source = tmp_path / "source.jpg" + exif = { + "0th": {piexif.ImageIFD.Artist: b"Test Artist", piexif.ImageIFD.Orientation: 6}, + "GPS": {piexif.GPSIFD.GPSLatitudeRef: b"N", piexif.GPSIFD.GPSLatitude: ((1, 1), (2, 1), (3, 1))}, + } + Image.new("RGB", (16, 8)).save(source, exif=piexif.dump(exif)) + assert piexif.load(str(source))["GPS"] + output = tmp_path / "clean.jpg" + remove_ai_metadata(source, output, keep_standard=False) + assert not piexif.load(str(output))["0th"] + assert not piexif.load(str(output))["GPS"] + + +def test_existing_directory_is_failed_collection(tmp_path): + record = collect_metadata_record(tmp_path) + assert record["status"] == "error" + assert record["issues"] + with pytest.raises(ValueError, match="collection failed"): + identify_metadata_record(record, path=tmp_path) + + +def test_read_error_after_successful_stat_is_failed_collection(tmp_path, monkeypatch): + source = tmp_path / "source.png" + Image.new("RGB", (8, 8)).save(source) + import remove_ai_watermarks.metadata_record as collector + + def denied(*args, **kwargs): + raise PermissionError("synthetic read failure") + + monkeypatch.setattr(collector, "open", denied, raising=False) + record = collect_metadata_record(source) + assert record["status"] == "error" + assert record["issues"] + with pytest.raises(ValueError, match="collection failed"): + identify_metadata_record(record, path=source) + + +def _run(arguments): + return subprocess.run(arguments, capture_output=True, text=True, check=True) # noqa: S603 + + +def _packets(path): + return json.loads( + _run( + [ + shutil.which("ffprobe"), + "-v", + "error", + "-show_streams", + "-show_packets", + "-show_data_hash", + "sha256", + "-of", + "json", + str(path), + ] + ).stdout + ) + + +@pytest.mark.skipif(not shutil.which("ffmpeg") or not shutil.which("ffprobe"), reason="ffmpeg/ffprobe required") +@pytest.mark.parametrize("in_place", [False, True]) +def test_remux_preserves_every_stream_and_packet(tmp_path, in_place): + source = tmp_path / "source.mkv" + _run( + [ + shutil.which("ffmpeg"), + "-v", + "error", + "-f", + "lavfi", + "-i", + "sine=frequency=440:duration=0.1", + "-f", + "lavfi", + "-i", + "sine=frequency=880:duration=0.1", + "-map", + "0:a", + "-map", + "1:a", + "-c:a", + "pcm_s16le", + "-metadata", + "title=Suno AI generated", + str(source), + ] + ) + before = _packets(source) + assert len(before["streams"]) == 2 + output = source if in_place else tmp_path / "clean.mkv" + remove_ai_metadata(source, None if in_place else output) + after = _packets(output) + assert len(after["streams"]) == 2 + assert [(p["stream_index"], p["data_hash"]) for p in after["packets"]] == [ + (p["stream_index"], p["data_hash"]) for p in before["packets"] + ] + assert b"Suno AI generated" not in output.read_bytes() + + +@pytest.mark.parametrize("in_place", [False, True]) +def test_failed_remux_does_not_publish_partial_output(tmp_path, monkeypatch, in_place): + source = tmp_path / "source.mkv" + source.write_bytes(b"original media") + output = source if in_place else tmp_path / "clean.mkv" + if not in_place: + output.write_bytes(b"existing output") + original = source.read_bytes() + previous = output.read_bytes() + + def fail(cmd, **kwargs): + Path(cmd[-1]).write_bytes(b"partial remux") + return subprocess.CompletedProcess(cmd, 1, stderr="synthetic failure") + + monkeypatch.setattr(shutil, "which", lambda _: "/synthetic/ffmpeg") + monkeypatch.setattr(subprocess, "run", fail) + with pytest.raises(RuntimeError, match="synthetic failure"): + remove_ai_metadata(source, output) + assert source.read_bytes() == original + assert output.read_bytes() == previous + assert set(tmp_path.iterdir()) == {source, output} + + +@pytest.mark.parametrize("stage", ["regions", "trailer", "decoder"]) +def test_later_collection_failure_is_partial(tmp_path, monkeypatch, stage): + import remove_ai_watermarks.metadata_record as collector + + source = tmp_path / "source.png" + Image.new("RGB", (8, 8)).save(source) + called = [] + + def fail(*args, **kwargs): + called.append(True) + raise RuntimeError("synthetic parser failure") + + helper = {"regions": "_container_regions", "trailer": "_trailer", "decoder": "_decoder_info"}[stage] + monkeypatch.setattr(collector, helper, fail) + record = collect_metadata_record(source) + assert called + assert record["status"] == "partial" + assert {"stage": stage, "code": "collection-failed"} in record["issues"] + with pytest.raises(ValueError, match="collection status"): + identify_metadata_record(record, path=source) + + +@pytest.mark.parametrize("container", ["png", "webp", "isobmff"]) +def test_structural_read_failure_reaches_collection_status(tmp_path, monkeypatch, container): + import remove_ai_watermarks.metadata as metadata + from remove_ai_watermarks._internal import isobmff + + source = tmp_path / f"source.{container}" + if container == "isobmff": + source.write_bytes(b"\x00\x00\x00\x18ftypavif\x00\x00\x00\x00avifmif1") + else: + Image.new("RGB", (8, 8)).save(source, format=container) + called = [] + + def denied(*args, **kwargs): + called.append(True) + raise PermissionError("synthetic structural read failure") + + module = isobmff if container == "isobmff" else metadata + monkeypatch.setattr(module, "open", denied, raising=False) + record = collect_metadata_record(source) + assert called + assert record["status"] == "partial" + assert {"stage": "regions", "code": "collection-failed"} in record["issues"] + + +def test_trailer_read_failure_reaches_collection_status(tmp_path, monkeypatch): + import remove_ai_watermarks.metadata as metadata + + source = tmp_path / "source.jpg" + Image.new("RGB", (8, 8)).save(source) + + def denied(*args, **kwargs): + raise PermissionError("synthetic trailer read failure") + + monkeypatch.setattr(metadata, "open", denied, raising=False) + record = collect_metadata_record(source) + assert record["status"] == "partial" + assert record["issues"] == [{"stage": "trailer", "code": "collection-failed"}] + + +@pytest.mark.parametrize("serialization_failure", [False, True]) +def test_c2pa_reader_failure_is_partial_even_after_tolerant_cached_read(tmp_path, monkeypatch, serialization_failure): + from remove_ai_watermarks._internal import c2pa + + source = tmp_path / "source.png" + Image.new("RGB", (8, 8)).save(source) + calls = [] + + class BrokenReader: + @staticmethod + def try_create(path): + calls.append(path) + if not serialization_failure: + raise RuntimeError("synthetic C2PA open failure") + return BrokenReader() + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def json(self): + raise RuntimeError("synthetic C2PA serialization failure") + + monkeypatch.setattr(c2pa, "_C2PA_READER_AVAILABLE", True) + monkeypatch.setattr(c2pa, "_C2paReader", BrokenReader) + assert c2pa.read_manifest_store_json(source) is None + assert calls + first = collect_metadata_record(source) + second = collect_metadata_record(source) + assert first["status"] == second["status"] == "partial" + assert first["issues"] == [{"stage": "c2pa", "code": "collection-failed"}] + assert len(calls) == 3, "failed strict reads must be retried, never cached as absence" + + +def test_exif_parser_failure_is_partial(tmp_path, monkeypatch): + source = tmp_path / "source.jpg" + Image.new("RGB", (8, 8)).save(source, exif=piexif.dump({"0th": {piexif.ImageIFD.Artist: b"Test Artist"}})) + calls = [] + + def fail(*args, **kwargs): + calls.append(True) + raise RuntimeError("synthetic EXIF parser failure") + + monkeypatch.setattr(piexif, "load", fail) + record = collect_metadata_record(source) + assert calls + assert record["status"] == "partial" + assert record["issues"] == [{"stage": "decoder", "code": "collection-failed"}] diff --git a/tests/test_provider_oracles.py b/tests/test_provider_oracles.py index 2ff47cc..a3dd40d 100644 --- a/tests/test_provider_oracles.py +++ b/tests/test_provider_oracles.py @@ -583,6 +583,9 @@ def test_microsoft_check_uses_named_slot_and_dotenv_configuration( calls.append({"path": path, **kwargs}) return SimpleNamespace(to_dict=lambda: {"status": "not_detected", "provenance_status": "absent"}) + fixture_variables = {line.split("=", 1)[0]: None for line in env_path.read_text(encoding="utf-8").splitlines()} + for name in fixture_variables: + monkeypatch.setenv(name, "inherited-value") monkeypatch.setattr(oracles, "verify_microsoft", verify) result = CliRunner().invoke( oracles.cli, @@ -599,7 +602,7 @@ def test_microsoft_check_uses_named_slot_and_dotenv_configuration( "--env-file", str(env_path), ], - env={"AZURE_CONTENT_SAFETY_ENDPOINT": "", "AZURE_CONTENT_SAFETY_SUBSCRIPTION_ID": ""}, + env=fixture_variables, ) assert result.exit_code == 0, result.output diff --git a/tests/test_qwen_zimage_pipeline.py b/tests/test_qwen_zimage_pipeline.py index 70cd14c..4c1304f 100644 --- a/tests/test_qwen_zimage_pipeline.py +++ b/tests/test_qwen_zimage_pipeline.py @@ -1043,13 +1043,22 @@ def test_chroma_zimage_inherits_the_shared_stages_and_only_swaps_the_global(): assert ChromaZImagePipeline.profile_name == "chroma-zimage" -def test_chroma_requested_steps_compensate_for_the_diffusers_truncation(): - from remove_ai_watermarks._internal.chroma_zimage_pipeline import CHROMA_STEPS, requested_steps +@pytest.mark.parametrize( + ("strength", "requested", "effective"), + [(0.09, 45, 5), (0.20, 20, 4), (0.125, 32, 4), (0.17, 24, 5), (0.40, 10, 4)], +) +def test_chroma_preserves_calibrated_requests_and_actual_scheduler_steps(strength, requested, effective): + from types import SimpleNamespace - for strength in (0.20, 0.125, 0.17, 0.40): - steps = requested_steps(CHROMA_STEPS, strength) - assert int(steps * strength) >= CHROMA_STEPS - assert int(CHROMA_STEPS * strength) < CHROMA_STEPS + diffusers = pytest.importorskip("diffusers") + from remove_ai_watermarks._internal.chroma_zimage_pipeline import CHROMA_NOMINAL_STEPS, requested_steps + + steps = requested_steps(CHROMA_NOMINAL_STEPS, strength) + assert steps == requested + pipeline = SimpleNamespace(scheduler=SimpleNamespace(timesteps=list(range(steps)), order=1)) + timesteps, count = diffusers.ChromaImg2ImgPipeline.get_timesteps(pipeline, steps, strength, "cpu") + assert count == effective + assert len(timesteps) == effective def test_chroma_zimage_uses_its_own_calibrated_prompts(): @@ -1061,12 +1070,12 @@ def test_chroma_zimage_uses_its_own_calibrated_prompts(): assert chroma_mod.CHROMA_PROMPT != _GLOBAL_PROMPT assert chroma_mod.CHROMA_PROMPT == "high quality, sharp, detailed, faithful to the original" assert chroma_mod.CHROMA_GUIDANCE == 5.0 - assert chroma_mod.CHROMA_STEPS == 4 + assert chroma_mod.CHROMA_NOMINAL_STEPS == 4 def test_chroma_global_stage_calls_diffusers_with_the_calibrated_shape(monkeypatch): """The calibrated call shape must not drift: prompt, guidance, width/height - on the /16 grid, effective-step count, and no Canny conditioning. A silent + on the /16 grid, requested-step count, and no Canny conditioning. A silent parameter change moves the oracle floors without any test seeing it.""" # CI's base job installs the library without the qwen-zimage extra. diffusers = pytest.importorskip("diffusers") diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py new file mode 100644 index 0000000..124eb07 --- /dev/null +++ b/tests/test_release_workflows.py @@ -0,0 +1,166 @@ +"""Execute release workflow scripts against local, controlled service replies.""" + +# These tests execute repository-owned workflow snippets against synthetic services. +# ruff: noqa: S603, S607 + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] + + +def _workflow(name: str) -> dict: + return yaml.safe_load((ROOT / ".github/workflows" / name).read_text()) + + +def _step(name: str, job: str, title: str) -> dict: + return next(s for s in _workflow(name)["jobs"][job]["steps"] if s.get("name") == title) + + +@pytest.mark.parametrize( + ("dependency", "accepted"), + [ + ("remove-ai-watermarks>=0.38.0", True), + ("Remove_AI_Watermarks[visible]>=0.38.0,<1", True), + ("other-package>=0.38.0", False), + ("remove-ai-watermarks>=0.38.01", False), + ("remove-ai-watermarks>=0.37.0", False), + ("remove-ai-watermarks>=0.38.0,!=0.38.0", False), + ], +) +def test_registry_check_parses_distribution_and_exact_floor(dependency: str, accepted: bool) -> None: + script = _step("verify-release.yml", "verify", "Verify ComfyUI registry node requires this release")["run"] + code = re.search(r'python3 -c "\n(.*?)\n"\)', script, re.S) + assert code is not None + result = subprocess.run( + [sys.executable, "-c", code[1].replace('\\"', '"')], + input=json.dumps([{"dependencies": [dependency], "version": "1", "status": "active"}]), + text=True, + capture_output=True, + env={**os.environ, "VERSION": "0.38.0"}, + ) + assert (result.returncode == 0) == accepted, result.stderr + + +def test_distributed_version_survives_a_newer_pypi_release(tmp_path: Path) -> None: + producer = _step("distribute.yml", "resolve", "Record distributed release") + consumer = _step("verify-release.yml", "verify", "Resolve target version") + env = { + **os.environ, + "VERSION": "0.37.0", + "GITHUB_RUN_ID": "123", + "GITHUB_RUN_ATTEMPT": "2", + "GITHUB_OUTPUT": str(tmp_path / "output"), + "EVENT_NAME": "workflow_run", + "INPUT_VERSION": "", + "DISTRIBUTION_RUN_ID": "123", + "DISTRIBUTION_RUN_ATTEMPT": "2", + } + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + curl = bin_dir / "curl" + curl.write_text("#!/bin/sh\ncat <<'JSON'\n" + json.dumps({"info": {"version": "0.99.0"}}) + "\nJSON\n") + curl.chmod(0o755) + env["PATH"] = str(bin_dir) + os.pathsep + env["PATH"] + subprocess.run(["bash", "-eu", "-c", producer["run"]], cwd=tmp_path, env=env, check=True) + artifact = tmp_path / "distribution-identities" / "distributed-release-2" + artifact.mkdir(parents=True) + identity = artifact / "release-identity.json" + identity.write_bytes((tmp_path / "release-identity.json").read_bytes()) + # A failed-job rerun has no new resolve artifact and must keep this identity. + env["DISTRIBUTION_RUN_ATTEMPT"] = "3" + future = tmp_path / "distribution-identities" / "distributed-release-4" + future.mkdir() + (future / "release-identity.json").write_text( + json.dumps({"version": "0.99.0", "run_id": "123", "run_attempt": "4"}) + ) + + result = subprocess.run( + ["bash", "-eu", "-c", consumer["run"]], cwd=tmp_path, env=env, capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr + assert "version=0.37.0" in (tmp_path / "output").read_text() + uploads = [ + s for s in _workflow("distribute.yml")["jobs"]["resolve"]["steps"] if "upload-artifact@" in s.get("uses", "") + ] + downloads = [ + s + for s in _workflow("verify-release.yml")["jobs"]["verify"]["steps"] + if "download-artifact@" in s.get("uses", "") + ] + assert uploads + assert downloads + assert downloads[0]["with"]["run-id"] == "${{ github.event.workflow_run.id }}" + assert "github.run_attempt" in uploads[0]["with"]["name"] + assert downloads[0]["with"]["pattern"] == "distributed-release-*" + assert _workflow("verify-release.yml")["permissions"]["actions"] == "read" + for altered in ( + {"DISTRIBUTION_RUN_ATTEMPT": "1"}, + {"DISTRIBUTION_RUN_ID": "999"}, + ): + assert ( + subprocess.run( + ["bash", "-eu", "-c", consumer["run"]], + cwd=tmp_path, + env={**env, **altered}, + capture_output=True, + ).returncode + != 0 + ) + identity.write_text(json.dumps({"version": "0.38.0", "run_id": "123", "run_attempt": "3"})) + assert ( + subprocess.run( + ["bash", "-eu", "-c", consumer["run"]], + cwd=tmp_path, + env=env, + capture_output=True, + ).returncode + != 0 + ) + identity.write_text(json.dumps({"version": "0.38.0", "run_id": "999", "run_attempt": "2"})) + assert ( + subprocess.run(["bash", "-eu", "-c", consumer["run"]], cwd=tmp_path, env=env, capture_output=True).returncode + != 0 + ) + + +def test_clawhub_uses_local_install(tmp_path: Path) -> None: + script = _step("distribute.yml", "clawhub", "Publish the agent skill when its version moved")["run"] + script = script.replace("${{ github.event.release.name || github.ref_name }}", "test") + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + # A fresh runner installs the executable locally; no global clawhub exists. + npm = bin_dir / "npm" + npm.write_text('#!/bin/sh\nmkdir -p node_modules/.bin\ncp "$CLAW_STUB" node_modules/.bin/clawhub\n') + npm.chmod(0o755) + stub = tmp_path / "claw-stub" + stub.write_text( + '#!/bin/sh\nprintf "%s\\n" "$*" >> "$CALL_LOG"\n' + 'if [ "$1" = inspect ]; then cat skills/remove-ai-watermarks/SKILL.md; fi\n' + ) + stub.chmod(0o755) + skill = tmp_path / "skills/remove-ai-watermarks/SKILL.md" + skill.parent.mkdir(parents=True) + skill.write_text('metadata:\n version: "1.0.5"\n') + env = { + **os.environ, + "PATH": str(bin_dir) + ":/usr/bin:/bin", + "CLAWHUB_TOKEN": "synthetic", + "CLAW_STUB": str(stub), + "CALL_LOG": str(tmp_path / "calls"), + } + result = subprocess.run(["bash", "-c", script], cwd=tmp_path, env=env, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert (tmp_path / "calls").read_text().splitlines() == [ + "login --token synthetic", + "inspect remove-ai-watermarks --file SKILL.md", + ] diff --git a/tests/test_review_detection_contracts.py b/tests/test_review_detection_contracts.py new file mode 100644 index 0000000..f3d570e --- /dev/null +++ b/tests/test_review_detection_contracts.py @@ -0,0 +1,58 @@ +"""Regressions for provider confirmation and inter-mark comparisons.""" + +import numpy as np +import pytest + +from remove_ai_watermarks.watermark_registry import known_marks + + +@pytest.mark.parametrize( + ("producer", "confirmed"), + [(code, mark.key == "doubao") for mark in known_marks() for code in mark.tc260_producer_codes] + + [("001191110102MACQD9K64010000", True), ("DouBao", True), ("", False)], +) +def test_doubao_confirmation_reaches_the_video_plan(producer, confirmed): + from remove_ai_watermarks.video import _visible_removal_plan + from remove_ai_watermarks.video_visible import FrameLocalization, VideoScan + + box = (800, 470, 120, 30) + scan = VideoScan(960, 540, 30, tuple(FrameLocalization(i, 0.40, box) for i in range(12))) + regions, _, _ = _visible_removal_plan("doubao", scan, {"aigc_producer": producer}) + assert regions == ([box] if confirmed else [None]) * 12 + + +@pytest.mark.parametrize( + "markers", + [{"aigc_label": "China AIGC label (TC260); producer doubao"}, {"issuer": "ByteDance"}], +) +def test_doubao_confirmation_requires_structural_producer(markers): + from remove_ai_watermarks.video_visible import has_doubao_video_provenance + + assert not has_doubao_video_provenance(markers) + + +def test_baidu_margin_scores_the_declared_rival_templates(monkeypatch): + from remove_ai_watermarks import _text_mark_engine, baidu_engine + + scored = [] + + def score(mask, scale, config): + scored.append(config.asset_name) + return 0.1 + + monkeypatch.setattr(_text_mark_engine, "template_match_score", score) + engine = _text_mark_engine.TextMarkEngine(baidu_engine._CONFIG) + assert engine._rival_margin_ok(score=0.9, box_mask=np.zeros((20, 80), dtype=np.uint8), scale_base=960) + assert scored == list(baidu_engine._CONFIG.rivals) + + +def test_every_registered_text_engine_resolves_its_declared_rivals(): + from remove_ai_watermarks import _text_mark_engine, watermark_registry + + for key in watermark_registry._ENGINE_CLASS: + engine = watermark_registry._engine(key) + if isinstance(engine, _text_mark_engine.TextMarkEngine): + for asset in engine.config.rivals: + rival = _text_mark_engine._rival_config(asset) + assert rival is not None, (key, asset) + assert rival.asset_name == asset, (key, asset) diff --git a/tests/test_synthid_affine_lattice_probe.py b/tests/test_synthid_affine_lattice_probe.py index 397be0e..3be68af 100644 --- a/tests/test_synthid_affine_lattice_probe.py +++ b/tests/test_synthid_affine_lattice_probe.py @@ -309,55 +309,55 @@ def test_deskew_bank_recovers_small_rotation(periodic_fixture: tuple[np.ndarray, def test_registered_period_mode_uses_runtime_selected_period( periodic_fixture: tuple[np.ndarray, np.ndarray], monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: pixels, template = periodic_fixture runner = CliRunner() - with runner.isolated_filesystem(): - np.savez("template.npz", template=template) - Image.fromarray(pixels).save("image.png") - components = probe.RegisteredComponents( - raw_score=0.4, - amplitude_threshold=0.2, - selected_period=16.0, - spectral_period=16.0, - high_band_score=0.15, - confirmation=RegisteredConfirmationComponents( - period=16.0, - joint_coherence=0.5, - joint_amplitude=0.2, - unknown_codeword_fixed_confirmation=0.5, - selection_patches=8, - confirmation_patches=8, - ), - ) - monkeypatch.setattr(probe, "registered_components", lambda *_args: components) + np.savez(tmp_path / "template.npz", template=template) + Image.fromarray(pixels).save(tmp_path / "image.png") + components = probe.RegisteredComponents( + raw_score=0.4, + amplitude_threshold=0.2, + selected_period=16.0, + spectral_period=16.0, + high_band_score=0.15, + confirmation=RegisteredConfirmationComponents( + period=16.0, + joint_coherence=0.5, + joint_amplitude=0.2, + unknown_codeword_fixed_confirmation=0.5, + selection_patches=8, + confirmation_patches=8, + ), + ) + monkeypatch.setattr(probe, "registered_components", lambda *_args: components) - result = runner.invoke( - probe.main, - [ - "template.npz", - "image.png", - "--registered-period", - "--same-image-null", - "--patch-shift-consensus", - "--opponent-registered", - "--report-out", - "report.json", - ], - ) + result = runner.invoke( + probe.main, + [ + str(tmp_path / "template.npz"), + str(tmp_path / "image.png"), + "--registered-period", + "--same-image-null", + "--patch-shift-consensus", + "--opponent-registered", + "--report-out", + str(tmp_path / "report.json"), + ], + ) - assert result.exit_code == 0, result.output - report = json.loads(Path("report.json").read_text(encoding="utf-8")) - assert report["registered_period"] is True - assert report["same_image_null"] is True - assert report["patch_shift_consensus"] is True - assert report["opponent_registered"] is True - assert report["records"][0]["registered"]["selected_period"] == 16.0 - assert report["records"][0]["registered"]["decision_score"] == 2.0 - assert report["records"][0]["score"]["selected_period"] == 16.0 - assert report["records"][0]["same_image_null"]["joint_excess"] > 0.2 - assert report["records"][0]["patch_shift_consensus"]["joint_support_fraction"] == 1.0 - assert report["records"][0]["opponent_registered"]["decision_score"] > 1.0 + assert result.exit_code == 0, result.output + report = json.loads((tmp_path / "report.json").read_text(encoding="utf-8")) + assert report["registered_period"] is True + assert report["same_image_null"] is True + assert report["patch_shift_consensus"] is True + assert report["opponent_registered"] is True + assert report["records"][0]["registered"]["selected_period"] == 16.0 + assert report["records"][0]["registered"]["decision_score"] == 2.0 + assert report["records"][0]["score"]["selected_period"] == 16.0 + assert report["records"][0]["same_image_null"]["joint_excess"] > 0.2 + assert report["records"][0]["patch_shift_consensus"]["joint_support_fraction"] == 1.0 + assert report["records"][0]["opponent_registered"]["decision_score"] > 1.0 def test_registered_confirmation_uses_frozen_period_aware_gates( diff --git a/tests/test_synthid_audit_regressions.py b/tests/test_synthid_audit_regressions.py new file mode 100644 index 0000000..ab6b100 --- /dev/null +++ b/tests/test_synthid_audit_regressions.py @@ -0,0 +1,164 @@ +"""Regression coverage for research evidence contracts.""" + +import json +from dataclasses import asdict +from pathlib import Path + +import numpy as np +import pytest +import synthid_affine_lattice_probe as lattice +import synthid_periodic_tile_probe as periodic +import synthid_pixel_probe as pixel +import synthid_runtime_expert_scores as experts +from click.testing import CliRunner +from PIL import Image + + +def test_archive_imports(): + root = Path(__file__).resolve().parents[1] + assert Path(lattice.__file__).is_relative_to(root) + assert Path(experts.__file__).is_relative_to(root) + + +@pytest.mark.parametrize( + "detector_name", + [ + experts.synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID, + experts.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID, + ], +) +def test_registered_fallback_preserves_identity(monkeypatch, detector_name): + def detect(_path, *, image, register_scale): + return experts.synthid_detector.SynthIDDetection( + status="detected", + width=768, + height=768, + score=1.02, + threshold=1.05, + detector=detector_name if register_scale else experts.FIXED_EXPERT_NAME, + ) + + monkeypatch.setattr(experts.synthid_detector, "detect_synthid", detect) + observations = experts.score_pixels(np.zeros((768, 768, 3), dtype=np.uint8)) + routed = next(item for item in observations if item["name"] == detector_name) + assert routed["score"] == 1.02 + assert not next(item for item in observations if item["name"] == experts.REGISTERED_EXPERT_NAME)["supported"] + + +def test_period_selection_does_not_read_confirmation(monkeypatch): + pixels = np.random.default_rng(42).integers(0, 256, (64, 64, 3), dtype=np.uint8) + template = np.random.default_rng(43).normal(size=(8, 8, 3)) + monkeypatch.setattr(lattice, "_period_candidate_indices", lambda *_args: np.array([0, 1])) + monkeypatch.setattr(lattice, "_period_alias_candidate_indices", lambda *_args: np.array([0, 1])) + confirmation = {16.0: 0.1, 17.0: 0.9} + + def whitened(*_args, period, **_kwargs): + return (0.9 if period == 16.0 else 0.8, confirmation[period]) + + def amplitude(*_args, period, **_kwargs): + return lattice._AmplitudeScore(period, 0.9 if period == 16.0 else 0.8, confirmation[period], 0, 0, 0.0, 0.0) + + monkeypatch.setattr(lattice, "_content_whitened_score", whitened) + monkeypatch.setattr(lattice, "_amplitude_score", amplitude) + + def score(): + return lattice.score_lattice( + pixels, + template, + periods=np.array([16.0, 17.0]), + rotations_degrees=np.array([0.0]), + patch_size=32, + grid_size=4, + harmonic_count=4, + ) + + first = score() + confirmation.update({16.0: 0.9, 17.0: 0.1}) + second = score() + assert first.selected_period == second.selected_period == 16.0 + assert first.confirmation_whitened_match != second.confirmation_whitened_match + + +@pytest.mark.parametrize("size", [64, 65]) +def test_periodic_discover_save_load_registered_json(tmp_path, size): + paths = [] + for index in range(3): + path = tmp_path / f"{index}.png" + Image.fromarray(np.random.default_rng(index).integers(0, 256, (size, size, 3), dtype=np.uint8)).save(path) + paths.append(path) + model = periodic.discover_model(paths, tile_height=8, tile_width=8) + target = tmp_path / "model.npz" + periodic.save_model(target, model) + restored = periodic.load_model(target) + result = periodic.score_image(paths[0], restored, register=True) + assert type(result.row_shift) is int + assert type(result.column_shift) is int + assert json.loads(json.dumps(asdict(result)))["score"] == result.score + report = tmp_path / "scores.json" + command = CliRunner().invoke( + periodic.main, ["score", str(target), str(paths[0]), "--register", "--report-out", str(report)] + ) + assert command.exit_code == 0, command.exception + assert json.loads(report.read_text())["scores"] + + +@pytest.mark.parametrize("clean_shape", [(32, 32), (32, 128)]) +def test_removal_rejects_incomparable_geometry(tmp_path, clean_shape): + positive = tmp_path / "positive.png" + clean = tmp_path / "clean.png" + Image.fromarray(np.random.default_rng(1).integers(0, 256, (64, 64, 3), dtype=np.uint8)).save(positive) + Image.fromarray(np.random.default_rng(2).integers(0, 256, (*clean_shape, 3), dtype=np.uint8)).save(clean) + result = CliRunner().invoke(pixel.cli, ["removal", "--pos", str(positive), "--cleaned", str(clean)]) + assert result.exit_code != 0 + assert "geometry" in result.output.lower() + assert "attenuated" not in result.output + + +@pytest.mark.parametrize(("score", "verdict"), [(1.02, "abstain"), (1.06, "detected")]) +def test_fine_fallback_router_uses_its_own_threshold(monkeypatch, score, verdict): + from synthid_conformal_cascade import ExpertObservation + from synthid_routed_expert_bank import classify_routed + + def detect(_path, *, image, register_scale): + return experts.synthid_detector.SynthIDDetection( + status="indeterminate" if score < 1.05 else "detected", + width=768, + height=768, + score=score, + threshold=1.05, + detector=experts.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID + if register_scale + else experts.FIXED_EXPERT_NAME, + ) + + monkeypatch.setattr(experts.synthid_detector, "detect_synthid", detect) + observations = tuple( + ExpertObservation(**item) for item in experts.score_pixels(np.zeros((768, 768, 3), dtype=np.uint8)) + ) + result = classify_routed(observations) + assert result.verdict == verdict + assert result.fine_opponent_score == score + assert result.registered_score is None + if verdict == "detected": + assert result.selected_expert == experts.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID + + +def test_legacy_mislabeled_runtime_manifest_requires_rescoring(tmp_path): + from synthid_conformal_cascade import load_observation_records + + path = tmp_path / "legacy.json" + path.write_text( + json.dumps( + { + "schema_version": 1, + "records": [ + { + "id": "old", + "observations": [{"name": experts.REGISTERED_EXPERT_NAME, "supported": True, "score": 1.1}], + } + ], + } + ) + ) + with pytest.raises(ValueError, match="rescoring"): + load_observation_records(path) diff --git a/tests/test_synthid_pixel_probe.py b/tests/test_synthid_pixel_probe.py index dea6141..4971443 100644 --- a/tests/test_synthid_pixel_probe.py +++ b/tests/test_synthid_pixel_probe.py @@ -49,10 +49,11 @@ class TestCarrier: c = probe.carrier(_fixed_carrier()) assert np.isclose(probe.ncc(c, c), 1.0) - def test_ncc_mismatched_shape_is_zero(self): + def test_ncc_mismatched_shape_is_incomparable(self): a = probe.carrier(np.random.default_rng(1).standard_normal((8, 8))) b = probe.carrier(np.random.default_rng(2).standard_normal((16, 16))) - assert probe.ncc(a, b) == 0.0 + with np.testing.assert_raises_regex(ValueError, "geometry"): + probe.ncc(a, b) class TestConsistency: diff --git a/tests/test_synthid_research_manifest.py b/tests/test_synthid_research_manifest.py index 706609d..009ee36 100644 --- a/tests/test_synthid_research_manifest.py +++ b/tests/test_synthid_research_manifest.py @@ -250,3 +250,46 @@ def test_rejects_failed_source_control(tmp_path: Path): errors = manifest.audit_manifest(path) assert any("source_control must have a detected" in error for error in errors) + + +def test_external_negatives_require_evidence_for_every_verifier(tmp_path): + path = tmp_path / "negative.csv" + for target in sorted(manifest._TARGET_PROVIDERS): + for source in sorted(manifest._SOURCE_PROVIDERS - {target}): + for split in sorted(manifest._FINAL_SPLITS): + for verifier in sorted(manifest._VERIFIERS): + row = _row( + "a" * 64, + "b" * 64, + target_provider=target, + source_provider=source, + split=split, + synthid_outcome="not_detected", + verified_via=verifier, + oracle_session="oracle-1", + ) + _write_manifest(path, [row]) + errors = manifest.audit_manifest(path) + if verifier in manifest._MATCHING_VERIFIERS[target]: + assert errors == [] + else: + assert any("evidence_reference" in error for error in errors), (target, source, split, verifier) + row["evidence_reference"] = "https://example.test/original-record" + _write_manifest(path, [row]) + assert manifest.audit_manifest(path) == [] + + +def test_unlabeled_discovery_may_have_no_oracle(tmp_path): + row = _row( + "a" * 64, + "b" * 64, + source_provider="synthetic", + split="discovery", + synthid_outcome="not_checked", + verified_via="none", + oracle_session="", + oracle_checked_at="", + ) + path = tmp_path / "discovery.csv" + _write_manifest(path, [row]) + assert manifest.audit_manifest(path) == [] diff --git a/tests/test_synthid_routed_detector.py b/tests/test_synthid_routed_detector.py index d9d72cc..cc080ee 100644 --- a/tests/test_synthid_routed_detector.py +++ b/tests/test_synthid_routed_detector.py @@ -24,6 +24,8 @@ def test_detect_path_routes_one_scored_record(monkeypatch, tmp_path: Path) -> No "width": 1024, "height": 1024, "observations": [ + {"name": bank.synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID, "supported": False, "score": None}, + {"name": bank.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID, "supported": False, "score": None}, { "name": bank.synthid_detector.DETECTOR_ID, "supported": True, diff --git a/tests/test_synthid_routed_expert_bank.py b/tests/test_synthid_routed_expert_bank.py index 6589db1..c3a2a6f 100644 --- a/tests/test_synthid_routed_expert_bank.py +++ b/tests/test_synthid_routed_expert_bank.py @@ -23,6 +23,8 @@ def _observations( large_supported: bool = False, ) -> tuple[ExpertObservation, ...]: return ( + ExpertObservation(bank.synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID, False, None), + ExpertObservation(bank.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID, False, None), ExpertObservation(bank.synthid_detector.DETECTOR_ID, fixed_supported, fixed_score), ExpertObservation( bank.synthid_detector.REGISTERED_DETECTOR_ID, @@ -98,11 +100,21 @@ def test_cli_writes_hash_pinned_report(tmp_path: Path) -> None: observations_path.write_text( json.dumps( { - "schema_version": 1, + "schema_version": 2, "records": [ { "id": "candidate-1", "observations": [ + { + "name": bank.synthid_detector.OPPONENT_REGISTERED_DETECTOR_ID, + "supported": False, + "score": None, + }, + { + "name": bank.synthid_detector.FINE_OPPONENT_REGISTERED_DETECTOR_ID, + "supported": False, + "score": None, + }, { "name": bank.synthid_detector.DETECTOR_ID, "supported": True, diff --git a/tests/test_synthid_runtime_expert_scores.py b/tests/test_synthid_runtime_expert_scores.py index 71ba852..f4c3dd4 100644 --- a/tests/test_synthid_runtime_expert_scores.py +++ b/tests/test_synthid_runtime_expert_scores.py @@ -20,6 +20,8 @@ def test_unsupported_geometry_emits_no_synthetic_scores() -> None: {"name": scorer.FIXED_EXPERT_NAME, "supported": False, "score": None}, {"name": scorer.REGISTERED_EXPERT_NAME, "supported": False, "score": None}, {"name": scorer.LARGE_EXPERT_NAME, "supported": False, "score": None}, + {"name": scorer.OPPONENT_EXPERT_NAME, "supported": False, "score": None}, + {"name": scorer.FINE_OPPONENT_EXPERT_NAME, "supported": False, "score": None}, ] @@ -35,6 +37,7 @@ def test_supported_image_scores_each_expert_once(monkeypatch) -> None: height=1024, score=1.5 if register_scale else 0.25, threshold=1.0 if register_scale else 0.17, + detector=scorer.REGISTERED_EXPERT_NAME if register_scale else scorer.FIXED_EXPERT_NAME, ) monkeypatch.setattr(scorer.synthid_detector, "detect_synthid", detect) @@ -45,6 +48,8 @@ def test_supported_image_scores_each_expert_once(monkeypatch) -> None: {"name": scorer.FIXED_EXPERT_NAME, "supported": True, "score": 0.25}, {"name": scorer.REGISTERED_EXPERT_NAME, "supported": True, "score": 1.5}, {"name": scorer.LARGE_EXPERT_NAME, "supported": False, "score": None}, + {"name": scorer.OPPONENT_EXPERT_NAME, "supported": False, "score": None}, + {"name": scorer.FINE_OPPONENT_EXPERT_NAME, "supported": False, "score": None}, ] assert calls == {"fixed": 1, "registered": 1} @@ -63,12 +68,8 @@ def test_cli_writes_hash_pinned_observation_manifest(tmp_path: Path) -> None: assert result.exit_code == 0, result.output report = json.loads(report_path.read_text(encoding="utf-8")) - assert report["schema_version"] == 1 - assert report["experts"] == [ - scorer.FIXED_EXPERT_NAME, - scorer.REGISTERED_EXPERT_NAME, - scorer.LARGE_EXPERT_NAME, - ] + assert report["schema_version"] == 2 + assert report["experts"] == list(scorer.EXPERT_NAMES) assert len(report["records"][0]["id"]) == 64 assert report["records"][0]["width"] == 64 assert all(not observation["supported"] for observation in report["records"][0]["observations"]) @@ -94,4 +95,6 @@ def test_large_default_is_not_mislabeled_as_fixed(monkeypatch) -> None: {"name": scorer.FIXED_EXPERT_NAME, "supported": False, "score": None}, {"name": scorer.REGISTERED_EXPERT_NAME, "supported": False, "score": None}, {"name": scorer.LARGE_EXPERT_NAME, "supported": True, "score": 1.2}, + {"name": scorer.OPPONENT_EXPERT_NAME, "supported": False, "score": None}, + {"name": scorer.FINE_OPPONENT_EXPERT_NAME, "supported": False, "score": None}, ] diff --git a/tests/test_text_draft.py b/tests/test_text_draft.py index 4a8f3aa..5df6a1a 100644 --- a/tests/test_text_draft.py +++ b/tests/test_text_draft.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +import pytest from PIL import Image from remove_ai_watermarks.text_draft import ( @@ -77,15 +78,13 @@ class _FakeEngine: class _JitterEngine: - """Changes its answer with the crop height - exactly what the gate rejects.""" + """Changes its answer with the supplied crop dimensions - exactly what the gate rejects.""" def __init__(self, texts: list[str]) -> None: self._texts = texts - self._calls = 0 def predict(self, crop: Any) -> Any: - text = self._texts[self._calls % len(self._texts)] - self._calls += 1 + text = self._texts[(crop.shape[1] // 8) % len(self._texts)] yield {"rec_text": text, "rec_score": 0.97} @@ -154,5 +153,38 @@ class TestDraftTextLines: engines={"en": jitter, "ru": stable, "ch": stable}, ) (line,) = draft.accepted - assert line.text == "Hello" + assert line.text == "Hallo" assert line.min_score >= 0.75 + + +class TestActualCropJitter: + @pytest.mark.parametrize(("language", "prefix"), [("en", "Latin"), ("ru", "Кириллица"), ("ch", "中文")]) + def test_changes_real_crops_and_rejects_content_dependent_reads(self, tmp_path, language, prefix): + import numpy as np + + path = tmp_path / "gradient.png" + image = np.broadcast_to(np.arange(240, dtype=np.uint8)[:, None, None], (240, 400, 3)).copy() + Image.fromarray(image).save(path) + + class CropReader: + def __init__(self): + self.crops = [] + + def predict(self, crop): + self.crops.append(crop.copy()) + # A deterministic function of the supplied crop, never invocation order. + yield {"rec_text": f"{prefix}{int(crop[0, 0, 0])}", "rec_score": 0.99} + + reader = CropReader() + engines = {key: _FakeEngine("hello") for key in ("en", "ru", "ch")} + engines[language] = reader + draft = draft_text_lines( + path, + detector=_FakeDetector([(80, 80, 300, 180)]), + engines=engines, + ) + assert len(reader.crops) == 4 # Language probe plus three stability crops. + assert len({crop.tobytes() for crop in reader.crops[1:]}) == 3 + assert draft.accepted == () + assert len(draft.rejected) == 1 + assert draft.rejected[0].language == language diff --git a/tests/test_videoseal_oracle.py b/tests/test_videoseal_oracle.py index d6197f3..c9a90c0 100644 --- a/tests/test_videoseal_oracle.py +++ b/tests/test_videoseal_oracle.py @@ -59,16 +59,17 @@ class TestPins: class TestCacheOverride: def test_cache_dir_env_override_is_honored(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - monkeypatch.setenv("VIDEOSEAL_CACHE_DIR", str(tmp_path / "cache")) - + original_cache = videoseal_oracle.CACHE_DIR import importlib - importlib.reload(videoseal_oracle) - try: - assert tmp_path / "cache" == videoseal_oracle.CACHE_DIR + with monkeypatch.context() as environment: + environment.setenv("VIDEOSEAL_CACHE_DIR", str(tmp_path / "cache")) + importlib.reload(videoseal_oracle) + assert tmp_path / "cache" == videoseal_oracle.CACHE_DIR finally: importlib.reload(videoseal_oracle) + assert original_cache == videoseal_oracle.CACHE_DIR class TestAggregationFormulas: diff --git a/uv.lock b/uv.lock index 837bad3..07c8e05 100644 --- a/uv.lock +++ b/uv.lock @@ -225,15 +225,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -289,7 +289,7 @@ wheels = [ [[package]] name = "bce-python-sdk" -version = "0.9.76" +version = "0.9.79" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "crc32c" }, @@ -297,32 +297,29 @@ dependencies = [ { name = "pycryptodome" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ed/b41906366c0e1e3a1a883e0f1bcfc927403d9697d74b1eb7e89502870218/bce_python_sdk-0.9.76.tar.gz", hash = "sha256:01c630ce8dcbf8be0563d65f18b5201eaac6bd954b3dc776040aec268f81b6ff", size = 317399, upload-time = "2026-07-24T05:12:57.005Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/64/d31e61620ddaecb7039dc7b8f62eb75b95d2281f2d2016b6d6b22396e16f/bce_python_sdk-0.9.79.tar.gz", hash = "sha256:cd77476b43347ed28d0211d5ad557e524e1ec648bd093d57c6a6d3de075a7508", size = 322059, upload-time = "2026-09-01T05:48:52.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/06/4d4a26c3dfcdb29599f563329b87eefe55b02b3c2b9d8f8c3aeaa38a33a6/bce_python_sdk-0.9.76-py3-none-any.whl", hash = "sha256:e629181d060f4ed8f29749f47139bf08f1f9b0d8b15daedc956900785587e8d8", size = 435179, upload-time = "2026-07-24T05:12:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/ef/de/52b763b183e23f4dd73811a2dd2664824ae7fc6ceb1aa8dcd3f1102b1094/bce_python_sdk-0.9.79-py3-none-any.whl", hash = "sha256:71799ac8740505e0759d30873f6f1a478fa8f83aedf425d511f1419a5f30082e", size = 440268, upload-time = "2026-09-01T05:48:50.459Z" }, ] [[package]] name = "c2pa-python" -version = "0.37.8" +version = "0.37.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, - { name = "pytest" }, { name = "requests" }, - { name = "setuptools" }, { name = "toml" }, - { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/32/e93959d17db5af4078532f587b9638e87b6429ca0b51348a8ac6ad99e471/c2pa_python-0.37.8.tar.gz", hash = "sha256:c52eb07244558a5997fe61b6ed8d879b4f46f38642dd11956bcedf888c58649c", size = 113008, upload-time = "2026-08-27T20:40:06.081Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/e1/2917bd9ed1582d42903072c405c8787dcb1ce85bbb3b38aaeca3c2b2b61f/c2pa_python-0.37.10.tar.gz", hash = "sha256:4c383fe719d37f1bcb7434abb51fc56e3cc58f35bacc17287cf2fb083275805d", size = 112959, upload-time = "2026-09-04T19:13:50.636Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/6a/252d8d072ac4192df8da28771be64a54cb4ea73f08f3ca1f2b7fafa823a5/c2pa_python-0.37.8-py3-none-macosx_10_9_universal2.whl", hash = "sha256:8dae7d198dd583de3963529b60665e611e36ddca677f85700147d3bdbc885b27", size = 16186835, upload-time = "2026-08-27T20:39:35.421Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b7/3519bbb3735adce56eb0836d25fcf4e00c36c4e5cd480ee5277aadba18ed/c2pa_python-0.37.8-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d3ff8da9be41c6f74218872f8ebf0b0020dbc00202bb30fda751422418f679c5", size = 14230208, upload-time = "2026-08-27T20:39:38.587Z" }, - { url = "https://files.pythonhosted.org/packages/24/5c/ed3000ca3de522c17797482a90cd64f57cdd2fb9b21a49910c04327954bf/c2pa_python-0.37.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ee4fbbe3b98dd4349cc9c60db3c4fb1c349c65b5c72fb89a0de403ca778d3d0d", size = 13865982, upload-time = "2026-08-27T20:39:41.837Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bd/934002a34aa76e5136508b524734dc3c4719f6d87b5a98cf93cbf4917d05/c2pa_python-0.37.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e8c04f4220194a3392dda290be96181ffc94ee5008e87e1fa96c390ec4f9c53c", size = 14077063, upload-time = "2026-08-27T20:39:45.069Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e8/ca03c21e253e95a3aaab472697f1c0c766af852cb643879b4928f6c57298/c2pa_python-0.37.8-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20742bd702d09476390dc73a375883990291ea458af8f3e61ef48fa9928f2313", size = 14715089, upload-time = "2026-08-27T20:39:48.275Z" }, - { url = "https://files.pythonhosted.org/packages/44/cd/376d90d3073ad975d38a42bdbcb42cec071bff3b6831dd5a6b807526a1df/c2pa_python-0.37.8-py3-none-win_amd64.whl", hash = "sha256:5cf1dadd1c8de7da298db9c62c432180d7185a31e81a96dfae7bce7944b21ddd", size = 86478089, upload-time = "2026-08-27T20:39:55.511Z" }, - { url = "https://files.pythonhosted.org/packages/e1/df/3e59b7813be65678d28f88a7a0c7a13ba8c983fe2a1ecf406959dc7b88df/c2pa_python-0.37.8-py3-none-win_arm64.whl", hash = "sha256:af827698ead013ca20e829e3c9fe1846b548ae77d1ab84762118fbde340e3f0a", size = 83936189, upload-time = "2026-08-27T20:40:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f9/275640639c7dd1bea2e38d2ac0ffd09d42244893b09b82e4e4a1cfd77256/c2pa_python-0.37.10-py3-none-macosx_10_9_universal2.whl", hash = "sha256:8819dfc2992697abe8f0c6422230ddd7f39e7631c11924b02120e6c2b8a79216", size = 16133098, upload-time = "2026-09-04T19:13:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/3ed4c3e1b1efc754c2a38a830e269cd0edee76297cb036b046c2b6f7100e/c2pa_python-0.37.10-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fdaa9e0d8fd1090274c501649ee471626db02db2a6bfe292ca890db41e23b346", size = 14217523, upload-time = "2026-09-04T19:13:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/8a/e804d964eaf14bb86ddf16e62a7ca173dfbc9f067c976f94e297dfe861ba/c2pa_python-0.37.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f839cad2b43f2a48240c5ce8b3edb31c026f6dbcb2e79b896784348ebd7c5246", size = 13803003, upload-time = "2026-09-04T19:13:31.563Z" }, + { url = "https://files.pythonhosted.org/packages/3c/84/15f0321294575edc48b43c8c6f613e60ad8e1038262ed77278436b16b870/c2pa_python-0.37.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a96d87f45ea7986f31018b6cdbd88a9932ddbf14f551b8e850481373bf9ec7b2", size = 14063729, upload-time = "2026-09-04T19:13:34.464Z" }, + { url = "https://files.pythonhosted.org/packages/9a/54/815f9b116153f46647413007838eb913b1711badcf5ae8d0e65eaf512287/c2pa_python-0.37.10-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:a24d600a5d6a0759ae48a5a003690f7edfc175bdb86eca860016aef400778e61", size = 14755085, upload-time = "2026-09-04T19:13:37.551Z" }, + { url = "https://files.pythonhosted.org/packages/60/03/502ffca6024e50a83d4ac37b842dd59b095e23e0e1e84511ca6abfc72a71/c2pa_python-0.37.10-py3-none-win_amd64.whl", hash = "sha256:25ced040898ef3374e110b91e7d3754777950b5e992a87cde96937fa8ffa848a", size = 86751605, upload-time = "2026-09-04T19:13:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/8ad03f9316fc8b36d3a21c641ec4b0859714a43102be0bd4bd7587398494/c2pa_python-0.37.10-py3-none-win_arm64.whl", hash = "sha256:447c76a3e7ee40e2da55b4e61471d3bbc019e309716544db6e947f1fc9e5e303", size = 84081550, upload-time = "2026-09-04T19:13:47.665Z" }, ] [[package]] @@ -592,86 +589,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.3" +version = "7.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/9c/c8a3a923c24f631695cea2d5e2f02e776bc0af6e03800626e13a6c05a615/coverage-7.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f3f854ab4599d98f7799ac9b91e34e8ec9ebc9a6372ee8c1f3413a68cc8b5e9", size = 222328, upload-time = "2026-08-02T18:47:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/92/51/dda77f34cbd2513d6ffb898c901d19e9ca55f48c0cbc4a1eb173a97d157a/coverage-7.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75268348fee1f199653b8a846262aec5581c6bb008c4f58824959fb708cc688f", size = 222832, upload-time = "2026-08-02T18:47:51.219Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/e0faafc4c6e23bd76c76148875ee9ec5781b8f1cd62cea2bc4ca0f0f0e5d/coverage-7.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21081739f6264cc594cad2d42b62befbd17633824022866c68720eb0c4b8d6b4", size = 253250, upload-time = "2026-08-02T18:47:52.737Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/4b1e0eeb727ffb471e411c1bd3402184b5dd54a77a762b0e55e87cdf9ae3/coverage-7.15.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:718d366251b060c10731c7dd359de6caea72250036eb94576aa56dacbf830a11", size = 255160, upload-time = "2026-08-02T18:47:54.404Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/a602d2d48f9db9f795e578a86aa914f7b20008e9330902defcfb73d17b3a/coverage-7.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1bbaa502a6e877f3ee67cbac3eba2bb637f623e454e6c37b81b38896dbd48f", size = 257269, upload-time = "2026-08-02T18:47:56.157Z" }, - { url = "https://files.pythonhosted.org/packages/22/fa/bf6db13df2fcee00d2671849fe58c99232ee79a01fec7478c2bf7839b9e1/coverage-7.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:494880c9e60782610683f4eb9b65cce4f886673596b8f3cb2dfa079fc551c743", size = 259231, upload-time = "2026-08-02T18:47:57.76Z" }, - { url = "https://files.pythonhosted.org/packages/89/37/8118f13b17fa7d9a3aa2c301d93f2d5ffeef70fa7e27e639a74bdacd3fea/coverage-7.15.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3db264ea689f9e8f9fa4fb9005fee4048c3bff4a547f4cfa27f5086cb0804ec0", size = 253357, upload-time = "2026-08-02T18:47:59.261Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/c7b94fb03962f4d6f0fe13d01c4eb9c4c6e2e714a20d074516ec7582b110/coverage-7.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e869d4799674d67778e76ddbe2e26cf1673369262e231a8ec259421b1015fea", size = 254961, upload-time = "2026-08-02T18:48:00.901Z" }, - { url = "https://files.pythonhosted.org/packages/87/f9/fe0bd415fa56e36b62b649017c8fc98330858be4c7593789efb78cd24178/coverage-7.15.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:696fc7a28bbf717aba8d2c6963d26702945c7832cb313ba3b323aa5b1afb3156", size = 253024, upload-time = "2026-08-02T18:48:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7c/ffa53506d63ba8a77f5b9557dd6f5a5a5ad85adc680d7857410138f82bd9/coverage-7.15.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3fe9be1c527497d047f770d88a0110189714c36383bb88384508f750c302bffa", size = 256792, upload-time = "2026-08-02T18:48:04.377Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c6/df42458e72c18a49fe87e40ccd3fb0314210915256cf4a5593e1b3250e04/coverage-7.15.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2400591f4b2e33746c70846388f8bb4c7e33b820e31cb8c6cb2f25305310438b", size = 252744, upload-time = "2026-08-02T18:48:06.154Z" }, - { url = "https://files.pythonhosted.org/packages/f1/14/8bf18a4b10a44f8ba5f604b00e102f37daf49d581d66a37dc33fa267e1a6/coverage-7.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e557178799282269412a672e5753f2179edfe1b3f0f19b0c98f8e72d482326a", size = 253652, upload-time = "2026-08-02T18:48:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/e530c9bb94e4155817cbd149034105b062a6913bc356ae08f454d155de53/coverage-7.15.3-cp311-cp311-win32.whl", hash = "sha256:68ea6c947375982ae907e19e9d2ef156bd6e68e11f3566dd568d7f4ec974e715", size = 224428, upload-time = "2026-08-02T18:48:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/98/0050c692d120988f1973a15196f52dee4ae221848b760281461a2005b613/coverage-7.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:28743dad31622e8c474b17446118037361f5b1f4f2ecdf72d4f6fde246d64446", size = 224906, upload-time = "2026-08-02T18:48:11.611Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/c0ef3e2ba3f35fc1c6985811a40edd9331e5b8978c9ecf84699de3edacbe/coverage-7.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:c4398918c4fda32718191239e451fd86ac5ad1e8979b592f1921ee2d1f038965", size = 224448, upload-time = "2026-08-02T18:48:13.304Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6c/bac99d9d4c6abe856e93bf3f5212982ac0bfac126dd4a042753bd53bc5af/coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f", size = 222499, upload-time = "2026-08-02T18:48:15.018Z" }, - { url = "https://files.pythonhosted.org/packages/aa/bc/cb9a39b083bc1aa70586482dab25c9be20bab0ec6c155340e50d9066bb1e/coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60", size = 222866, upload-time = "2026-08-02T18:48:16.884Z" }, - { url = "https://files.pythonhosted.org/packages/58/fb/beaa453d62000a0a5b39838bee2a137afe609a50a71f55e83c73461e513b/coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f", size = 254367, upload-time = "2026-08-02T18:48:18.507Z" }, - { url = "https://files.pythonhosted.org/packages/66/64/43e72500ed6815cef189f9193f29d7af4b078830337c95ea976cd0c0d427/coverage-7.15.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63a4ff67364afb2cac826b8bbd78a5c50ce656a7b7137436b44d7b96a9271088", size = 257103, upload-time = "2026-08-02T18:48:20.172Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/2893e2937adfe02f45fd38e4a8a0a0d8b7a02ff9e012ac3d009bee3c4f16/coverage-7.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95e42856509675fe26560310313a6117640e96f9a1e19bb3d220116a27c94c", size = 258220, upload-time = "2026-08-02T18:48:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d5e6e2eb1a62961083734291304b1f85df72e2abe95c76eb88a7f472afd0/coverage-7.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abad631cba27094b4631993f4c72e89ac0ca1b3a0236c7abaf8ca79aea619851", size = 260481, upload-time = "2026-08-02T18:48:23.682Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c9/9b72c5c6a9798a9a12cf65f66e077cc1fdd396e61915c862688f9afe1cae/coverage-7.15.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b0807f1f051dd82a234ad6acdb6f1425baede60be1e84e862496c8cc9262ab9", size = 254749, upload-time = "2026-08-02T18:48:25.32Z" }, - { url = "https://files.pythonhosted.org/packages/92/20/e1c2f759e2dbce559ba85c40c0e4acfecc6cff4b740c294c88e41ccc6111/coverage-7.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d6df7aeb5bc464040bbc9ae173d875785d3677ebc4307817997d622d74225e", size = 256138, upload-time = "2026-08-02T18:48:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ab/48cc7e760f769e86ae290a125ea6e7209dfbdbbbb7ff4f5d9d1ee7a45d57/coverage-7.15.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:974471c506c9f5758808b47c1ebf7949ecd0848f5c1020e78675fefe5ff46866", size = 254283, upload-time = "2026-08-02T18:48:29.082Z" }, - { url = "https://files.pythonhosted.org/packages/15/26/39529a68154f99b3a1829debd8b25eac384effeec890a293b5bbdcb49186/coverage-7.15.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cba0c9c13e35c86df7998f1afaf6b1da224a3a39e4da59bdabf60c148046dcb", size = 258352, upload-time = "2026-08-02T18:48:30.892Z" }, - { url = "https://files.pythonhosted.org/packages/91/2f/55b82aa3d8d7dd8023a56e7c5c2a70e39a3c44b3353c6cf3faec9ad51566/coverage-7.15.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4d608dc36a364dce33acbf4fc3a50f9d2054c945f233bb0a2cdb4b90bfa17646", size = 253852, upload-time = "2026-08-02T18:48:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6d/839f4045124cd3518ecf2c58967e58a911202834e7c5a03cfdf2ab0b29f6/coverage-7.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2395869280554a1941da904423c12660c39f721315e1c02d076a7fe0971382f0", size = 255725, upload-time = "2026-08-02T18:48:34.848Z" }, - { url = "https://files.pythonhosted.org/packages/75/21/d25e3e2a9e327798078c877f469dfb6def860bf6e25036529046227d3e15/coverage-7.15.3-cp312-cp312-win32.whl", hash = "sha256:24f3b21840c3eb76cef3cc70b2bf6649010c64471a84a446538a39306e1ba04d", size = 224566, upload-time = "2026-08-02T18:48:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0f/df90cc1e8d095ce263968a93e04829821b2afb31ac2752c06a2e0a8e3c13/coverage-7.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:fa7b17902c3c1dd8a7adb52679b7f6340bba08443d710c8838e04db8cf62be2a", size = 225098, upload-time = "2026-08-02T18:48:38.941Z" }, - { url = "https://files.pythonhosted.org/packages/65/c7/ec49e43c58967a07163e2d1c6bbd58112b825b2772ab66784afd6a5400ba/coverage-7.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:fcbe83fb7258eacd293bf5322d88807acb35ed12a5cfa99dd8215c083e3b0235", size = 224485, upload-time = "2026-08-02T18:48:40.682Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/62ae61e1fc434956bec38ed1d5b1c494f58cf579dbd998e77abffe7b3e6b/coverage-7.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1182eed05674c63d40951fae27c43e822749f04d25f75df64c2e4fa3168678de", size = 222522, upload-time = "2026-08-02T18:48:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/13/ff/c74c673d81e0e77b6608c3d21331e3db42e30daeb3c8a0a8860d4c9e2e14/coverage-7.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c4b0d7c4cd56e470d0c9d8441f42e8a96cdfd95050fec027f1d4dd9f11006c", size = 222894, upload-time = "2026-08-02T18:48:44.274Z" }, - { url = "https://files.pythonhosted.org/packages/a1/91/ccb30f5ffafd7d69d0b18e5162f9b711a5654e807b7b0c13497f0826b33f/coverage-7.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5c9fce9f4998b0d50a753da765b9215a14decc7863822c89d72da7a89ca625b3", size = 253890, upload-time = "2026-08-02T18:48:46.097Z" }, - { url = "https://files.pythonhosted.org/packages/29/c6/e92a66cda49a2751b09826d51258f199b92aa0cb005bc5f34e9729a52a9c/coverage-7.15.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a47e2a0a0ace9241e70ee00e44520f88b843094603dd54303f1bafecd929c30", size = 256484, upload-time = "2026-08-02T18:48:47.846Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/730929164b457cf25cf76c23898b90f9039a104a647890801b6586797b14/coverage-7.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bad94f83807ae60ed76f3ac012f69b2605ac9ea81bee959a5a483f7fa09c10", size = 257723, upload-time = "2026-08-02T18:48:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/9e/be/04cb5672cb19f5c389eda81ba22d89807699a949653d3625b0e0fda169da/coverage-7.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:228e172a76c428bb17d1ab78a2ff188990b0597e5dbd291f52a4edf7412de049", size = 259854, upload-time = "2026-08-02T18:48:51.413Z" }, - { url = "https://files.pythonhosted.org/packages/96/25/5e7fd6af39f6507071455944b8906dd1fe5b7b6bffb6a163ceb20afa0d13/coverage-7.15.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cea9fb33887c99349996266f1fd60abe5af3577a90633392001d27ef46b4b66e", size = 254085, upload-time = "2026-08-02T18:48:53.158Z" }, - { url = "https://files.pythonhosted.org/packages/23/c8/55e58a853f1e61163a6e755897bd14a059d78411e86560f39d9951c019b5/coverage-7.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81760de3155d7f52c21860c4046628dc6bed182f72e3c028e2b4fd46f65aa040", size = 255850, upload-time = "2026-08-02T18:48:55.031Z" }, - { url = "https://files.pythonhosted.org/packages/be/74/8bcec66dbcf3d22bea2a0b2b77ee2fa6f766a647d0023d4eabbc4f2b2756/coverage-7.15.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b47ea0a1d3a3d089826c6cbfad8429d7d8872e28e86baa95ddef330f6875da21", size = 253818, upload-time = "2026-08-02T18:48:57.163Z" }, - { url = "https://files.pythonhosted.org/packages/ce/06/450b673fdfece0997b4e16a31d6bde6b18889c578f1013ddd34c962ac6f9/coverage-7.15.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5459ba486b2a5d58a6c05254779ecdf525e7f20174d0210ceda75ba40fdb8f2c", size = 257973, upload-time = "2026-08-02T18:48:59.098Z" }, - { url = "https://files.pythonhosted.org/packages/56/fd/3ec7409aec0ddc943132452b65672f065f043b844f1830e1fe173c98b3ab/coverage-7.15.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c59209f80a08dbfcdd5109a80dc623cd3b9d22895c85757d34f57a6e6e95570f", size = 253638, upload-time = "2026-08-02T18:49:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/75/20/30a8dabb194123631c93f860fdd86401ad405d56cfb1841873afbfe4e92b/coverage-7.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f863856c1779d4a5bb6a94698a2f9073e09c6706501f76f3e7780e72df97d21c", size = 255407, upload-time = "2026-08-02T18:49:03.143Z" }, - { url = "https://files.pythonhosted.org/packages/13/4d/e14365b1953b43653341412f9088b0d752614c626a73a705ff9af400f3a3/coverage-7.15.3-cp313-cp313-win32.whl", hash = "sha256:00cbdc5e322927dc30c5e42b863819b1bb867cc66f26ab5372c585850876ab93", size = 224575, upload-time = "2026-08-02T18:49:05.011Z" }, - { url = "https://files.pythonhosted.org/packages/1c/64/88f762ea80de2070207246faef514513be874486b2773528f2cc2b4b515c/coverage-7.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:835528518a1d823cf336740324b2f335f7c01e609e74abcb5d5163b3e66661e3", size = 225116, upload-time = "2026-08-02T18:49:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/ab/66/03c34c53a319f522554cd29d4f2e16c5eab61aa4cdcf55753129fd7d926c/coverage-7.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:0d2e1f2cbbf36b842f3e2aff8d118c60d677adb498bc6c7fa9c6838738f82767", size = 224509, upload-time = "2026-08-02T18:49:09.129Z" }, - { url = "https://files.pythonhosted.org/packages/35/6f/8c2dc014357618b3226c90f731b8282766c3685786f422558991dc49fbf2/coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d", size = 222571, upload-time = "2026-08-02T18:49:11.242Z" }, - { url = "https://files.pythonhosted.org/packages/07/50/d867c7ceae9d56b7e74ee61ea834f1aa4f9a1e1c7f0ce39393ba573b1c12/coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef", size = 222902, upload-time = "2026-08-02T18:49:13.448Z" }, - { url = "https://files.pythonhosted.org/packages/62/77/4f6dfc490c5f2bcacb2d296d9aa4d1e128c43b48e94ad313fec7f49f09ad/coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd", size = 253947, upload-time = "2026-08-02T18:49:15.304Z" }, - { url = "https://files.pythonhosted.org/packages/16/8a/6777f192af264165103e2a3d3768dbadb9894a0a2359a16877141d9ae8f5/coverage-7.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731", size = 256452, upload-time = "2026-08-02T18:49:17.801Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7b/3d7ac46a0234bc684f41ee42be95e29b2b6525695adb04083609d5ac2149/coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e", size = 257798, upload-time = "2026-08-02T18:49:19.878Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/c6ee59c29afcb5fdb35f936381340d1a06429a07c48f20e809646647acbe/coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c", size = 260112, upload-time = "2026-08-02T18:49:21.858Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e1/e8ea39a46e89e3a143312ee5f80336e992e3ae8fe44bf9c76b83fefeed42/coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764", size = 253944, upload-time = "2026-08-02T18:49:23.926Z" }, - { url = "https://files.pythonhosted.org/packages/95/67/31ab5f6a37fd887d1386f81f0da9306851ad2264e9baaa9c7f606e0b3e17/coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e", size = 255805, upload-time = "2026-08-02T18:49:25.973Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6a/ee505a80c8fd89620fb337c0596daecff87f33171fbb4ee3015fc3d7331f/coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f", size = 253769, upload-time = "2026-08-02T18:49:27.883Z" }, - { url = "https://files.pythonhosted.org/packages/b0/41/6ab0f81c9e89660230d8f3f581d4732e5ddb75a885b0a5dfc73d315dc94f/coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd", size = 258045, upload-time = "2026-08-02T18:49:30.201Z" }, - { url = "https://files.pythonhosted.org/packages/bc/62/c995e91cae28cf31d6defab3bfb553dda5ac83ac7381b0f2b121264c307a/coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a", size = 253587, upload-time = "2026-08-02T18:49:32.349Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/f2049980f82d6890321f2065f9e66216eabbf4b2001815db958bc543f40a/coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6", size = 255243, upload-time = "2026-08-02T18:49:34.324Z" }, - { url = "https://files.pythonhosted.org/packages/1d/82/2c841b67a978c0eb9c3707630b68f93f9e7585d78bb906bc8823ec6b07a5/coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8", size = 224759, upload-time = "2026-08-02T18:49:36.326Z" }, - { url = "https://files.pythonhosted.org/packages/b3/78/5c93ec43784fd3e404ca23cd0584ae24bc1732de4a3fc194b68c3be88db0/coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2", size = 225246, upload-time = "2026-08-02T18:49:38.366Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/813a054371f3b018cc63c6bdb46a3c35d5e95d4e3ed4f1449d4196106db5/coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926", size = 224673, upload-time = "2026-08-02T18:49:40.552Z" }, - { url = "https://files.pythonhosted.org/packages/8f/63/8c9f36cc71178d26db930baa03a4494abcc516d8d41bf820d0d85ef1d80b/coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b", size = 223298, upload-time = "2026-08-02T18:49:42.634Z" }, - { url = "https://files.pythonhosted.org/packages/54/66/211f24d058ce9f56ebf1420d55b7574fdae924f6da3836f83c8bd4793e38/coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95", size = 223568, upload-time = "2026-08-02T18:49:44.706Z" }, - { url = "https://files.pythonhosted.org/packages/dd/bb/9c2ad5574a0d6420a96c6cade4f8a683931b9e79fe609f8924d7b6964616/coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43", size = 264932, upload-time = "2026-08-02T18:49:47.153Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/938c39e77bdd5a0a440412f975609ce3702dabbda6ac715719d93ca45a7b/coverage-7.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8", size = 267052, upload-time = "2026-08-02T18:49:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a3/7b431a98af35d9cc6394e54cde9435b33b8591672fbece6a4931267d7a8e/coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779", size = 269473, upload-time = "2026-08-02T18:49:51.599Z" }, - { url = "https://files.pythonhosted.org/packages/32/58/dbc9951dce46be47a732823a1c571f62bcabdd54a68d8c281489a1a55cfb/coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77", size = 270591, upload-time = "2026-08-02T18:49:53.865Z" }, - { url = "https://files.pythonhosted.org/packages/71/bd/1d610772c7c0889bfe477a59c46ee66ea53e271f3f06951e9d55b317f7c6/coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005", size = 264007, upload-time = "2026-08-02T18:49:55.875Z" }, - { url = "https://files.pythonhosted.org/packages/69/97/852eb3dcdba156b1a9078503f098499916bf889f964b61ad4a08223ac169/coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57", size = 266926, upload-time = "2026-08-02T18:49:57.944Z" }, - { url = "https://files.pythonhosted.org/packages/52/f8/b72cd238757fba2b587fc7dee047efe6e10b0c18343509faaaf502dd4680/coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91", size = 264529, upload-time = "2026-08-02T18:50:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0a/6c52ec4b7fb007cb6433d1fcfda4080cb15d75ad37ef9c31025f3427293e/coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2", size = 268263, upload-time = "2026-08-02T18:50:02.161Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4e/f1f9aa3efd109a04353563a43fb5155340c1fdcdeaa6296ebed3b6f510ea/coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef", size = 263377, upload-time = "2026-08-02T18:50:04.243Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/6b268a0b2728ef1c379ad656b899274477a5f6bed1bf6765b4b387fb0601/coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c", size = 265688, upload-time = "2026-08-02T18:50:06.428Z" }, - { url = "https://files.pythonhosted.org/packages/29/54/1a3ea96e5d5e7cd41dc432597bfc60692910e635d05e1cc25a8ccc243581/coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42", size = 225066, upload-time = "2026-08-02T18:50:08.533Z" }, - { url = "https://files.pythonhosted.org/packages/31/9d/a7b0d9afd18ed5274dd00651a78e7810a931c70d94b79996f150bec1a30f/coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429", size = 225897, upload-time = "2026-08-02T18:50:10.572Z" }, - { url = "https://files.pythonhosted.org/packages/ca/11/34c5ae40b945e69aa72b87dc268135b7049905f3824af573b7073acbb946/coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e", size = 225212, upload-time = "2026-08-02T18:50:12.63Z" }, - { url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/c76bf165ff01664ca8b1ca7f2b2b5f311353d3959dbac1187dd21c6cc7f8/coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d", size = 223019, upload-time = "2026-08-28T21:51:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/16/7d/a47cebf71cb789b6e25de07035d350bff110d02f9c28bf32f92b4c818874/coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db", size = 223524, upload-time = "2026-08-28T21:51:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/51/b3/42e46d7e247ba33758156a0cc88dc64715f7e7b04640fbe430c4da437ab1/coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3", size = 253934, upload-time = "2026-08-28T21:51:05.365Z" }, + { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4", size = 255846, upload-time = "2026-08-28T21:51:06.922Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/38e5d8cf45af5db7419e9580bba4017113f8f1e2697cb6c52213bf7e7e40/coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719", size = 257953, upload-time = "2026-08-28T21:51:08.51Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bb/2f44b99723d0306095dacdf90f994631e299ff8f087a384b42ecc2d1ccb9/coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3", size = 259915, upload-time = "2026-08-28T21:51:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7d/3f1c312944d88b2d3cae8af72007c15dcf5f92bda6da6d433c2d5f050ee7/coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a", size = 254028, upload-time = "2026-08-28T21:51:11.845Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f6/52a7e26baeeca7f3114b15da5e840bebcfe6491eb234f6922d33c79ee8fc/coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c", size = 255648, upload-time = "2026-08-28T21:51:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d1/0673e78d9ca29d56f663623791338647753c673f0bc964e860086da07bce/coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9", size = 253708, upload-time = "2026-08-28T21:51:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6c/23/b74c87828369059415b20884b6f48260f049bff750d6eb454be8554732ab/coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d", size = 257479, upload-time = "2026-08-28T21:51:16.988Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/09e172472c45a956e226dddf82d449f245764208b7cea47b32a73df955a3/coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6", size = 253428, upload-time = "2026-08-28T21:51:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/e378e4f7ffa290ea4775b34e319fa182640bba650a2c6781af791b66b79a/coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce", size = 254337, upload-time = "2026-08-28T21:51:20.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/9a6ca653d86e46c3383a905f726a28bcf7bb2528088794d30a53687b381c/coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce", size = 225103, upload-time = "2026-08-28T21:51:22.685Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/6d4627be89ac02f579d88806875a5d6e328c59d7d79c594643c7a4460ef6/coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a", size = 225577, upload-time = "2026-08-28T21:51:24.334Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3d/d7be38564d00a17775426685776b4bf18e8a6048a085eccf65d75eb0fa5a/coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7", size = 225126, upload-time = "2026-08-28T21:51:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, + { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, + { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, + { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, + { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, + { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, + { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, + { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, + { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, + { url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z" }, + { url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, ] [package.optional-dependencies] @@ -681,69 +678,65 @@ toml = [ [[package]] name = "crc32c" -version = "2.8" +version = "2.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/66/7e97aa77af7cf6afbff26e3651b564fe41932599bc2d3dce0b2f73d4829a/crc32c-2.8.tar.gz", hash = "sha256:578728964e59c47c356aeeedee6220e021e124b9d3e8631d95d9a5e5f06e261c", size = 48179, upload-time = "2025-10-17T06:20:13.61Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/2a/1b7eadbd3c858204a90406a2a1e0dccd1592f69b77ec278a202fa325c50f/crc32c-2.9.tar.gz", hash = "sha256:d6d2ae0299a417e5fb2e3641838cec34a23778a62fb4ce928792366476c1983b", size = 48240, upload-time = "2026-08-21T03:57:23.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/0b/5e03b22d913698e9cc563f39b9f6bbd508606bf6b8e9122cd6bf196b87ea/crc32c-2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e560a97fbb96c9897cb1d9b5076ef12fc12e2e25622530a1afd0de4240f17e1f", size = 66329, upload-time = "2025-10-17T06:19:01.771Z" }, - { url = "https://files.pythonhosted.org/packages/6b/38/2fe0051ffe8c6a650c8b1ac0da31b8802d1dbe5fa40a84e4b6b6f5583db5/crc32c-2.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6762d276d90331a490ef7e71ffee53b9c0eb053bd75a272d786f3b08d3fe3671", size = 62988, upload-time = "2025-10-17T06:19:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/3e/30/5837a71c014be83aba1469c58820d287fc836512a0cad6b8fdd43868accd/crc32c-2.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:60670569f5ede91e39f48fb0cb4060e05b8d8704dd9e17ede930bf441b2f73ef", size = 61522, upload-time = "2025-10-17T06:19:03.796Z" }, - { url = "https://files.pythonhosted.org/packages/ca/29/63972fc1452778e2092ae998c50cbfc2fc93e3fa9798a0278650cd6169c5/crc32c-2.8-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:711743da6ccc70b3c6718c328947b0b6f34a1fe6a6c27cc6c1d69cc226bf70e9", size = 80200, upload-time = "2025-10-17T06:19:04.617Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3a/60eb49d7bdada4122b3ffd45b0df54bdc1b8dd092cda4b069a287bdfcff4/crc32c-2.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eb4094a2054774f13b26f21bf56792bb44fa1fcee6c6ad099387a43ffbfb4fa", size = 81757, upload-time = "2025-10-17T06:19:05.496Z" }, - { url = "https://files.pythonhosted.org/packages/f5/63/6efc1b64429ef7d23bd58b75b7ac24d15df327e3ebbe9c247a0f7b1c2ed1/crc32c-2.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fff15bf2bd3e95780516baae935ed12be88deaa5ebe6143c53eb0d26a7bdc7b7", size = 80830, upload-time = "2025-10-17T06:19:06.621Z" }, - { url = "https://files.pythonhosted.org/packages/e1/eb/0ae9f436f8004f1c88f7429e659a7218a3879bd11a6b18ed1257aad7e98b/crc32c-2.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c0e11e3826668121fa53e0745635baf5e4f0ded437e8ff63ea56f38fc4f970a", size = 80095, upload-time = "2025-10-17T06:19:07.381Z" }, - { url = "https://files.pythonhosted.org/packages/9e/81/4afc9d468977a4cd94a2eb62908553345009a7c0d30e74463a15d4b48ec3/crc32c-2.8-cp311-cp311-win32.whl", hash = "sha256:38f915336715d1f1353ab07d7d786f8a789b119e273aea106ba55355dfc9101d", size = 64886, upload-time = "2025-10-17T06:19:08.497Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e8/94e839c9f7e767bf8479046a207afd440a08f5c59b52586e1af5e64fa4a0/crc32c-2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60e0a765b1caab8d31b2ea80840639253906a9351d4b861551c8c8625ea20f86", size = 66639, upload-time = "2025-10-17T06:19:09.338Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/fd18ef23c42926b79c7003e16cb0f79043b5b179c633521343d3b499e996/crc32c-2.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:572ffb1b78cce3d88e8d4143e154d31044a44be42cb3f6fbbf77f1e7a941c5ab", size = 66379, upload-time = "2025-10-17T06:19:10.115Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b8/c584958e53f7798dd358f5bdb1bbfc97483134f053ee399d3eeb26cca075/crc32c-2.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cf827b3758ee0c4aacd21ceca0e2da83681f10295c38a10bfeb105f7d98f7a68", size = 63042, upload-time = "2025-10-17T06:19:10.946Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/6f2af0ec64a668a46c861e5bc778ea3ee42171fedfc5440f791f470fd783/crc32c-2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:106fbd79013e06fa92bc3b51031694fcc1249811ed4364ef1554ee3dd2c7f5a2", size = 61528, upload-time = "2025-10-17T06:19:11.768Z" }, - { url = "https://files.pythonhosted.org/packages/17/8b/4a04bd80a024f1a23978f19ae99407783e06549e361ab56e9c08bba3c1d3/crc32c-2.8-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6dde035f91ffbfe23163e68605ee5a4bb8ceebd71ed54bb1fb1d0526cdd125a2", size = 80028, upload-time = "2025-10-17T06:19:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/21/8f/01c7afdc76ac2007d0e6a98e7300b4470b170480f8188475b597d1f4b4c6/crc32c-2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e41ebe7c2f0fdcd9f3a3fd206989a36b460b4d3f24816d53e5be6c7dba72c5e1", size = 81531, upload-time = "2025-10-17T06:19:13.406Z" }, - { url = "https://files.pythonhosted.org/packages/32/2b/8f78c5a8cc66486be5f51b6f038fc347c3ba748d3ea68be17a014283c331/crc32c-2.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecf66cf90266d9c15cea597d5cc86c01917cd1a238dc3c51420c7886fa750d7e", size = 80608, upload-time = "2025-10-17T06:19:14.223Z" }, - { url = "https://files.pythonhosted.org/packages/db/86/fad1a94cdeeeb6b6e2323c87f970186e74bfd6fbfbc247bf5c88ad0873d5/crc32c-2.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59eee5f3a69ad0793d5fa9cdc9b9d743b0cd50edf7fccc0a3988a821fef0208c", size = 79886, upload-time = "2025-10-17T06:19:15.345Z" }, - { url = "https://files.pythonhosted.org/packages/d5/db/1a7cb6757a1e32376fa2dfce00c815ea4ee614a94f9bff8228e37420c183/crc32c-2.8-cp312-cp312-win32.whl", hash = "sha256:a73d03ce3604aa5d7a2698e9057a0eef69f529c46497b27ee1c38158e90ceb76", size = 64896, upload-time = "2025-10-17T06:19:16.457Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8e/2024de34399b2e401a37dcb54b224b56c747b0dc46de4966886827b4d370/crc32c-2.8-cp312-cp312-win_amd64.whl", hash = "sha256:56b3b7d015247962cf58186e06d18c3d75a1a63d709d3233509e1c50a2d36aa2", size = 66645, upload-time = "2025-10-17T06:19:17.235Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d8/3ae227890b3be40955a7144106ef4dd97d6123a82c2a5310cdab58ca49d8/crc32c-2.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36f1e03ee9e9c6938e67d3bcb60e36f260170aa5f37da1185e04ef37b56af395", size = 66380, upload-time = "2025-10-17T06:19:18.009Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8b/178d3f987cd0e049b484615512d3f91f3d2caeeb8ff336bb5896ae317438/crc32c-2.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b2f3226b94b85a8dd9b3533601d7a63e9e3e8edf03a8a169830ee8303a199aeb", size = 63048, upload-time = "2025-10-17T06:19:18.853Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a1/48145ae2545ebc0169d3283ebe882da580ea4606bfb67cf4ca922ac3cfc3/crc32c-2.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e08628bc72d5b6bc8e0730e8f142194b610e780a98c58cb6698e665cb885a5b", size = 61530, upload-time = "2025-10-17T06:19:19.974Z" }, - { url = "https://files.pythonhosted.org/packages/06/4b/cf05ed9d934cc30e5ae22f97c8272face420a476090e736615d9a6b53de0/crc32c-2.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:086f64793c5ec856d1ab31a026d52ad2b895ac83d7a38fce557d74eb857f0a82", size = 80001, upload-time = "2025-10-17T06:19:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/15/ab/4b04801739faf36345f6ba1920be5b1c70282fec52f8280afd3613fb13e2/crc32c-2.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcf72ee7e0135b3d941c34bb2c26c3fc6bc207106b49fd89aaafaeae223ae209", size = 81543, upload-time = "2025-10-17T06:19:21.557Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1b/6e38dde5bfd2ea69b7f2ab6ec229fcd972a53d39e2db4efe75c0ac0382ce/crc32c-2.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a717dd9c3fd777d9bc6603717eae172887d402c4ab589d124ebd0184a83f89e", size = 80644, upload-time = "2025-10-17T06:19:22.325Z" }, - { url = "https://files.pythonhosted.org/packages/ce/45/012176ffee90059ae8ec7131019c71724ea472aa63e72c0c8edbd1fad1d7/crc32c-2.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0450bb845b3c3c7b9bdc0b4e95620ec9a40824abdc8c86d6285c919a90743c1a", size = 79919, upload-time = "2025-10-17T06:19:23.101Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/f557629842f9dec2b3461cb3a0d854bb586ec45b814cea58b082c32f0dde/crc32c-2.8-cp313-cp313-win32.whl", hash = "sha256:765d220bfcbcffa6598ac11eb1e10af0ee4802b49fe126aa6bf79f8ddb9931d1", size = 64896, upload-time = "2025-10-17T06:19:23.88Z" }, - { url = "https://files.pythonhosted.org/packages/d0/db/fd0f698c15d1e21d47c64181a98290665a08fcbb3940cd559e9c15bda57e/crc32c-2.8-cp313-cp313-win_amd64.whl", hash = "sha256:171ff0260d112c62abcce29332986950a57bddee514e0a2418bfde493ea06bb3", size = 66646, upload-time = "2025-10-17T06:19:24.702Z" }, - { url = "https://files.pythonhosted.org/packages/db/b9/8e5d7054fe8e7eecab10fd0c8e7ffb01439417bdb6de1d66a81c38fc4a20/crc32c-2.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b977a32a3708d6f51703c8557008f190aaa434d7347431efb0e86fcbe78c2a50", size = 66203, upload-time = "2025-10-17T06:19:25.872Z" }, - { url = "https://files.pythonhosted.org/packages/55/5f/cc926c70057a63cc0c98a3c8a896eb15fc7e74d3034eadd53c94917c6cc3/crc32c-2.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7399b01db4adaf41da2fb36fe2408e75a8d82a179a9564ed7619412e427b26d6", size = 62956, upload-time = "2025-10-17T06:19:26.652Z" }, - { url = "https://files.pythonhosted.org/packages/a1/8a/0660c44a2dd2cb6ccbb529eb363b9280f5c766f1017bc8355ed8d695bd94/crc32c-2.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4379f73f9cdad31958a673d11a332ec725ca71572401ca865867229f5f15e853", size = 61442, upload-time = "2025-10-17T06:19:27.74Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/6108d2dfc0fe33522ce83ba07aed4b22014911b387afa228808a278e27cd/crc32c-2.8-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e68264555fab19bab08331550dab58573e351a63ed79c869d455edd3b0aa417", size = 79109, upload-time = "2025-10-17T06:19:28.535Z" }, - { url = "https://files.pythonhosted.org/packages/84/1e/c054f9e390090c197abf3d2936f4f9effaf0c6ee14569ae03d6ddf86958a/crc32c-2.8-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b48f2486727b8d0e7ccbae4a34cb0300498433d2a9d6b49cb13cb57c2e3f19cb", size = 80987, upload-time = "2025-10-17T06:19:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ad/1650e5c3341e4a485f800ea83116d72965030c5d48ccc168fcc685756e4d/crc32c-2.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ecf123348934a086df8c8fde7f9f2d716d523ca0707c5a1367b8bb00d8134823", size = 79994, upload-time = "2025-10-17T06:19:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/d7/3b/f2ed924b177729cbb2ab30ca2902abff653c31d48c95e7b66717a9ca9fcc/crc32c-2.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e636ac60f76de538f7a2c0d0f3abf43104ee83a8f5e516f6345dc283ed1a4df7", size = 79046, upload-time = "2025-10-17T06:19:30.894Z" }, - { url = "https://files.pythonhosted.org/packages/4b/80/413b05ee6ace613208b31b3670c3135ee1cf451f0e72a9c839b4946acc04/crc32c-2.8-cp313-cp313t-win32.whl", hash = "sha256:8dd4a19505e0253892e1b2f1425cc3bd47f79ae5a04cb8800315d00aad7197f2", size = 64837, upload-time = "2025-10-17T06:19:32.03Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1b/85eddb6ac5b38496c4e35c20298aae627970c88c3c624a22ab33e84f16c7/crc32c-2.8-cp313-cp313t-win_amd64.whl", hash = "sha256:4bb18e4bd98fb266596523ffc6be9c5b2387b2fa4e505ec56ca36336f49cb639", size = 66574, upload-time = "2025-10-17T06:19:33.143Z" }, - { url = "https://files.pythonhosted.org/packages/aa/df/50e9079b532ff53dbfc0e66eed781374bd455af02ed5df8b56ad538de4ff/crc32c-2.8-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3a3b2e4bcf7b3ee333050e7d3ff38e2ba46ea205f1d73d8949b248aaffe937ac", size = 66399, upload-time = "2025-10-17T06:19:34.279Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2e/67e3b0bc3d30e46ea5d16365cc81203286387671e22f2307eb41f19abb9c/crc32c-2.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:445e559e66dff16be54f8a4ef95aa6b01db799a639956d995c5498ba513fccc2", size = 63044, upload-time = "2025-10-17T06:19:35.062Z" }, - { url = "https://files.pythonhosted.org/packages/36/ea/1723b17437e4344ed8d067456382ecb1f5b535d83fdc5aaebab676c6d273/crc32c-2.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bf3040919e17afa5782e01b1875d6a05f44b8f19c05f211d8b9f8a1deb8bbd9c", size = 61541, upload-time = "2025-10-17T06:19:36.204Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6a/cbec8a235c5b46a01f319939b538958662159aec0ed3a74944e3a6de21f1/crc32c-2.8-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5607ab8221e1ffd411f64aa40dbb6850cf06dd2908c9debd05d371e1acf62ff3", size = 80139, upload-time = "2025-10-17T06:19:37.351Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/d096722fe74b692d6e8206c27da1ea5f6b2a12ff92c54a62a6ba2f376254/crc32c-2.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f5db4f16816926986d3c94253314920689706ae13a9bf4888b47336c6735ce", size = 81736, upload-time = "2025-10-17T06:19:38.16Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a2/f75ef716ff7e3c22f385ba6ef30c5de80c19a21ebe699dc90824a1903275/crc32c-2.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70b0153c4d418b673309d3529334d117e1074c4a3b2d7f676e430d72c14de67b", size = 80795, upload-time = "2025-10-17T06:19:38.948Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/6d647a12d96ab087d9b8eacee3da073f981987827d57c7072f89ffc7b6cd/crc32c-2.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c8933531442042438753755a5c8a9034e4d88b01da9eb796f7e151b31a7256c", size = 80042, upload-time = "2025-10-17T06:19:39.725Z" }, - { url = "https://files.pythonhosted.org/packages/cd/dc/32b8896b40a0afee7a3c040536d0da5a73e68df2be9fadd21770fd158e16/crc32c-2.8-cp314-cp314-win32.whl", hash = "sha256:cdc83a3fe6c4e5df9457294cfd643de7d95bd4e9382c1dd6ed1e0f0f9169172c", size = 64914, upload-time = "2025-10-17T06:19:40.527Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b4/4308b27d307e8ecaf8dd1dcc63bbb0e47ae1826d93faa3e62d1ee00ee2d5/crc32c-2.8-cp314-cp314-win_amd64.whl", hash = "sha256:509e10035106df66770fe24b9eb8d9e32b6fb967df17744402fb67772d8b2bc7", size = 66723, upload-time = "2025-10-17T06:19:42.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/d5/a19d2489fa997a143bfbbf971a5c9a43f8b1ba9e775b1fb362d8fb15260c/crc32c-2.8-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:864359a39777a07b09b28eb31337c0cc603d5c1bf0fc328c3af736a8da624ec0", size = 66201, upload-time = "2025-10-17T06:19:43.273Z" }, - { url = "https://files.pythonhosted.org/packages/98/c2/5f82f22d2c1242cb6f6fe92aa9a42991ebea86de994b8f9974d9c1d128e2/crc32c-2.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:14511d7cfc5d9f5e1a6c6b64caa6225c2bdc1ed00d725e9a374a3e84073ce180", size = 62956, upload-time = "2025-10-17T06:19:44.099Z" }, - { url = "https://files.pythonhosted.org/packages/9b/61/3d43d33489cf974fb78bfb3500845770e139ae6d1d83473b660bd8f79a6c/crc32c-2.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:918b7999b52b5dcbcea34081e9a02d46917d571921a3f209956a9a429b2e06e5", size = 61443, upload-time = "2025-10-17T06:19:44.89Z" }, - { url = "https://files.pythonhosted.org/packages/52/6d/f306ce64a352a3002f76b0fc88a1373f4541f9d34fad3668688610bab14b/crc32c-2.8-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cc445da03fc012a5a03b71da1df1b40139729e6a5571fd4215ab40bfb39689c7", size = 79106, upload-time = "2025-10-17T06:19:45.688Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b7/1f74965dd7ea762954a69d172dfb3a706049c84ffa45d31401d010a4a126/crc32c-2.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e3dde2ec59a8a830511d72a086ead95c0b0b7f0d418f93ea106244c5e77e350", size = 80983, upload-time = "2025-10-17T06:19:46.792Z" }, - { url = "https://files.pythonhosted.org/packages/1b/50/af93f0d91ccd61833ce77374ebfbd16f5805f5c17d18c6470976d9866d76/crc32c-2.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:61d51681a08b6a2a2e771b7f0cd1947fb87cb28f38ed55a01cb7c40b2ac4cdd8", size = 80009, upload-time = "2025-10-17T06:19:47.619Z" }, - { url = "https://files.pythonhosted.org/packages/ee/fa/94f394beb68a88258af694dab2f1284f55a406b615d7900bdd6235283bc4/crc32c-2.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:67c0716c3b1a02d5235be649487b637eed21f2d070f2b3f63f709dcd2fefb4c7", size = 79066, upload-time = "2025-10-17T06:19:48.409Z" }, - { url = "https://files.pythonhosted.org/packages/91/c6/a6050e0c64fd73c67a97da96cb59f08b05111e00b958fb87ecdce99f17ac/crc32c-2.8-cp314-cp314t-win32.whl", hash = "sha256:2e8fe863fbbd8bdb6b414a2090f1b0f52106e76e9a9c96a413495dbe5ebe492a", size = 64869, upload-time = "2025-10-17T06:19:49.197Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/c7735034e401cb1ea14f996a224518e3a3fa9987cb13680e707328a7d779/crc32c-2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:20a9cfb897693eb6da19e52e2a7be2026fd4d9fc8ae318f086c0d71d5dd2d8e0", size = 66633, upload-time = "2025-10-17T06:19:50.003Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1d/dd926c68eb8aac8b142a1a10b8eb62d95212c1cf81775644373fe7cceac2/crc32c-2.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5833f4071da7ea182c514ba17d1eee8aec3c5be927d798222fbfbbd0f5eea02c", size = 62345, upload-time = "2025-10-17T06:20:09.39Z" }, - { url = "https://files.pythonhosted.org/packages/51/be/803404e5abea2ef2c15042edca04bbb7f625044cca879e47f186b43887c2/crc32c-2.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1dc4da036126ac07b39dd9d03e93e585ec615a2ad28ff12757aef7de175295a8", size = 61229, upload-time = "2025-10-17T06:20:10.236Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3a/00cc578cd27ed0b22c9be25cef2c24539d92df9fa80ebd67a3fc5419724c/crc32c-2.8-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15905fa78344654e241371c47e6ed2411f9eeb2b8095311c68c88eccf541e8b4", size = 64108, upload-time = "2025-10-17T06:20:11.072Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bc/0587ef99a1c7629f95dd0c9d4f3d894de383a0df85831eb16c48a6afdae4/crc32c-2.8-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c596f918688821f796434e89b431b1698396c38bf0b56de873621528fe3ecb1e", size = 64815, upload-time = "2025-10-17T06:20:11.919Z" }, - { url = "https://files.pythonhosted.org/packages/73/42/94f2b8b92eae9064fcfb8deef2b971514065bd606231f8857ff8ae02bebd/crc32c-2.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8d23c4fe01b3844cb6e091044bc1cebdef7d16472e058ce12d9fadf10d2614af", size = 66659, upload-time = "2025-10-17T06:20:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a0/dd6e6e80ade758f8cf1bcf03c13ccf52d763a34d2cb23cb2c8422aaf1ab4/crc32c-2.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7840fb5c918978398818b827ddf9d11259e66fc7b493a9a57e5b5d2b85ed3af1", size = 66365, upload-time = "2026-08-21T03:55:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/57/6c/3389c79e95e8c697b45f0e70db846b104ef2e2463696e683cfb821266e3f/crc32c-2.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b97dfdcd08cd964938f3e2c54cc8132248aa11650d9b9e4eff909fa0d3061960", size = 63026, upload-time = "2026-08-21T03:55:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/15/5f/1b0c7b71b194431227b7e1fa92fa0cc43756af5ce7e27e9ad6d49eb6bddd/crc32c-2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:137bfbfb135f3ae599be014ba2081131fd8e655aaf78ca9a96b6ffcddb01dba9", size = 61560, upload-time = "2026-08-21T03:55:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7d/2118dbe1c55ab1916a3fe377f678763abd28fd5155aed86f7cc0de9e078b/crc32c-2.9-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15cd5139b17b5b0c606fb6980bc7ef481ec4e8efcb488216faecceed5232ca9a", size = 80242, upload-time = "2026-08-21T03:56:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/f8d4b7c13992bbf2a02eb6b5fa83b07f61333a9bbf16c2eb8ba3d58c99ce/crc32c-2.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c93503c0b0058b55c58483f546902cb31d96c50e5c42ab090c60f58fd5727a", size = 81789, upload-time = "2026-08-21T03:56:01.788Z" }, + { url = "https://files.pythonhosted.org/packages/bf/77/e0740c0ceed59d4d11c79e8f3405fb29008965e64dd0d492b7f1e406a245/crc32c-2.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1fa11bf77a6618a17d83f8828b5a54d22ae65fcb651a60765493d62289fa8a2e", size = 80868, upload-time = "2026-08-21T03:56:02.753Z" }, + { url = "https://files.pythonhosted.org/packages/0a/28/865e6d050199d241048a1183b2ebd4650db24948518841d1ddc93802c65f/crc32c-2.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32432ffce62ad4dee949d764ba2e8059ef8fe958cf4b605d9b5fe2dfe8fd92fd", size = 80129, upload-time = "2026-08-21T03:56:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/57167723eabab66e4292a6ae4fa0538defa074225209a02ea53c803cd32f/crc32c-2.9-cp311-cp311-win32.whl", hash = "sha256:bb00a03b434d170acd45a728274ef1a33376aa6f44666689c932f4732d68e950", size = 64388, upload-time = "2026-08-21T03:56:04.642Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/329b23392ccf26d9e6e3538942b4482c9430ff6bdd617a98b6987095e9bc/crc32c-2.9-cp311-cp311-win_amd64.whl", hash = "sha256:6c1faee6d17fb5202c39ad9f8521ca0345992ea9f0c8cef25971bdc896956396", size = 65774, upload-time = "2026-08-21T03:56:05.71Z" }, + { url = "https://files.pythonhosted.org/packages/70/c9/1ad3c072207fc98acb0a65147bc01fc252877da94dd1b60c8adc425922a3/crc32c-2.9-cp311-cp311-win_arm64.whl", hash = "sha256:22e12c373691bef1788b9cca6b4db7d55c91e740f06d7841ae844d846d239e47", size = 63345, upload-time = "2026-08-21T03:56:06.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/96/a9f8506bea8d7a88ce40884bc8e0afa9ad465af5c7d68217638ec4a56b89/crc32c-2.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8828d7aaf53eff93ab5fe3507e11184be39bc719555c9fe17404df894a26fdd0", size = 66421, upload-time = "2026-08-21T03:56:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/77/e5/22eee27b9f3daf741ef9eecc448d7a5b560af41a6ad37ee2ff6c2d70bb28/crc32c-2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e79a247d2d2e574531b55a69deb2f9d48c36693ca4e95007252fab4c7c66d3b1", size = 63077, upload-time = "2026-08-21T03:56:08.355Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/c1ea96251fcb5c0f24f573dbe5f658e0d6bc78922ac9687dff687ec60044/crc32c-2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a2348add3f702b0a0cb035db54ee8cddb773610c9b44234a3c2465f5d3cb8079", size = 61568, upload-time = "2026-08-21T03:56:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/77/2c/06173d0b1c2d3ca00b5dd1535113feea16a5ad7093691f380e95c598ff20/crc32c-2.9-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:364a6e754e228c6123c745ac38f2cb8389e9a2461c17fcbede7fedb6e3bb8641", size = 80063, upload-time = "2026-08-21T03:56:10.214Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2b/8e68e794770a67ae17f5dd805f4de45145779617581fe82a6bfd4214e611/crc32c-2.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa0ebd0d365d8cb1cd202a053b5cb8ffa07c9ecc8c38728f116b97e116cc8ec5", size = 81567, upload-time = "2026-08-21T03:56:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3a/21ecf1a535d3b2a3871abb44f2c982a2802282db397a5cf0fdc92e0baeaa/crc32c-2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:790545690c4798ff628db970f31a27aff12a79dafd8e2aaa03a34157b81879bf", size = 80642, upload-time = "2026-08-21T03:56:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/6fe35a9f4292dc1447d3ed38c0f4ac121ec73a4e9d579438b01d69a08eb7/crc32c-2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c16d6eb17c90a6574655f3717767e4362ba01b95769155411096fde37c86274c", size = 79924, upload-time = "2026-08-21T03:56:12.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/33/dbd2e2c65bf3265eed58ef087c7cdd4187c46797796571ed120ac0d32658/crc32c-2.9-cp312-cp312-win32.whl", hash = "sha256:a2669c0993b8f2a3133526b4208f78b70447200c4d87f89159f184fc2e9e2db3", size = 64407, upload-time = "2026-08-21T03:56:14.011Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/d0542d34396c935fc11017f2b1cb0b57c41aed8e6de22f53cce90748b54d/crc32c-2.9-cp312-cp312-win_amd64.whl", hash = "sha256:147adf3cce798ffae2ae737b240fb3c62fffd3579efdd8226e7af84b7ae42b26", size = 65792, upload-time = "2026-08-21T03:56:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9a/eda45e85b76c419f0af313c275c8e8d566e399102f949ff1861f525fb514/crc32c-2.9-cp312-cp312-win_arm64.whl", hash = "sha256:da60a865d6592935acee05a7166058a38dce78a816b9325440bc734d8d430b33", size = 63357, upload-time = "2026-08-21T03:56:15.957Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6d/d2975dc9ad6583bc841cab16fbe5a3bcc643410da75d19af5d3e1d277912/crc32c-2.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac29290a6ecd02d7cfe0b76c372b843dda5f20f9d1b75a8d52745340aca00472", size = 66423, upload-time = "2026-08-21T03:56:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/f4b15d2188696ce28788a6e3576fda764797c81e0a527bf8b9414927fac5/crc32c-2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2f16be57f15506a27fe69aec9e277808c88e6a43a8e294ef176730bbdfbbf7d", size = 63084, upload-time = "2026-08-21T03:56:17.742Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f7/c30afdb698947d932ce586b5f4998e40343a8584c58e598b614bdd2e881c/crc32c-2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:17c0a207793366af63c09b1c69056c5b86a6c69446e895325e284d8de19cbb35", size = 61567, upload-time = "2026-08-21T03:56:18.642Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/fab87c4959c22220a9f6e48a571c2c4afd6b8c830d5dd99f6c8917dd65e2/crc32c-2.9-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d069246a61d0b0c3532dc69b0d17de86aaad486547b3a700865613f4b74090b7", size = 80049, upload-time = "2026-08-21T03:56:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/0078d536ccc0ce37d27e9dd0b757ebb45f3cc82410ed5e9c4527115f2b43/crc32c-2.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14991d08980627077e7c756aa292c458def79484a743e5910e453fdcef5ab725", size = 81605, upload-time = "2026-08-21T03:56:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8a/9f55e3a04b6c886f3d91734d346737813c9f8cfbd2c9593efe352376f8fe/crc32c-2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2976ed983caeafcb36ac9a839386d080198b0a36c7b545d1b8472f4f1f03aee2", size = 80687, upload-time = "2026-08-21T03:56:21.499Z" }, + { url = "https://files.pythonhosted.org/packages/78/ce/6c6ee8d57851d322438f8b32f4cb180fc61e7444e636160a478eb2bd4920/crc32c-2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:941eb9f57ff7d0a7f50361d329fa2b9c94a982da6b52b26f432e67103582281f", size = 79973, upload-time = "2026-08-21T03:56:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/55073bc92e5a8078051d9361cbf930075c2fa103a1679221ec0c2fa1c247/crc32c-2.9-cp313-cp313-win32.whl", hash = "sha256:9f6f8d47189e4d6516367e3ee72fb9d0c47f11ad0045802313fb5076de980348", size = 64407, upload-time = "2026-08-21T03:56:23.483Z" }, + { url = "https://files.pythonhosted.org/packages/45/70/e6634715415dfa557716b2344e4d8590a5a77eee5e899e442de0e4ff8d26/crc32c-2.9-cp313-cp313-win_amd64.whl", hash = "sha256:a51230dc5f9b8fd0564b78a8aff419b5956c78801c13454c7ad7f6ffaf2af25e", size = 65792, upload-time = "2026-08-21T03:56:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b2/6166c789ebacfef08b5752996f79d1e472644e9b393ed7c87b1997686910/crc32c-2.9-cp313-cp313-win_arm64.whl", hash = "sha256:94e9646a05951c7cf0657fee48eec85bf669cbf9b7bec09eff02ae12da2e9ca7", size = 63356, upload-time = "2026-08-21T03:56:25.304Z" }, + { url = "https://files.pythonhosted.org/packages/45/da/7a71dea269afc2094090ad6cbf39d0238517acba8bf5708956ef4f60b26d/crc32c-2.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:420b56519516d1255fcfd36ae73d2bc483f65f46178da11acb29a07b34aac192", size = 75600, upload-time = "2026-08-21T03:56:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/cc9031ad8b9b40cf48e3412f44af0af657bc93b130a4c6dd45107d3f131a/crc32c-2.9-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0211e775d0bd3daeb92ff8e6a257f43f16dc0d38080c3341b66cec7d44f16ff9", size = 63155, upload-time = "2026-08-21T03:56:27.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/0b/919eee694b8e09c94ea9a4df727a101c4f1495f3de22f2fae1e7a870f56d/crc32c-2.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:daf2a1d41b04bc1f818b99982fb18ba22b4caefe2c8dff4cc541cd0a03868f51", size = 61576, upload-time = "2026-08-21T03:56:28.037Z" }, + { url = "https://files.pythonhosted.org/packages/72/19/11357ab4d0df2de1faa428c31d78fc82c47c5422dd9aafe449b2a6c4ed0b/crc32c-2.9-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c8453d97d8cfa71b84bb30c7ff2dba21c3db12afab838dcd8cd5329a5e61a73", size = 80201, upload-time = "2026-08-21T03:56:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/b78ae10df9e52dc6538801a71f88c030c6347ec032913684f38f137700cf/crc32c-2.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83d2153c72aba692515e2e241d6b24ba3297b3b937fcd0b865c9d9c9054e5ce2", size = 81792, upload-time = "2026-08-21T03:56:30.121Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/703ebe12dcb9ef6029bce6acedd4ad19bccdcf1d785d476b1e71bd246ca8/crc32c-2.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e7aed324f322d5bd41d200a6c9784f7e4956a9ba34b480991eb729a900f22ec", size = 80852, upload-time = "2026-08-21T03:56:31.036Z" }, + { url = "https://files.pythonhosted.org/packages/80/0d/4e3c93e6075a77af365571481fb65cbe7b2196e88159d0526e93b5742c03/crc32c-2.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6295089652dfc2d239ef565622cb7179e4036f67088a6f722895be839393e78e", size = 80083, upload-time = "2026-08-21T03:56:31.951Z" }, + { url = "https://files.pythonhosted.org/packages/79/1d/2fe44332bf9611ee6eca6a008a3765a734b3bfddd6610c933246f88151e1/crc32c-2.9-cp314-cp314-win32.whl", hash = "sha256:de1ac71dd0ad9ce8ed307eeb2f451633b51c51fd5f28da48177f445e66834816", size = 64386, upload-time = "2026-08-21T03:56:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/af/28/659438876a6d3a4fed01d60fb0304fc1e156c4dbc3d475916fcafef1addd/crc32c-2.9-cp314-cp314-win_amd64.whl", hash = "sha256:861157615af8275843102d61c14d600abd676f17c94155292e97823e2b6b90db", size = 65860, upload-time = "2026-08-21T03:56:34.06Z" }, + { url = "https://files.pythonhosted.org/packages/39/a9/e7161731565a9dc1fc9b438eaf0de187674136c250a59d6964450d1884dd/crc32c-2.9-cp314-cp314-win_arm64.whl", hash = "sha256:ac827509a1e3107ad700e4160932811514b7438be451505307b16b5b9b417fc8", size = 63333, upload-time = "2026-08-21T03:56:34.996Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ee/59655094b91794da9111e148726b4bd79f15ee8293303d05d696114d751e/crc32c-2.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:cab353e68965de7f901f15d4151c94d86a07264e3301a400aaa5f08f7eb52cf9", size = 75406, upload-time = "2026-08-21T03:56:36.061Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/1b8e9907a99322fa7d115710c2a777105ea72d0a55d4b31e0bdf0e4533b7/crc32c-2.9-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:050611c7a773107cd05ba7b208b7f691865b7254b8816c73618a419afe764d10", size = 63061, upload-time = "2026-08-21T03:56:37.241Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8f/bda9dbaf75116f3a4169dd9b0be990d62197fb625dbae92e234768def7b1/crc32c-2.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ce9d194374bffda7ec59ac07fff2b63a0a78ab4b9fac1c998f0f76ee90945a2f", size = 61476, upload-time = "2026-08-21T03:56:38.112Z" }, + { url = "https://files.pythonhosted.org/packages/30/28/0d7133f5761d4bf429b03eca6a848622ccb18f8ae3d9704ea80205be63f5/crc32c-2.9-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045a5e0fa7b4d4ae6e104dc1b3e63c246a3d34997723bf723d7ff20af9622129", size = 79155, upload-time = "2026-08-21T03:56:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/18/b9/c8b129ab8f57288303f5a715ff88808ac67ca9ecc5b1f920166328b4ff22/crc32c-2.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14ef3e86194209308ec2c3e2416d31a8acf8fab08f4fd988a2f65dcafa797f13", size = 81017, upload-time = "2026-08-21T03:56:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a7/d64d8ecc78b0c5e29404720969832c5b3b45b432adaefaafc8d84caac04c/crc32c-2.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ba29ab14a7c3c03a3bc38ce02fdbde5922d3c2a07d81aa428ed40d5305d30e13", size = 80049, upload-time = "2026-08-21T03:56:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/e8ebe70ba2f9f77ef7291ddce673c1036d690a68b8effff0df9da9c6f22e/crc32c-2.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d63b4968624a6a2b5987c0d0d75fbc346fdba67008cc3165013d4ac4db41ff12", size = 79103, upload-time = "2026-08-21T03:56:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/58/28/1b67b5088da3c8a0e7be1c1e46d7fcaff3304e017ab315bfa7324678d46d/crc32c-2.9-cp314-cp314t-win32.whl", hash = "sha256:df3529cbbacc90d57c94560d6a34b5ec7522337ed8ab43f5df8c933490ac43af", size = 64324, upload-time = "2026-08-21T03:56:43.104Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/3e825e4dabdeaf8dfe863c736e0789fa31d124baa294f8603ce5057e6cb2/crc32c-2.9-cp314-cp314t-win_amd64.whl", hash = "sha256:b11a7264cf31498bc74cc7adaa9633b0137ee6927feccdf71764caf1bb94bed9", size = 65737, upload-time = "2026-08-21T03:56:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/51/a9/761e882197c0fca5d65def7e44e211997984de65d554c91c33b8154bf1bc/crc32c-2.9-cp314-cp314t-win_arm64.whl", hash = "sha256:8559843583ef099b1ca6c93af2a6ec83311255bdd476829186d2c5afa6f40f31", size = 63258, upload-time = "2026-08-21T03:56:45.032Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a7/88213fdaa273d3a4a34e34bda05f91830ecd4c5fe6ec71ac06b133cb7196/crc32c-2.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:925b8ac1d545dae4f220f04ce224f32f8d0dd72f8e73b2bf8138dea5732a33da", size = 62381, upload-time = "2026-08-21T03:57:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/80/47523995496a80fdc3349fc51c5f0d628e6ef0574957be307001e6dcb5db/crc32c-2.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:25a1b37f6c5bdc86473203a1d0ed9054f5c5fdf47c8789ac8e40d4f9b003ff38", size = 61265, upload-time = "2026-08-21T03:57:17.028Z" }, + { url = "https://files.pythonhosted.org/packages/a1/07/ab215bb1c8616e3fe86a5af6cbb23e1dce1eb744672ea58c633053d2348b/crc32c-2.9-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:964e3447889fae9d32614e6a4bfe393d82d600af6d0194dc585d73b908cede17", size = 64145, upload-time = "2026-08-21T03:57:18.861Z" }, + { url = "https://files.pythonhosted.org/packages/44/a1/8bf4a53df5145e724094ea58c7aa1bd2fae8e33d8fb05e8054cf7cb4ca58/crc32c-2.9-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57b0f30f74f681286a1a1d626ff5be0a02d0df825f6cbbc5891ef2f5e4c5bce9", size = 64853, upload-time = "2026-08-21T03:57:20.457Z" }, + { url = "https://files.pythonhosted.org/packages/6c/26/9f4d12e292b962eea25f8a8b472845721c3ab26c3be4967f4d581dad9039/crc32c-2.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ef209d2dbbb08808ef930f7b33f0822da0b782d139a0947d4e7e974848b58976", size = 65812, upload-time = "2026-08-21T03:57:21.831Z" }, ] [[package]] @@ -824,10 +817,10 @@ wheels = [ [[package]] name = "cuda-pathfinder" -version = "1.6.0" +version = "1.8.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/22df83f82f9bc26cb1c42265cf14d34d4908dba2a0f261bd7b28244acb00/cuda_pathfinder-1.8.1-py3-none-any.whl", hash = "sha256:ae0137ff9e56ea97499bcbf54f5f2778ec25f3266715ac86da192a795af982a8", size = 62552, upload-time = "2026-09-02T16:55:28.64Z" }, ] [[package]] @@ -881,7 +874,7 @@ nvtx = [ [[package]] name = "diffsynth" -version = "2.1.5" +version = "2.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -896,9 +889,9 @@ dependencies = [ { name = "torchvision" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/55/b02e7f35acb7897e000f78c3108ddaf957823089826d627e5aaec01bde18/diffsynth-2.1.5.tar.gz", hash = "sha256:0170e6359efc0e45895cb4b981facb1651c9b5c729c33d9b892d35e420bbbccb", size = 710924, upload-time = "2026-08-28T07:11:55.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/ca/899aefdb84fc06d4f950054ef1b8ba2ce15a5165795ede2e8b3c3667f483/diffsynth-2.1.7.tar.gz", hash = "sha256:8288e7de90eced019c258979721472464041fb32fef9c145d5240d627c105d92", size = 740328, upload-time = "2026-09-08T07:43:45.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/b0/de59bed6d9291ffae47245fdacd32133abe947bc153391e449ce4e73c024/diffsynth-2.1.5-py3-none-any.whl", hash = "sha256:1c67cbdb1476c57dcbd49f0d4961fade900f2e4aa9020884f9e4bf2e6c4fc56b", size = 789689, upload-time = "2026-08-28T07:11:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2f/9c892a99a31a1c6b3e52f263f1075372052856b5d979ca5cce51d5a26dd2/diffsynth-2.1.7-py3-none-any.whl", hash = "sha256:9567686b6e7d0983f9b9299fa95f686c0796f1b9b3d1d6d15bea92f94f43ba14", size = 825969, upload-time = "2026-09-08T07:43:43.359Z" }, ] [[package]] @@ -942,11 +935,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.2" +version = "3.32.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/a0/50c2c0ce5e74d7721bbb1b19a26ebd339aac5878553a6e35308c2f31f935/filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d", size = 222838, upload-time = "2026-08-31T18:56:34.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/36/d2/b70a31e13d04456d28493f31d2aa087e99eeb2767ef0293b2625727ccb8c/filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2", size = 100003, upload-time = "2026-08-31T18:56:33.078Z" }, ] [[package]] @@ -1064,11 +1057,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.6.0" +version = "2026.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] [package.optional-dependencies] @@ -1256,7 +1249,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1269,9 +1262,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/35/42316e8f6908b6d21bc8df017cc6efba94fb5edbf99b64e28dd142325e20/huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad", size = 963121, upload-time = "2026-08-27T12:18:37.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/97/2eb4abaa5b969ed385066a0496a3823b3ff467fc1082e2202955f1867d60/huggingface_hub-1.30.0.tar.gz", hash = "sha256:e6a6120bc8c8e2723d03648434ee247088cceb55ba7067e7d34d692cad5fdb57", size = 964291, upload-time = "2026-09-03T10:05:14.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/a5/47c2ea9b228ccbcba8467e9a64823146e8ebbad29855e591d8f5eedcc9c7/huggingface_hub-1.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd", size = 795768, upload-time = "2026-08-27T12:18:35.431Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/3e45bbe0dd48f4e56b1d46649d342de853cd1c7e815323472ab62687f153/huggingface_hub-1.30.0-py3-none-any.whl", hash = "sha256:96ae0a8e99a234374a6fe43e989ebd21c04640b91ab2927e7e5773ba1131ca59", size = 796795, upload-time = "2026-09-03T10:05:12.21Z" }, ] [[package]] @@ -1319,23 +1312,23 @@ wheels = [ [[package]] name = "imagesize" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, ] [[package]] @@ -1354,9 +1347,11 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python" }, + { name = "opencv-python", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "pillow" }, - { name = "pywavelets" }, + { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "pywavelets", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "torch" }, ] wheels = [ @@ -1591,7 +1586,7 @@ wheels = [ [[package]] name = "modelscope" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1602,24 +1597,25 @@ dependencies = [ { name = "tqdm" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/56/9ecae749768f93d42e85be8d00d2b02cd85eedc58e965f721fc51a7035b4/modelscope-1.39.0.tar.gz", hash = "sha256:d2e223bd42cc8054d1388b7584c52fdfa7f304888ae5c0efe6406d621499d259", size = 4548145, upload-time = "2026-07-28T08:35:47.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/ae/5118b6b8cea49c4071dc54dba80e63ed20da920bde388bf1ed7916f5cd5c/modelscope-1.39.1.tar.gz", hash = "sha256:40cc02051e4cd67931c70f1956b2a22efc1b35f2a432b5110204406d4a28e17f", size = 4548222, upload-time = "2026-08-04T08:27:03.936Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/25/b4ea67e6f9e9a3453b5df7bf3d6fab297a7f9a92b1701165f1950b8f9164/modelscope-1.39.0-py3-none-any.whl", hash = "sha256:a363298e4620db83b3ec0057a23a73c4ee1fa122ca11b375e72ef94874123efa", size = 6038508, upload-time = "2026-07-28T08:35:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c5/ff578b44a7ba5d86edfaae1ce7db9082f95477bd22ccb2514c1aee4636b7/modelscope-1.39.1-py3-none-any.whl", hash = "sha256:c1567b9d319b11d815832d13c11f71a6413b19cf6374cef6937c54d830552a97", size = 6038125, upload-time = "2026-08-04T08:27:01.121Z" }, ] [[package]] name = "modelscope-hub" -version = "0.2.0" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cryptography" }, { name = "filelock" }, { name = "requests" }, { name = "tqdm" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/9d/3fabfd2b147d94e1980d670fecd3c183999db04a71b80a546bcf37cc6f89/modelscope_hub-0.2.0.tar.gz", hash = "sha256:00288d3e08ad7fadfdd7539f4dffa9d75f9006c43c73abd33ea51767317f001c", size = 163316, upload-time = "2026-08-01T15:00:08.694Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/d8/a61ac59ab55f4d8d6c743ac52421bbe0cd68fabf270908e68585c04deab6/modelscope_hub-0.4.0.tar.gz", hash = "sha256:5be8a9a317871e45bd34e9e82ea18b0f831e48d79763b600255b869c396025b2", size = 233483, upload-time = "2026-09-02T06:46:34.771Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/be/3b0e14c296a622c920287fc6d2c1b5a8668c8e0d98662f7b2bd8f3fcc779/modelscope_hub-0.2.0-py3-none-any.whl", hash = "sha256:84668c80c1d36d1fb1ea1ce17b707d77e044b92255288618c3b7de5b13834418", size = 156023, upload-time = "2026-08-01T15:00:07.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/6e/27dbdc0a324ee7496636c033e81753ccc97dafe7480270513cb9b86359f2/modelscope_hub-0.4.0-py3-none-any.whl", hash = "sha256:deab83b5cc6991fa7889e87c1f99d61f26ba57a02ec6d5b5156afa198eaa170f", size = 200414, upload-time = "2026-09-02T06:46:33.287Z" }, ] [[package]] @@ -1936,14 +1932,14 @@ wheels = [ [[package]] name = "nvidia-cudnn-cu13" -version = "9.20.0.48" +version = "9.24.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/791cffd048fe5b044e620df55267e3e95c0e6e07d50b41e377c03dfc910f/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:71f181cd810e90f9b6023b01186fe82d13d65f0ec098581ee201d39fad769e4b", size = 553099438, upload-time = "2026-07-02T16:27:42.58Z" }, ] [[package]] @@ -2013,11 +2009,11 @@ wheels = [ [[package]] name = "nvidia-nccl-cu13" -version = "2.29.7" +version = "2.30.7" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, - { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, ] [[package]] @@ -2100,7 +2096,7 @@ wheels = [ [[package]] name = "openai" -version = "3.6.0" +version = "3.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2110,9 +2106,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/76/913b755a1a6b54e2d9140eb8d488aa0d47c7359b1d7eac5e864cb7913bbf/openai-3.6.0.tar.gz", hash = "sha256:18fe3f6e96390ef41ee27b152fc9effefca321c33673bd9b956a572493d3ab9b", size = 1455376, upload-time = "2026-08-28T22:29:18.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/77/3508ca0f04f42124bb89ab6e3e853c144660b9a680165beb49cd48634bce/openai-3.9.0.tar.gz", hash = "sha256:fea0fe63d04a27e9da588f673e291183fbf4813767e9df743e351bb548afc60b", size = 1481863, upload-time = "2026-09-08T16:43:15.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/94/805b87ecc951c49ec8f247f5e8eb324ab064bd2ad73b6a0e704dd49aa073/openai-3.6.0-py3-none-any.whl", hash = "sha256:508e2158bf971687f953b62e44b02f207792c815aac306816386d7ba34d37f5f", size = 1699841, upload-time = "2026-08-28T22:29:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ec/59307303cdb38d89f73555245cec54ab5fe34628c39b78b24ea48d0bbed8/openai-3.9.0-py3-none-any.whl", hash = "sha256:8e0e9a1abea664507f03fdad204579092d0fdf1aff54e8335c2098041d4c0345", size = 1741583, upload-time = "2026-09-08T16:43:12.369Z" }, ] [[package]] @@ -2137,9 +2133,20 @@ wheels = [ name = "opencv-python" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] 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 = [ @@ -2151,13 +2158,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, ] +[[package]] +name = "opencv-python" +version = "4.14.0.94" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/e8/c2959c0f969ea4ae96b55ad10c108c0410de4e8b3550d9941a4cfdd52703/opencv_python-4.14.0.94.tar.gz", hash = "sha256:afadc76a38a07713a4c2b4702da917cf0ed489be57e2a40a34e670189731de7d", size = 96400225, upload-time = "2026-07-28T19:23:09.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/91/e4fd7c2b64fda9792bc1f5dcb5a6cbdb54d50c3a2fa9dccc3943fd47afcf/opencv_python-4.14.0.94-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:04456937c8f12ca64ff94df8bfd813ca98f5c9767da792948969a759c716ebe2", size = 46491087, upload-time = "2026-07-29T03:52:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/8895ca8731f741925acf238f87ccb21fa8f47378d5291792e932b9a4ab71/opencv_python-4.14.0.94-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6a49842cc0ba9f43e35a289e61c0e15a1f8be8df60145ee5c7d42593b0d4f4b8", size = 33084035, upload-time = "2026-07-28T20:05:52.633Z" }, + { url = "https://files.pythonhosted.org/packages/84/ec/5777752567986b99222d52e22306e9616829480cc2d48ab241daa9d7e9ef/opencv_python-4.14.0.94-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3198fcbcb8f98fd68063bf2c75d129b0f50d46e555a4e47578a6eb086eb38e1f", size = 49513397, upload-time = "2026-07-28T19:17:28.309Z" }, + { url = "https://files.pythonhosted.org/packages/13/09/c05887c383caa9080ef612febffd08c5da1c283418687a3bea6ef7fb6faa/opencv_python-4.14.0.94-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ae0b5a2acbcfb9813035f1644cfba751edd02797fac994e32180813eeef8420", size = 72049625, upload-time = "2026-07-28T19:18:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0b/df7ac73ed46258d569e4334c03b42515fbc2f901d40831e64414ac0da66a/opencv_python-4.14.0.94-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:add600736d0d65a39e797a9ceeb494f3cdc5736d4bd96b4f89d65a8c92451528", size = 53432460, upload-time = "2026-07-28T19:18:43.878Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/97f176047c56726a61ddd02300b45b35d1451278c6182688a1fb27035e81/opencv_python-4.14.0.94-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:726a2b40c1120a0bbd0116394c6a5b2aa0a4dfa4e609a81ee10d12c6a34cd372", size = 77393698, upload-time = "2026-07-28T19:19:14.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2c/42491165dea74fcefa9a438ccf9ab6c7f3a52d595688ac90e87c4c1e9774/opencv_python-4.14.0.94-cp37-abi3-win32.whl", hash = "sha256:549c2401f8654708c702bdf0e26d45816b9c411369a0efa41907ec2b4ec0e707", size = 33162505, upload-time = "2026-07-28T18:32:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/08/f0/ebf59732b54b4dd8a9e23f3b453ad73e11ec2230faf25ff4b1befa191ce0/opencv_python-4.14.0.94-cp37-abi3-win_amd64.whl", hash = "sha256:ace53616cdffc9643e17e075397b493e607c427d388c7508798ef7ad2ed577cc", size = 41166165, upload-time = "2026-07-28T18:32:19.031Z" }, +] + [[package]] name = "opencv-python-headless" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ @@ -2169,6 +2218,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, ] +[[package]] +name = "opencv-python-headless" +version = "4.14.0.94" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/8a/51ff1c6287be0b6cd3070cf095875da4ff7d6eda38507e6ada5608786a78/opencv_python_headless-4.14.0.94.tar.gz", hash = "sha256:4afa2ea1214453648be88259f035712454faa9039b686de7753569ba8eec1577", size = 96405664, upload-time = "2026-07-28T19:23:48.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/56/12f9b05628cf4ab6aad54479b9124f7c1f2f92b6a4e47c071beb63d926a2/opencv_python_headless-4.14.0.94-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:bc7db37dc234f7bb3190a158fd9dd750357246fc7d8adb23698843ef721a993b", size = 46491231, upload-time = "2026-07-29T03:52:11.902Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/2f9c42466f62aa27e97aaf6c9d914008895ecde28013ba4beed7f541f593/opencv_python_headless-4.14.0.94-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1777f43c9fa064f54b916ad70d944b4fde0a644a17e49f04a966bb24a4b5f1e1", size = 33084176, upload-time = "2026-07-28T20:05:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/7d/91/83c51e3fe000b5dfce4f3be5c5d320e07ebab5c1742abc4e45674503f90f/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29714d7716dbfddf9fec20ffb878765e94ccaecd7d3ebd6750877420296e2dc5", size = 35405458, upload-time = "2026-07-28T19:19:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/de/06/14c8c5d331bc72738683a9b227966aceaa4c6d08752f33d64578aebcb6f8/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5e02669eac0ba67b2a22d7245af1e8ee1a2ef1185ee526a063d8f0555224bd52", size = 57534165, upload-time = "2026-07-28T19:19:52.742Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/0b98bd582582253f61c4506f9b465cf5a8aa7fa4f7237dd99e1b5151a159/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:97c6e818c6f71c0cfa214e12293b1d0266d679c38f4e086de08357c8ac6ece0d", size = 38104309, upload-time = "2026-07-28T19:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/7f538891722a4f06921419d55bac6f41729258d54442861edb2de0dbdf5e/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:211e581f5a4670acbbe08fff36a35e9946039d2eea28b80394632d036d1be527", size = 61960165, upload-time = "2026-07-28T19:20:33.074Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/ded03abad74d7264fa6bc7c923d7cade6f94a9023eeb372cfad54c2d0334/opencv_python_headless-4.14.0.94-cp37-abi3-win32.whl", hash = "sha256:f70296aa7ac9d7ade0d925c43fdc006c83e20a23f30e2f40f1b82c74a7a54460", size = 33030271, upload-time = "2026-07-28T18:32:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8d/db8673846ee53cbb5de4c2b4decc11cf733e203eb7d5146297869f69bd48/opencv_python_headless-4.14.0.94-cp37-abi3-win_amd64.whl", hash = "sha256:cbed65415b8f6a9541c705afe3e64795840524d0ff3bc58f507826284a1dc64b", size = 41028030, upload-time = "2026-07-28T18:32:36.724Z" }, +] + [[package]] name = "opt-einsum" version = "3.3.0" @@ -2427,58 +2507,58 @@ wheels = [ [[package]] name = "pillow-heif" -version = "1.5.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/dc/6ab7a469ced899fbdbbc27b00067e4da02c21ff1ce7e9c6110f14e8fea6c/pillow_heif-1.5.0.tar.gz", hash = "sha256:16b11a37b762ff42da2d36527bb5cb14bd9194c24c389ee911155d6e23c53065", size = 17114105, upload-time = "2026-07-22T14:27:50.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/be/0202b6492a225ec0a6cebc615fbf587179a72cf3a7fa91c0753cba7b986f/pillow_heif-1.7.0.tar.gz", hash = "sha256:1caa5a97364665d45056d25be302e586c39df64ee42ce55241e3735e089809ed", size = 17118169, upload-time = "2026-09-06T09:36:53.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/ff/c9c74db32f93b890a9c90ee75e00e9a6e40f781417bc12acd1c1f0613c7a/pillow_heif-1.5.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:e07ad41fde21396c01b14f2f3026b26c85a4d8696e7b9514b6f43b29f9cc08fa", size = 4742816, upload-time = "2026-07-22T14:26:36.684Z" }, - { url = "https://files.pythonhosted.org/packages/34/f3/c3b63ff41ddd65bea40b0e1a176ae08c613e9948ca1a45afca10207dd19d/pillow_heif-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:88d74a74b144c2e6e99e18e23a1637aef5f63ad32c718c8358da87ea81f94194", size = 4262190, upload-time = "2026-07-22T14:26:38.242Z" }, - { url = "https://files.pythonhosted.org/packages/9a/19/aa5c298a1b9dc41cfc0a846cfc10f1aaac9b72b9eb27abfc954575bb8929/pillow_heif-1.5.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fece01cccd4faccadc912760cdae3e3a87b66fc2bda36e44d3bff79549c83d1c", size = 6348002, upload-time = "2026-07-22T14:26:39.653Z" }, - { url = "https://files.pythonhosted.org/packages/84/69/a8f1a432263632db6e633d87a1658265b09a1760020cb7dc711490b871c6/pillow_heif-1.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f7ada424aaf38d1a4592552c1340a04f1f0e3de30f6269514fbdc3f3b45bbb", size = 5602706, upload-time = "2026-07-22T14:26:41.241Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f5/9b2a6d237f40adb7c7300de8b5c74df6292a358b155e78510ea3362162d0/pillow_heif-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcdbac30cc1f10a89e6c6ce5ef92a539c34d03b1b68c536b092b8309c40cd02e", size = 7376030, upload-time = "2026-07-22T14:26:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/07/f4/bedec2253833a385ce4425c3791a5a37e493aab93a27622471928751201e/pillow_heif-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6838ed3b88bb1a96db3079ec4fb7850c17a6a743b506f12a26217d7bbefa2731", size = 6634293, upload-time = "2026-07-22T14:26:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/8d476b89c1e1da50a5f51eee44c2a18cab025e1dc12678ab0fe8ebadb1aa/pillow_heif-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3cb8c18e5939839cf76a1617f4df5e0b497d1e3a8260b273590168e72d7f8f8", size = 6520153, upload-time = "2026-07-22T14:26:46.743Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a9/813d6c8c6320bfbdde3f5ada81dc6db460a24589b38ba9dd33a62b7cab17/pillow_heif-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:392dc2eba0e7eb526b3a5f475c25ee162a341c695f8db321c7c106c8724303fa", size = 3810987, upload-time = "2026-07-22T14:26:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2c/6135e0f6d76d5c5e31dbeb8ea50a1c4f2f872122055bf5c2db714a772c1b/pillow_heif-1.5.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:115d538c6b4ebbf13dcbadfa5215ffe56365a0de10849ae3a75329d36bc04ee6", size = 4743136, upload-time = "2026-07-22T14:26:49.693Z" }, - { url = "https://files.pythonhosted.org/packages/23/3f/96c6143c006fa6188898c19cde2d131ca7f32f7f2d720514912d56cd29df/pillow_heif-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f14cde17b9288a9cbabc59c98a5928c54150ab98e3577ef2e4fd9f77f524b61", size = 4264318, upload-time = "2026-07-22T14:26:51.163Z" }, - { url = "https://files.pythonhosted.org/packages/78/f9/05177861cec40bf8645e5194d5bac8a88b409f4c63a447298df7ecc2ef67/pillow_heif-1.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a0e2b2966418cd2fb9a2b8427f60516980e3c9146ddf0a00e443f254d7ae291", size = 6346272, upload-time = "2026-07-22T14:26:52.746Z" }, - { url = "https://files.pythonhosted.org/packages/a8/70/5f9c81a022339914ea8b8370c284e3a4327d21dcf4242b9461baa44047dc/pillow_heif-1.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c8032c6c2bddde009e703e045a0e17f7724925403982821ddd1a8d78559f28d", size = 5601930, upload-time = "2026-07-22T14:26:54.412Z" }, - { url = "https://files.pythonhosted.org/packages/68/91/ce4fbd148e76daeb147a50ed761dc43d7f7d4b95c4796182b6f36e408afa/pillow_heif-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36742b4f3f5bee57a90d9492b17ad0336b95ab7fc358172cfae144505433ac94", size = 7374394, upload-time = "2026-07-22T14:26:56.061Z" }, - { url = "https://files.pythonhosted.org/packages/20/5c/32af6c25df15783a068b524648a83f854abddcd681155b6320737875c144/pillow_heif-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5533c061cfef1ed508137bb7303093ef7b3b1ea045c70a778554f6d551ed540", size = 6633463, upload-time = "2026-07-22T14:26:57.722Z" }, - { url = "https://files.pythonhosted.org/packages/eb/14/0a73f6d2605a20d32bbf7d20ee033d103b49215957d0330f09c64b3d0e19/pillow_heif-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:9aa2cdc94d8d0b26dd3c16c3421273eb394c24e708899d8740dc314904ea0453", size = 6520223, upload-time = "2026-07-22T14:26:59.31Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2c/102cfc58546ce2c00c312174ebc14d89efae8670f5c4f3c6bbe2e4f23197/pillow_heif-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:901121ed12b15bd1451e70ba7fca0233990ba958ec9c6f8751838402e69e1078", size = 3811009, upload-time = "2026-07-22T14:27:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/33/92/502ae6bebaf1fc48850f690858040932fb6fe0bd0de25b5e715df2517d14/pillow_heif-1.5.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b736040d566271143c09bf87967549daf3d3090e9b4283ca2076c3383ab141d8", size = 4743134, upload-time = "2026-07-22T14:27:02.444Z" }, - { url = "https://files.pythonhosted.org/packages/e3/4f/a9f2e1525655c93e9b72cec3792a41f7d732222dad1d329fa04c227cbe28/pillow_heif-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d7c9dd6f490c433267ab9b66b63961e59a4c04c9d0e38ba66e4b6664d5e7d8", size = 4264310, upload-time = "2026-07-22T14:27:04.13Z" }, - { url = "https://files.pythonhosted.org/packages/7f/27/5f901b1d69d641b3b4e99aae6b3cbebe0cfa53188517ca7c79f86f86d4eb/pillow_heif-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdada8d4cd5886c9ec5454b907290c4cbd420809afbf2c251120f73ba2ee1d12", size = 6346291, upload-time = "2026-07-22T14:27:05.547Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7e/72c9fd99a74c82b08df3690077141bd16f1c9c7a3b3cd1c30e6fec4eb4a5/pillow_heif-1.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e87c9a51ee973b5c1896beba74ddf9b58417f5b8a52d021b493f3ef6257c5f90", size = 5601975, upload-time = "2026-07-22T14:27:07.252Z" }, - { url = "https://files.pythonhosted.org/packages/82/ff/be2b42f3aa4ec040fca5a2bab8bac6b611d39b7a3c69c185c85ae8537a6d/pillow_heif-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ff580e1453bcf3d4965dbbfa00bfabef7784a51214413a344ce2eb652a81a987", size = 7374392, upload-time = "2026-07-22T14:27:08.804Z" }, - { url = "https://files.pythonhosted.org/packages/b7/34/047aba6e860dbe8e32dc948d5ed707c921168769d34379b24956322fb4c3/pillow_heif-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cfa3761cf4fe43ee4b909963f87f05bac925d7bcff7016802f02a26574fb11e", size = 6633487, upload-time = "2026-07-22T14:27:10.815Z" }, - { url = "https://files.pythonhosted.org/packages/46/0e/4b463d7183ee45624fc822f22501feb4b772e5667377d2f981a31c08310a/pillow_heif-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:139f95d1052954804b349089deb91c55ecfe344a7a21ef46c961778a45a17589", size = 6520219, upload-time = "2026-07-22T14:27:12.642Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ce/e69cd5b9aea6e75e361d3d3a98201bb7eab31455b07a5b1fc8ec570ab2c1/pillow_heif-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:c6156bee6f69797a782d8ca373536dac4a7e0d4f793c9842f6bd09669fe4da0f", size = 3811013, upload-time = "2026-07-22T14:27:14.211Z" }, - { url = "https://files.pythonhosted.org/packages/67/51/9b5a9352b86b7d82ad62969a84e3b4f998f6b5dc0320cec3e2d54d74d754/pillow_heif-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8d96783ca1648ba8384e66b888128ca3a48d769c0e3593acf84c40ce0c791629", size = 4743156, upload-time = "2026-07-22T14:27:15.721Z" }, - { url = "https://files.pythonhosted.org/packages/2a/10/dc89bec0edf37df5d7609d76ce125a2f8d1e901b2d34944d64d05f497bef/pillow_heif-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5c50626e77510f38cb11aacb3b4bba56eff17ce39a9d85b7ee210bd81387a083", size = 4264368, upload-time = "2026-07-22T14:27:17.198Z" }, - { url = "https://files.pythonhosted.org/packages/cf/54/837e9077784df4a0ba69d3e76e9f29a4e52929c84f0e1b0d29a13437a5cc/pillow_heif-1.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fb15135733626ee3fd4d7104bde1e54d4370f9f8b697a5d239b8b552b73fc7b", size = 6346432, upload-time = "2026-07-22T14:27:18.55Z" }, - { url = "https://files.pythonhosted.org/packages/2b/90/42f392c0513bf351ba2bca1350c457b0d7bd94f12d0e5fa74d7a9deae336/pillow_heif-1.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:646ddad98c8f9c5e637dbb84b6b7db905e565994e9fd6a496a36adff36ae28b3", size = 5602088, upload-time = "2026-07-22T14:27:20.112Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4d/8070974a2e695f83fb10883b7d83c3eda635561f99993623bf111a55a984/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9803c1291c7c598134f16016a071ec8d9dce268ab1806c8541450c4e3972893", size = 7374543, upload-time = "2026-07-22T14:27:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/4a/78/9a5abfb163b0fb44a4f624146de142ba4887e5011f965442af2c997db6bc/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ad925299422d4592e4b5344c03e25b27dc7a5130447603a89404d7d138b746c", size = 6633558, upload-time = "2026-07-22T14:27:23.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bc/7bae1c1efcec25dfdca950fe1a10cd723c544269db73203019e965394015/pillow_heif-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:cfd5f8a8a7b65f2ded217c5bde4dd9365b6b67187657a121b05224841a1b92c0", size = 6703549, upload-time = "2026-07-22T14:27:25.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/8d/fff074683f96eb004d7159228d2fc9e2e9fe8afd899d53aa695c622487d8/pillow_heif-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:6759febf63ea31fd00756e4a828cd643d0b9f98c93f5814b0ce5baa036da50e4", size = 4017234, upload-time = "2026-07-22T14:27:27.443Z" }, - { url = "https://files.pythonhosted.org/packages/57/04/fa5727a62b2fc37b611c7a172fc10c6719530d27bc9126de8456fac69346/pillow_heif-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e2c3f47eac33d8368ea5779eb7bd8c736e7048b5897474d7e1799c073c9505e3", size = 4744030, upload-time = "2026-07-22T14:27:29.047Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6f/afaccf55774c1084e5beba83f0ac201d315096f6a95d003696376184a7a3/pillow_heif-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0881c82752d676fbe054cb0258100790ead94946fbb075ffa041bb9ae579b11", size = 4265061, upload-time = "2026-07-22T14:27:30.703Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a6/c90f9fffd5cb56e213753f22a9a955525d5ad99816481ae2392ab6c7ccd8/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d5810606f70d3432b55987a5e44aa26fb7598fea565d8645b017f7ed935f5e99", size = 6351671, upload-time = "2026-07-22T14:27:32.231Z" }, - { url = "https://files.pythonhosted.org/packages/74/b3/e15933817e71b9fcd8865e3b64171995ed26f5ca329bf4d03f3571213c52/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e737d326410e733868566c045f651b5371086a4ec294c40d39e129481d024bef", size = 5606028, upload-time = "2026-07-22T14:27:34.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/2665f6c781b90bb5a25595c58ede1955125f0ad2c7d81bf3c153c9e9c9d8/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522719e5dd56eae6f9f48828c08eefe0e5c02dd92f5a07780e6f3c04bdbcf880", size = 7379667, upload-time = "2026-07-22T14:27:35.883Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/0f0487b9f6cb1f5dff629b42ad08a7ae1997886048c4c2671e17079ad5d6/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f1d281b2efee954ef78c0a473a41498d0548a32290d7250b4879ddd2480dffb8", size = 6637581, upload-time = "2026-07-22T14:27:37.67Z" }, - { url = "https://files.pythonhosted.org/packages/8a/b7/c65adbcab1b1c99c886c55b97b144b3092c432e7b0fedb91501c479cebe4/pillow_heif-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:65b3d619cea2d1f758cd039cddb331cb44d63768c355606ea9100cfee4421eb4", size = 6704174, upload-time = "2026-07-22T14:27:39.32Z" }, - { url = "https://files.pythonhosted.org/packages/17/53/2a52f64f399717adae560068e9b0ae12ae3e43be2c64991e246af5aedc3f/pillow_heif-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:508fc9dd8fb4df933b666c30b60b0930270d9e072fcd90b75990166050e44656", size = 4017728, upload-time = "2026-07-22T14:27:41.102Z" }, - { url = "https://files.pythonhosted.org/packages/f5/67/d710e1ee10ddbe748e9df5aef1df591123468616a40fada56569de0f2518/pillow_heif-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:563dd2cf45325944fb8d405f14c2876dc5e18e85a94dfafcd474579400010651", size = 4731076, upload-time = "2026-07-22T14:27:42.582Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/4259389bff87df8ea5d51f4399016e75171325dc2c21d5fd8ffd4bb9cebe/pillow_heif-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e402f7a6f3c3d8352e773562d35525b13ba1e9372ff660034754dd65c52ce463", size = 4261485, upload-time = "2026-07-22T14:27:44.235Z" }, - { url = "https://files.pythonhosted.org/packages/d6/38/e00160ffb328b639f2182b308c94a3f55123a115127a82a6aea25d51eb03/pillow_heif-1.5.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f8289a54b6109268ed3a518970eecf9c834d76ee2af5aa489051a0ed4d3d462", size = 6303812, upload-time = "2026-07-22T14:27:45.68Z" }, - { url = "https://files.pythonhosted.org/packages/12/1f/a64b73c8c8eac669e04d4769d6ed76507a80be1c20634adbd5579dbdee00/pillow_heif-1.5.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:920ea2d3a88dc115d58bd792ef3e89be98f01d68ccd144efcbffc5d4a683aba7", size = 5555237, upload-time = "2026-07-22T14:27:47.297Z" }, - { url = "https://files.pythonhosted.org/packages/db/f6/4d85fc3a81cbb2969f485fe3dd165677574080a25f7160245cd4cae73998/pillow_heif-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6aa53c40d9aa1d7433b35823a8cd13463e057b2106919fec40cea14967548bd1", size = 6520559, upload-time = "2026-07-22T14:27:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4b/5f2ea81e48a683423d1eeb4eed1052b4873c6b2a16891b1e87ddac4fbc8e/pillow_heif-1.7.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:bfa2e9607d13d6d2dc8fa14916bac5e8793bcebdb7357d41a7dca6754a6d6967", size = 4759580, upload-time = "2026-09-06T09:34:41.679Z" }, + { url = "https://files.pythonhosted.org/packages/af/97/a3bc9b757a80beb7dd1a341e246070587f082529e27b6692ccb45cda286f/pillow_heif-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e33fc164268d96ce119e2bee43311bae77a7e770f3b6fc38deef082adaee5b3", size = 4277407, upload-time = "2026-09-06T09:34:43.798Z" }, + { url = "https://files.pythonhosted.org/packages/08/be/8c399b9ac1af825e1b45a57b65f7955cd5918fcd50b6ef974958934aadb5/pillow_heif-1.7.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e7280cbb6db614a6dd88b71c1d7eb7e9cfc2e0d58e733afdae85e3227a63306", size = 6368897, upload-time = "2026-09-06T09:34:45.763Z" }, + { url = "https://files.pythonhosted.org/packages/97/5e/7d3e1f4d88b811e43df85b5094238d43e093fbf82589a8a8dab413130cf2/pillow_heif-1.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b31b45bfab2b3f6893199bd13387283445ba656d2febb13c386fc749b2438a", size = 5621744, upload-time = "2026-09-06T09:34:47.621Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/6d72d93d2ee681c9b2ed0c45bc21a20f6a7ae2235d62a6853537fd3d767c/pillow_heif-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0049ecf390e7915e753e585651d22134284176ae583e54c4b863330c838a9835", size = 7401602, upload-time = "2026-09-06T09:34:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/24dc10716589dc748d3b48effbc8f3c5d974fd708fc404301a5f54593118/pillow_heif-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9abac47964c29585f8c7914ec8cff0a010723a0a127cf6efe9ee78a7aa91c2a9", size = 6656918, upload-time = "2026-09-06T09:34:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/69/3a/6af9a351fef29ff408223ea930e2be58ca8b3ee1e318bea7359d8636f54b/pillow_heif-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8654b111c2d5cacbb603dbb2a01931421489a26a391d810b81400e4f97478650", size = 6583495, upload-time = "2026-09-06T09:34:54.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/51/44b6d35226091f0c25df12d3ce6b50de938c5b21f5e7dd3016270e0c85e5/pillow_heif-1.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6111f39c197a78dac0cdbd19bf85f45dfea3847dd57f39af7c28ac538bea5b7", size = 3851336, upload-time = "2026-09-06T09:34:56.514Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ee/ec6ee6fc94088f213ab17738966f9670e832f18a6b5d03ceba1e74dfa653/pillow_heif-1.7.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:cba79f030caf9e543b6c9a7c77cd8df465785bc6dcc1361b3fd5f905b3ca4e01", size = 4759735, upload-time = "2026-09-06T09:34:58.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4f/15a43142f42d29e302d3054df18bdbedc744af4002b490deea74cf543ce9/pillow_heif-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1f1d001576e4cc2a37f1a7a3c06b3241e7966be16670789daba428a3edf8e30", size = 4279520, upload-time = "2026-09-06T09:35:00.316Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/5bc352ebdc2924edcb3798d284416dfb77f5d90c7c08273104bda7edeeac/pillow_heif-1.7.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f23113505672a6c55e88d7c96cbd82f3d8a5f902d31a3c071be96a9a7e98c5dc", size = 6366611, upload-time = "2026-09-06T09:35:02.208Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4d/b7be3eebd23e042b738ee57ed211e5fc0d6fc037b24e0047f99e3a16ba9c/pillow_heif-1.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c4751fcffb55f555a7559cfa6721bdfb30f50f14b7f0cbd2b1cba3b5d5961b7", size = 5620429, upload-time = "2026-09-06T09:35:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/76/c4/b4cc21989894938e49b20a6c58e14719ce8a8b2991b1c297d92bf9525442/pillow_heif-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b37df3c08df3401085737e7ed68026398046eb01a8850df27b7a461e9045ed92", size = 7399452, upload-time = "2026-09-06T09:35:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/92/0d/272a1b5ba521dc1448a434a7a043861b0a9b5264e470d40c6cfd02efa6fa/pillow_heif-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b156781538370f3b30a3d2e9899d71a5bc1c89c7e5264123e55d21153e0aa555", size = 6655604, upload-time = "2026-09-06T09:35:08.255Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f7/5bf5eb718f0c4dfb844f012b787943d75ca1bc5c1eb83bfc049430d86080/pillow_heif-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf8e46cfe653a9a43830a7e9993ddf70dcf3877c474b7f1c1e8364d0c85214a5", size = 6583574, upload-time = "2026-09-06T09:35:10.893Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/26cd5ee278b21819c14dc74deb9b1b0002ec64f127c60495623a0c650549/pillow_heif-1.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:78d77d94456d95444683b4539ffe3946554bcc25cad97564b60ae5cc7d22286c", size = 3851353, upload-time = "2026-09-06T09:35:12.765Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/f5aa099875cc881aa19bcd1b8c5ef3d97376a05bdc3b56487e49d01dd9be/pillow_heif-1.7.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:2c602d5177e46fca3e0491572d76ed4de66c63ac5aaef999e5b3a4c1fe101395", size = 4759721, upload-time = "2026-09-06T09:35:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/0d/32/59dfe8f1799eef6a442fe5007e5c199b1961d33c7a56c64e0babc9d4a95c/pillow_heif-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9912a8301d469012fe2ba1f2da539b56de048aab3db649857b4ab5fa9a07919b", size = 4279534, upload-time = "2026-09-06T09:35:17.025Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9c/f2ce4e20fcb898757c0793e56a1696e789a209e7082b9ca4c81e07089d0a/pillow_heif-1.7.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fc18bacac527936118edca386c19b44a5e4e492b3d484284f990f47c0ec6443", size = 6366688, upload-time = "2026-09-06T09:35:19.329Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/ed893eed7d7b6b1cc160a4ddd56d7c2ac1a2122d7c2e530ecd46c1b86c3c/pillow_heif-1.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25978cd8b765e30901d6f1fb39c5172ab9d9c00e1b1b39430e2243f3b4f3e9d8", size = 5620509, upload-time = "2026-09-06T09:35:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488b9e0d072b20dd34844e3967be7b22c5e9600e6af927a137a37e90c6e2/pillow_heif-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35d6be7a1633f8ebba25617b45f35893e5e4200f944a223090fd6f5e529caa51", size = 7399490, upload-time = "2026-09-06T09:35:23.91Z" }, + { url = "https://files.pythonhosted.org/packages/62/fd/a225a80f3b545c2aeb8eca8cfab3b98267aaa5c79df7b0908a17a99e95ca/pillow_heif-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b8de90c07085a45182ca214911fbe211af0fd1c65c78deda108b4164d1586df", size = 6655639, upload-time = "2026-09-06T09:35:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/25/16/e5a07da98b345f11fc9045b488a55442d4562398c839af939e1adf8c2c8f/pillow_heif-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:722ccef14e47a254ef46fbf60863b8290e032d0ac998204f8e455e718a41cc64", size = 6583555, upload-time = "2026-09-06T09:35:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/13/82/d607e549305a77be2a0b51308cab7a3a52db4801e47ab52ba1fab5664b53/pillow_heif-1.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:429c975ecf438ab6417efbc171e9d601701202afe68298fe6055286583dfeb6f", size = 3851355, upload-time = "2026-09-06T09:35:30.643Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cf/810c2ad915e017cdc129579093eddaa00017143de227b93a955ce97abc8b/pillow_heif-1.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6188e8a4203be50366e24345e8fdb50f8f6af54ac6b1875a2fe21b17505e01ae", size = 4759761, upload-time = "2026-09-06T09:35:32.756Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d2/3ac9f209ea5adebbbee6da55e28a3017b672370078feb5c2940526d5a57c/pillow_heif-1.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbb155e2fe0c3c95414661b48bfc6972d53f94eaa487e0ccd29dd3ea17725b1a", size = 4279585, upload-time = "2026-09-06T09:35:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/5e/04/d4481661a720860f48998575916ce5c19d62369b90155a58bbed3ea49e1a/pillow_heif-1.7.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09e29bd7d1cecfcbdb9041c3e2a2f1d6f9327ec3804d9035e2909ec88578f25d", size = 6366862, upload-time = "2026-09-06T09:35:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/fe24ebe67fcac47864b2966620958c21f403feda4fd37fdf4a7108134c17/pillow_heif-1.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:850005a64f30b1905866d4ee4c99b09e3b4d5358873e95e64161bcc45ee9f406", size = 5620591, upload-time = "2026-09-06T09:35:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/64/fb/332ff7e2ed90c8fe87a799221e25d762807073b2e6678bb9c6521cd7bc36/pillow_heif-1.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d62d25bb7d2db573b9386d1624bdb4bd7ba1b8b8aeb7989af06ae33b9333bc4f", size = 7399716, upload-time = "2026-09-06T09:35:40.849Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/4ce87892f1db087ca4aad720c8694912b0cc4477ad27bb5b45a40caec5bd/pillow_heif-1.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b7a259056bd7de5a5d56799f5ed01591d33c1a622aef18be8562db7979961d3", size = 6655686, upload-time = "2026-09-06T09:35:42.762Z" }, + { url = "https://files.pythonhosted.org/packages/02/d6/3234cad7323d3580fb15df02c9992b9a6d7b5878717cd8bc5fca526f0c28/pillow_heif-1.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:21754c373ae59e69fcb1d568f64b5d7c8140f61d5114289dd753dcb2c4869c8b", size = 6762464, upload-time = "2026-09-06T09:35:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/08c25c14a9d6cd64db7a303978c5d2994d5d294f233fcdb14d0928139569/pillow_heif-1.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:f349f9591a19ef4ade9155595b646d3ce907fa95a33fada1b1cda89a441c305d", size = 4058274, upload-time = "2026-09-06T09:35:47.964Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5a/e5083fc7dc247312f717dc418b0503d3cb6b83b2972dd004ba36df6fda99/pillow_heif-1.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:43d79fa16ab142a9a48577417b3efa6ec2d7a610b8a5aa3c77548ecfeb30e473", size = 4760669, upload-time = "2026-09-06T09:35:50.051Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4a/eadca94ba7e38dd9257a6814e71a30c8dedde54cac7be509057ff2d1bdaa/pillow_heif-1.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1494eaf01de9b5571bf064f14239e0036d59e9e0db6c99ad47b9a216f6187383", size = 4280430, upload-time = "2026-09-06T09:35:51.88Z" }, + { url = "https://files.pythonhosted.org/packages/6f/43/e400e019d3e36d1c7e13c45633b8c36b072b378825b103c60d28611ba5b8/pillow_heif-1.7.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52513a022d3fcf14993dcd7290df9b43bb92b5aa8471db79bb680b62c8378b7c", size = 6372810, upload-time = "2026-09-06T09:35:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/c1/cf/bd34565fd81a32ff930894f2823bebed27a44e0954224f71d5ec597b1518/pillow_heif-1.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2444f95432fd3556a915378789cbc1282e1da55a7f17633e8d928fa72e8fbc9a", size = 5624815, upload-time = "2026-09-06T09:35:55.856Z" }, + { url = "https://files.pythonhosted.org/packages/df/28/233c03f61e214ebb6213a48fba1ee4a17202244e1800ad92a884d02500ce/pillow_heif-1.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7af31a970c620b25dd144e3094ebf74162e72e08d367a1901a80ccb321859b2", size = 7404821, upload-time = "2026-09-06T09:35:58.076Z" }, + { url = "https://files.pythonhosted.org/packages/28/5a/0a836f5dac24a517f48b0871e31d234792246aa6e94fbeb2210da84b5b28/pillow_heif-1.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3f70c05d9f732b54ffd226750cc0c2e0d89e4cfd9e19eeb88971c899406ecc98", size = 6659823, upload-time = "2026-09-06T09:36:00.416Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/29365ea5bc00b0338cd2126da2eab5dc70fb2ea79ffad25a775b10c118cd/pillow_heif-1.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:988c0a49af25d856d2291cc5e86562d254a0956352dcfd4e2928c7e2e56dbf17", size = 6763115, upload-time = "2026-09-06T09:36:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/21e8bc38edc447c7dfa0bdcea431cf5f1a58a71736fa9e12462fa11d21cf/pillow_heif-1.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6f211efb455f3daae0ef9c9615327fa864075ab24b664dd0734b2c7ac2c8572d", size = 4058776, upload-time = "2026-09-06T09:36:04.753Z" }, + { url = "https://files.pythonhosted.org/packages/d1/28/ad58e9e3be95105658705eec3b9a2b1dce7ad73d543463cfb5e1e483bac8/pillow_heif-1.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:28e9d787079d8f7e937c6482a3cb2cd816fa23a763d3056e563a8f8a9743c0a1", size = 4747497, upload-time = "2026-09-06T09:36:41.846Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/2fd98449b811b391e200500b615c327f5cdaa559788e3ac81b69a694af01/pillow_heif-1.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ef4a6dd7868f1789d9e0496c83a567903adc947a410338504839b00fe755484", size = 4276386, upload-time = "2026-09-06T09:36:43.691Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/b5594f4838765c0d8ea247dce5ba5f6631fbda6adac24cc349fc80d667f1/pillow_heif-1.7.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc5f92590ac3be371cb5af448918e33e2aaf7770a075e26f5bec10d9c90b2c61", size = 6322385, upload-time = "2026-09-06T09:36:45.862Z" }, + { url = "https://files.pythonhosted.org/packages/f2/21/67716eba85d369c177f5aad65b23ac89db0ff758a840e0a6519de55cd9a2/pillow_heif-1.7.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c6e5ea1084caa5ae9f0571eaa46cb8a584ac32943fb8946b085bb2bce4aa052", size = 5572048, upload-time = "2026-09-06T09:36:48.172Z" }, + { url = "https://files.pythonhosted.org/packages/0b/22/73bc097c0bbc571b5f2cc4596290528b220dbdaa4217e3d7d2658f1af68e/pillow_heif-1.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3c9350ccbd854b67736f84ebafaa4363d2bb45cdb66e71c323032aab166e8633", size = 6583858, upload-time = "2026-09-06T09:36:50.398Z" }, ] [[package]] @@ -2634,17 +2714,17 @@ wheels = [ [[package]] name = "protobuf" -version = "7.35.1" +version = "7.36.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/73/f66c748df06e7fe24e658eddd600d19c4b40bad836c97ce2d0ad9851fb6b/protobuf-7.36.1.tar.gz", hash = "sha256:d0f6470f0ce2b84e3feaea2d4b816378b37ba4d4aa08a274305373de93e2d524", size = 512499, upload-time = "2026-08-31T22:40:04.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6c/3a54a58f2948b0f485df9ecdd06590f15d0a7abf46a89d50c3de709ff4ff/protobuf-7.36.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:3cf2ee25d006cee57294a1196ea43b37feb78e0dcd1e8af5c1aeddb777655aca", size = 456046, upload-time = "2026-08-31T22:39:56.865Z" }, + { url = "https://files.pythonhosted.org/packages/6e/08/9f9548793c771095245c0eeaf0c84b76b16a5ef26158043d456f551344a0/protobuf-7.36.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:43d3d37b1eb24c113b9b7d02008cac44e423f00b611b7781ae998d7623972969", size = 344226, upload-time = "2026-08-31T22:39:58.263Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/1bdbd612fa3c51e42ea6f45d05e84dc748ddcd2663e1aa1e89a00a33facd/protobuf-7.36.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:39c518c05586c016d7874ff6079ee115bcec1ea5fbb1d177fbf7867ef4c67e44", size = 357229, upload-time = "2026-08-31T22:39:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/22/df/c799fe7a05ef16ba853a59db01f3a2c5f7d0676469589ccc4874f76a2a88/protobuf-7.36.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:97198b77e369a0abd8e262b8f6c7266c55ddb796a3a12c76d7b8881188ed83aa", size = 343228, upload-time = "2026-08-31T22:40:00.179Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/d4724ec5d86d496fe4108e220618aa0837ad3423a7af7ccdd9684ccc77c8/protobuf-7.36.1-cp310-abi3-win32.whl", hash = "sha256:0b53ce95272aad50ad25d7ff03373743209822e8ba42ea7fad27d2bee1547d00", size = 443002, upload-time = "2026-08-31T22:40:01.32Z" }, + { url = "https://files.pythonhosted.org/packages/db/37/155788a0d8daded960375af604202805308169f9b859419ea0aa370946e2/protobuf-7.36.1-cp310-abi3-win_amd64.whl", hash = "sha256:51139351435d9b43d88a55eaa49fb6f737fbb478fb0cbf2cf694d1a04a9d3363", size = 456518, upload-time = "2026-08-31T22:40:02.494Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c47f91d3cab175b01fd8c4f0d80fdf8613be876cc616e66ad281a59c5ddf/protobuf-7.36.1-py3-none-any.whl", hash = "sha256:7d951e46b3f963d6c264c367c437921de9d5aedd9c3f9612b9077736b4e3ad5c", size = 179813, upload-time = "2026-08-31T22:40:03.54Z" }, ] [[package]] @@ -2762,7 +2842,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2770,111 +2850,111 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] @@ -3117,9 +3197,15 @@ wheels = [ name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -3173,6 +3259,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/d2/a8065103f5e2e613b916489e6c85af6402a1ec64f346d1429e2d32cb8d03/pywavelets-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3b6ff6ba4f625d8c955f68c2c39b0a913776d406ab31ee4057f34ad4019fb33b", size = 4306793, upload-time = "2025-08-04T16:20:02.934Z" }, ] +[[package]] +name = "pywavelets" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(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 != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/84/a9e80e33eb114e78bd2f1829a90d72d8d4d660109c721a8aa2f0d58d7592/pywavelets-1.10.0.tar.gz", hash = "sha256:f3cb8640225a8f3fdcbc3f6f3bd112c78927c9407cbfa65a5ff303990a6a3e32", size = 3948163, upload-time = "2026-09-07T17:15:15.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/0c/bba2a675907453876c47025c15d25eeb9a3b40359fbdcee2752f6fe567e1/pywavelets-1.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5378e24d8e0bc7a5a13e5b3ed69e5ae3e2404889dd77ec2b66f7567ce6022f00", size = 4327195, upload-time = "2026-09-07T17:13:40.915Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/5af6d72915bc5fb4bf162a6db1b8b78670727c10e7eb9e6b07dca76aa587/pywavelets-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba9e246d5f6e03ce051f07d448d1ba89691ea4815cf8df4f4a5831832a43c0b0", size = 4286953, upload-time = "2026-09-07T17:13:42.766Z" }, + { url = "https://files.pythonhosted.org/packages/42/61/8c7a9337973571511ee5beb6fa11bcc54b7d728273334e271feaf38477ec/pywavelets-1.10.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b08149aaf7446280a7bb515a3731e87e16b39d4dbb98af11243a278c36d12c8f", size = 4411264, upload-time = "2026-09-07T17:13:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/e0c4cc749feda5984af84e510179dc9c37c89a5af62d2ce20c7d2414b85e/pywavelets-1.10.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:262d11406c57a5ffba1fa531d8ecbb270c85208612e9d1f4fc98897ccf536722", size = 4466610, upload-time = "2026-09-07T17:13:46.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4f/e9f7fbbc6a0be1f629fda74706c95fdf7f3026f8e6bdea1a4a73202b14de/pywavelets-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbccca7722a31ea740c48d8ae1a0edc653e37ef554e516d122e2a19522b38893", size = 4417881, upload-time = "2026-09-07T17:13:48.236Z" }, + { url = "https://files.pythonhosted.org/packages/a0/19/dd6ed2b8114ea058cdce9346b5024d1028df9f163a0456b95ec8514ae851/pywavelets-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82e134f287918dde43f9d56ee8aea9174faab509feaea835127e1685365660fd", size = 4472602, upload-time = "2026-09-07T17:13:49.783Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5f/9a57ad0f2a4a15ec5c2dce859f0b502ed360f6da6ccd0f3b97cb2fc38a58/pywavelets-1.10.0-cp312-cp312-win32.whl", hash = "sha256:9cfb974fc316fbca735761474c92cd1d6be2d98bc72c37ddc7ac7a2ae83e221b", size = 4177188, upload-time = "2026-09-07T17:13:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/6490c47aefdc3d210db3e071a9592ca453d01c55a1338682da970792bfad/pywavelets-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:5be6bdee86f8088eb15becaefabfecaa6336cd6103c58f46d2466d127b4f08cc", size = 4281235, upload-time = "2026-09-07T17:13:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/a5/63/bae289a235152e54c757049da0cb6e73ca996ae131d3382ddf4608a028d1/pywavelets-1.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:fab763c551e33c1bdb567688d08bf33d8d79410f2cc0cf0f964c567bf2495639", size = 4175595, upload-time = "2026-09-07T17:13:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/07/dd/6db37a5c4d3e8c0ae7316384fa0cd52f8575c2e15d71a12d320f9dc4b2c3/pywavelets-1.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:761af8d319c9c104969b1a92e0e9c41aad34ca98f229cf87060f202cd9e633eb", size = 4329364, upload-time = "2026-09-07T17:13:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0f/3b2762f936510c0344ce8b97438720ee9f1879a83316410ba475bdeea463/pywavelets-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bfeaada62a12664626b21cfc41e5d0538b3c238dde22e78ea4769fac4bf6160", size = 4287304, upload-time = "2026-09-07T17:13:58.4Z" }, + { url = "https://files.pythonhosted.org/packages/6c/15/dc9a391a7bcf191f6f175492018fb0d3cce3e42784bd759c39e3f422ec4d/pywavelets-1.10.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0699c4c095f4a3121fcedfdb2696d8b173bd7bd47346e2ab7f49f4fc21e71263", size = 4406919, upload-time = "2026-09-07T17:14:00.266Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/4b0bec531c157f5c933df20964a95a793fbd8c933fcc5eddaa0ae9ad35ce/pywavelets-1.10.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d27de9fd9d76d2dcd2debd12f8ac236697cec82345dbada5a0646821cae8ea85", size = 4463469, upload-time = "2026-09-07T17:14:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/a7a54a2fc2aae1c097b03604f4dcb488014134c544d36c7ae78e4b16f58a/pywavelets-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:86fe1cd2cd42e33f21f92a249077988767eb209bd98af07ce4eba431bb8205f2", size = 4413686, upload-time = "2026-09-07T17:14:03.788Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ef/2decd0c6bbb9b508f985dac9208257cf82c5ffa70721bbdd47be4d7d7b16/pywavelets-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45f4ea69941424e4268955f29c34ecc9bf26eeef2df3105689e404075693d149", size = 4468051, upload-time = "2026-09-07T17:14:05.446Z" }, + { url = "https://files.pythonhosted.org/packages/3f/64/f7c0fd52d21e2a0a04116a86a94b0178d1e861550a5d7079b1828215a7bb/pywavelets-1.10.0-cp313-cp313-win32.whl", hash = "sha256:d2c09ee7c5f975b1e176a73d4df105fa6bcb547517bf5e54a6cc304d5e0d2d09", size = 4176584, upload-time = "2026-09-07T17:14:07.365Z" }, + { url = "https://files.pythonhosted.org/packages/73/89/bc45f5be9b4141aa8a9b4dec2340c32301e6f0e968fc348ca59973ab3f7a/pywavelets-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:47179b616e3afc53bfdc1274437af9b541bb1ddf2404929599affdfbd21e51c4", size = 4280183, upload-time = "2026-09-07T17:14:09.188Z" }, + { url = "https://files.pythonhosted.org/packages/a1/59/0fcd739c2a289f741e3ed43832f436a046c751c23a8c8d1b1f41903172f5/pywavelets-1.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:74003c55544b4c090f340cae72e279148fbc5e7994ff85c61dcd9b1feda90311", size = 4174810, upload-time = "2026-09-07T17:14:10.765Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/e7bf1a8608bb9cad5a1e67fbbc9a0f5d6be39cb3af059f2c44be8ae7deba/pywavelets-1.10.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f9a24a229ae7dd53d504aa1381c67777d3adf6cb7f0f6303879e192424fe5580", size = 4331809, upload-time = "2026-09-07T17:14:12.351Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/06309e3c78013514937be186fa2272a30c024c125d87b69344c8a6788443/pywavelets-1.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:949c931ea27bd91552c074d03c14ed7978467c57c0cf0801e8df8b69b0842e85", size = 4291167, upload-time = "2026-09-07T17:14:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/33919732ea21f119cb1e5dbb41e91560e819a9fb2ca865cb0a1a778ae96b/pywavelets-1.10.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6457cee6d84efb9e649dabb14ad0ce251b562552cd889db814cc3bf06847730a", size = 4409664, upload-time = "2026-09-07T17:14:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8a/be55f96a0dffae67d5300726b12deff545c2ce1fe41395527ab8a3b6f207/pywavelets-1.10.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f687e77f8b6affbd760e56322624c6c26023e33cb2aca14ac6ee7659c3798c6", size = 4462173, upload-time = "2026-09-07T17:14:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b5/f1c9fe74b0f2fe4db78b71c97fae9fbc390f00f0fecbb1427bab461c8fc3/pywavelets-1.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c3518a446fd281b194d8e00a4ad3cb17c8feb42b22faa7c5d00292101943d3ef", size = 4417623, upload-time = "2026-09-07T17:14:19.129Z" }, + { url = "https://files.pythonhosted.org/packages/83/91/f2cff049877be4d2d62f309789f806c619d9d4491a444849ebe5c0d8133d/pywavelets-1.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5dffa97177d58833f2a7879dba572ae983ddea5fdbb1abfd9e65af63ec29494a", size = 4465246, upload-time = "2026-09-07T17:14:21.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/fb/7501c14dbf52d3e7a3ca0d520304a418cffefbb0efd4fbfa1a0f88e91b0b/pywavelets-1.10.0-cp314-cp314-win32.whl", hash = "sha256:386732f819a56f67e0d1379062c090814e4ae271b7ef7956d40a1fc31e903b54", size = 4188798, upload-time = "2026-09-07T17:14:22.685Z" }, + { url = "https://files.pythonhosted.org/packages/bd/66/be5f2ff3db2646637dbd698600d58cbd7af3597f342a7344e1895a4b6fed/pywavelets-1.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2567834ca971b1655eb3220b1343d71ff3318f9405ab5d83ee621b2342192d", size = 4292200, upload-time = "2026-09-07T17:14:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d6/8abeae303f34fc93fd7aa1fdf724a9bb96d0d925932dba73145a0b145645/pywavelets-1.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:b30e98e23b4c674237c94196a8d926da7008e3649a1e82a1e7cc6c29e5b112a1", size = 4190974, upload-time = "2026-09-07T17:14:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/4a/95/9c95f032a71b6e06288d3dd510256c2c6ac38fa25ed10719704d85f5d1e5/pywavelets-1.10.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f307a4b5701c2b944d455be9baa795086815a33630bba3e058c7f2e382d37d6c", size = 4358720, upload-time = "2026-09-07T17:14:27.452Z" }, + { url = "https://files.pythonhosted.org/packages/eb/65/348be445712b12e99014ba515c57db8b429419eae67a79241428bee49fd8/pywavelets-1.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ff8395c0c17132653bb24a5a6aec421534a2ec2fd9f065cb96d8c1152418784", size = 4332900, upload-time = "2026-09-07T17:14:29.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/82/917ab7e7d533ca2ee7e0221c7cbcda8ae16e9be4c0026127339d84b1d595/pywavelets-1.10.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2bae0632854f3558a6485662b720f5218084ee0fab7513f8039ecdffd48ce1e", size = 4398294, upload-time = "2026-09-07T17:14:30.67Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/babf7c5860d163af1d81ec7e7be454be296468ab3be1090e165d984cb7e8/pywavelets-1.10.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fc1278f262e001a201f0c6af511bbb62cbf9bc385c2fd9071682ef6b3623233", size = 4448434, upload-time = "2026-09-07T17:14:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/70be0cd8920651607f1f83e3e6f1d2e70ba8b291138235b7c76fbf4848f3/pywavelets-1.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7d31f4684420d310820fb30fef2766e0313df6fdb2d95446304f3faec4b3f544", size = 4407624, upload-time = "2026-09-07T17:14:33.957Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/04256964256b5b6bb03fbee6f289304741ea25bfe7b340f4737fc0e40846/pywavelets-1.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0b267f0a786b3da4efa7f8caf75c8b4e44326bf322b5cf624a555523dc26d1fe", size = 4454192, upload-time = "2026-09-07T17:14:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a4/1380522b1188892c9e8b4f00978e7dc3e63910da53c8594294ed25b59ed4/pywavelets-1.10.0-cp314-cp314t-win32.whl", hash = "sha256:29b741cba0bc3763a3c6cd4a03da08760037284d9d510e87d088e282e8980b36", size = 4222826, upload-time = "2026-09-07T17:14:37.171Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d9/7c1ce2bc5d193e4e7fa64db0c6573a91983d91de43a8b1167df5b5680f05/pywavelets-1.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:97acc85f97095fec421021ec01e188f9c6b362eb89572ace6b462929d7b391e2", size = 4327881, upload-time = "2026-09-07T17:14:38.747Z" }, + { url = "https://files.pythonhosted.org/packages/88/61/0375be8264b1b86b3182459bec94ac0d6ffba08c9b6a090840d4ba38d05b/pywavelets-1.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeb5073bb1859f7a911b275ab0fef1e41acede52fb1661331d74cb7f45ecbb4c", size = 4217922, upload-time = "2026-09-07T17:14:40.596Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -3210,106 +3361,90 @@ wheels = [ [[package]] name = "regex" -version = "2026.7.19" +version = "2026.9.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" }, - { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" }, - { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" }, - { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" }, - { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" }, - { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" }, - { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" }, - { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" }, - { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, - { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, - { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, - { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, - { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, - { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, - { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, - { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, - { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, - { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, - { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, - { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, - { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, - { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, - { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, - { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, - { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, - { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, - { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, - { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, - { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, - { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, - { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, - { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, - { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, - { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, - { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, - { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, - { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, - { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, - { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, - { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, - { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, - { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, - { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, - { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, - { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, - { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, - { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, - { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, - { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, - { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, - { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, - { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/db/fc/b4ff5d9550796f9f955b6c2470f37e2b582ec944638f3004499929a69c96/regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65", size = 493967, upload-time = "2026-09-01T00:50:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/ab60a24883b810a4b29065540f56e6f72590bb97d704d7e0ee7bcd2ba98c/regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4", size = 295227, upload-time = "2026-09-01T00:50:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/e3bf61522cd82eda85a90dd8f69c839f0ce1da64dc6e87f803bb79267c59/regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e", size = 290639, upload-time = "2026-09-01T00:50:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/e8492b054c5c9fe09bc0aefdc308b8f845ec2094b1ae779accec8f14dedb/regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328", size = 791712, upload-time = "2026-09-01T00:50:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/429ee3479bb438f7b2b41147fa68d63a7ef9c7486fcd3960e04e61133cf3/regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71", size = 861711, upload-time = "2026-09-01T00:50:06.988Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/6ebf3cec538ed49d0a4cf5729129fe39580ddfa80c1c59c4274651762202/regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e", size = 905894, upload-time = "2026-09-01T00:50:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/a7bb1e4038a170714c824dc7c739f8b911cb1172f87ab950be39e0691f95/regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01", size = 801135, upload-time = "2026-09-01T00:50:10.308Z" }, + { url = "https://files.pythonhosted.org/packages/bd/47/1b318fb249d5fa3dcb0375ad284d34155f0da4a2fe92c8f01edef0cb0258/regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141", size = 773578, upload-time = "2026-09-01T00:50:12.125Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/63f2644a0a3e92623f83ccadd04873916f5b650e2028bc44ee9df104a839/regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc", size = 783915, upload-time = "2026-09-01T00:50:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/f078963d1dbd3632f6e25f7d972cc9a9332649ed239995bfeb403cf88a3b/regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b", size = 854924, upload-time = "2026-09-01T00:50:15.826Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/457ac94a2a2aaa2ab5c076324dfd7e38e76f36d4d78923c4ddb8c3d93d75/regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124", size = 763172, upload-time = "2026-09-01T00:50:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/db145a6ad935cdc6c694381543d53e6e7d7212287d4f8885f965e91ea4a4/regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f", size = 844983, upload-time = "2026-09-01T00:50:19.572Z" }, + { url = "https://files.pythonhosted.org/packages/76/00/100346c5cc2384a7ce0adfb7dbb11f4a865c76e3a920fe68542042da66ea/regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce", size = 789532, upload-time = "2026-09-01T00:50:21.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/d878c1d3eed2768f19e8b6fa4aa81b0a0e6bc23fa34e64173ef48816844a/regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420", size = 266766, upload-time = "2026-09-01T00:50:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/da/6a0ac27bacb39aca3452cdd7d1bfc361608ad000cea3e1b0d7dbaa1e2b15/regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc", size = 278007, upload-time = "2026-09-01T00:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a2/b23997814f48f823bcda615764b4fb00d72a5a06bdbb41cafa895ec62428/regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8", size = 276960, upload-time = "2026-09-01T00:50:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, + { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, + { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, + { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, + { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, + { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, + { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a0/322f050f77289a1acf99b5c400fe201780bdcfa8f8876fbe250ae3de37af/regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886", size = 496701, upload-time = "2026-09-01T00:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/91/30/85df182aec58aaa34b02a3f0a99068bf34a7c9b3f9bd7ff06d58aa866e1d/regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6", size = 297055, upload-time = "2026-09-01T00:51:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/64b8bbd0316baaede047dbdbf2bcc96f10a4eeddec2e9cbf289e855e636f/regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7", size = 292008, upload-time = "2026-09-01T00:51:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/224614ef16bc8336269a84b578fb0dbab1433b584aaefc5f0680cacf9227/regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944", size = 796341, upload-time = "2026-09-01T00:51:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e1/10ad988d3b673db86353666c56cb32e1b6246ca1ab0c81018a58ebec889c/regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b", size = 866311, upload-time = "2026-09-01T00:51:31.592Z" }, + { url = "https://files.pythonhosted.org/packages/08/16/8d597f97bb6215876512cd7b3c47179c0eb79d1820ab2ed8412d3ba5599a/regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8", size = 911869, upload-time = "2026-09-01T00:51:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/f6bcb26472873b9308df329f507d9c6cb526e3f9aed847498f153f2b7539/regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec", size = 801375, upload-time = "2026-09-01T00:51:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/38c7cd736bfd58b190412dbf5a81aae1c38456c538d5c4987f5980cbfa3d/regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69", size = 776522, upload-time = "2026-09-01T00:51:37.81Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/8ecefa1c9b57b29fcbe963797435e69a3ba6a9773d802b3d4fca4c75ecf6/regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7", size = 785727, upload-time = "2026-09-01T00:51:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4c/af402c5aa95615ab66620d775de07ea09b0fea206cd79e3c8a5716d5fc96/regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c", size = 861375, upload-time = "2026-09-01T00:51:41.829Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7b/b83dd26470fa4cc6d7beee9b3c52ff0e988ba636c1367a04d4b2829d8d95/regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc", size = 766221, upload-time = "2026-09-01T00:51:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/94/5b/3b91b19d8fb11a353738cec56b19440bf181f3f9a0298d91140fb4061778/regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69", size = 851737, upload-time = "2026-09-01T00:51:46.708Z" }, + { url = "https://files.pythonhosted.org/packages/2e/be/ea0762fd8a12fa711f1a626283f6d79a3d8a81df4eaada4fbe82009e46f6/regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380", size = 789455, upload-time = "2026-09-01T00:51:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dd/b9af9d65de78831fb76cb8ef420db4094295b09f93f8abd17c867261c4fb/regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd", size = 272528, upload-time = "2026-09-01T00:51:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fe/ecb15616ae7aa4892299b9ca7c20ef0dd6e5c833643b7ed46e27ff5fcccd/regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846", size = 280812, upload-time = "2026-09-01T00:51:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/18/4f/60efc748152c82b967bd186c356dd1fc21c4f6c225b8026b52547ae60c9f/regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16", size = 281095, upload-time = "2026-09-01T00:51:55.052Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cc/aa269583e8986be9604ef5fb9e2164f33108f6220c896efff2dfb0d4e4c4/regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16", size = 501014, upload-time = "2026-09-01T00:51:56.902Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/bb8a3f3a47beccbf92c3bcda67c2c0336c13c2e10f9744306275f428ebc9/regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d", size = 299376, upload-time = "2026-09-01T00:51:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/c646df2b62c1b1334e28581715ed776814c8ac80c0c9106175f0c61ce12a/regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595", size = 294424, upload-time = "2026-09-01T00:52:01.113Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/3dd8c49cc4af440a9a1a0ebb2b4a939398398c81fb7fecbc3c7d534924a1/regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17", size = 811421, upload-time = "2026-09-01T00:52:03.289Z" }, + { url = "https://files.pythonhosted.org/packages/71/32/c35f031aec040b9136189e3f403681e3168f1f9862e31d9df92ea599f2c1/regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970", size = 870110, upload-time = "2026-09-01T00:52:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7b/931d962f9fbca3326f282a01fa5bed4d6734dab5215d1c20bac0ab85df58/regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3", size = 917489, upload-time = "2026-09-01T00:52:07.703Z" }, + { url = "https://files.pythonhosted.org/packages/46/1a/da0c28f2b1e65d46a24abc1dd33e0dc9ddbcfeeee7c5625103b3e09e548c/regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5", size = 817470, upload-time = "2026-09-01T00:52:09.686Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/6a540ec40c3b8eeefec445362cf08bbb686b22d4088f97d3c1ea38323434/regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181", size = 784742, upload-time = "2026-09-01T00:52:11.778Z" }, + { url = "https://files.pythonhosted.org/packages/fc/88/75500ca27ba85256eb46119ebd99ec10ead4c3ffacd47fe7dfe034662960/regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef", size = 800609, upload-time = "2026-09-01T00:52:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/18541aaad8b88977e2c3fbbb53c1a75f6f6c999800a82d2f7306581cb6b6/regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae", size = 864798, upload-time = "2026-09-01T00:52:16.137Z" }, + { url = "https://files.pythonhosted.org/packages/16/62/ffbfbeb5a3f2cca701afb9f8aebbfcbf37a4aae2f1584c96680142aab445/regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88", size = 772892, upload-time = "2026-09-01T00:52:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/04e2db28f490b1fcc684428bd2de21e1ec46f7d8d421b7f2cc74d120affd/regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177", size = 858065, upload-time = "2026-09-01T00:52:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/c0cac930d1dc0b86c5d8ff10602f15df4fb15fa3f33e04a98be197f33618/regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e", size = 804739, upload-time = "2026-09-01T00:52:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/69e6f4d02e11a5cb45033abede6e04b0da3716b1b6c311d4609be14a01a7/regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f", size = 274514, upload-time = "2026-09-01T00:52:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/28/64/064b78028b52c0c21b9c0b362b1bdcee7179f009891ddf959d9b6390b30c/regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16", size = 283797, upload-time = "2026-09-01T00:52:27.358Z" }, + { url = "https://files.pythonhosted.org/packages/f2/46/fbb41eb6cc92403ba9f896dfd56a1d5d31549c4033dfb7418da7ecd5802a/regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5", size = 283444, upload-time = "2026-09-01T00:52:29.632Z" }, ] [[package]] @@ -3334,9 +3469,11 @@ all = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "onnxruntime" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "pillow-heif" }, - { name = "pywavelets" }, + { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "pywavelets", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "safetensors" }, { name = "sentencepiece" }, { name = "tokenizers" }, @@ -3349,7 +3486,8 @@ classify = [ { name = "huggingface-hub" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "tokenizers" }, { name = "torch" }, { name = "transformers" }, @@ -3357,26 +3495,30 @@ classify = [ detect = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, - { name = "pywavelets" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "pywavelets", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] dev = [ { name = "audioseal" }, { name = "av" }, - { name = "huggingface-hub" }, { name = "httpx" }, + { name = "huggingface-hub" }, { name = "invisible-watermark" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "openai" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "packaging" }, { name = "playwright" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, - { name = "pywavelets" }, + { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "pywavelets", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "ruff" }, ] diffusion = [ @@ -3384,7 +3526,8 @@ diffusion = [ { name = "diffusers" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "torch" }, @@ -3398,19 +3541,22 @@ lama = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "onnxruntime" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] migan = [ { name = "huggingface-hub" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "onnxruntime" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] pixels = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] qwen-zimage = [ { name = "accelerate" }, @@ -3418,7 +3564,8 @@ qwen-zimage = [ { name = "diffusers" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "safetensors" }, { name = "sentencepiece" }, { name = "tokenizers" }, @@ -3439,7 +3586,8 @@ text-restoration = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "onnxruntime" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "safetensors" }, { name = "sentencepiece" }, { name = "tokenizers" }, @@ -3454,12 +3602,14 @@ video = [ { name = "av" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] visible = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "opencv-python-headless", version = "4.14.0.94", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] [package.metadata] @@ -3562,27 +3712,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.5" +version = "0.16.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, - { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, - { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, - { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, - { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, - { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, - { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] @@ -3858,7 +4008,7 @@ wheels = [ [[package]] name = "torch" -version = "2.13.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, @@ -3877,26 +4027,26 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, - { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, - { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, - { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, - { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, - { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, - { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, - { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, - { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, - { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2c/0c9757a1ea1fff8ccc4faff1d1c87e98a69542b5d4b606e50f7518f72e58/torch-2.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ae530ddd3f3b94248b77f1fd3313c4f3405bd0d17283d505b81574816767133a", size = 127277244, upload-time = "2026-09-02T13:42:57.855Z" }, + { url = "https://files.pythonhosted.org/packages/ed/30/eb2f6cf77cb8f2d5e7ce4343882a856783521f51b557673d78d1d7bb8661/torch-2.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4dbea8a10d80b10dff2be1ed467d2617b704fe6bf76bbfad4194774c8f6d886b", size = 453993896, upload-time = "2026-09-02T13:43:29.231Z" }, + { url = "https://files.pythonhosted.org/packages/8d/be/c0c8a845bf00552960d2f1f6a00b6ac9750fd312f55429c3e8c6c0e3aac4/torch-2.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d9e232b6376c62f3090237889fb1cb6887c0ece9f73e6c1052e80fc1481f999", size = 554583496, upload-time = "2026-09-02T13:44:10.199Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/ed9d177f6a133389d5fa89e298f3e2a879b62952aa928ac6ddd04467f225/torch-2.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:6981872df75eb5409c439050d36b67cb57d88b4e11080516e2aa7410f6730705", size = 124096356, upload-time = "2026-09-02T13:43:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/17/76/bb4770f56cf6d8971671dbcbb7493e5a6a15ad2825f4e359b02c27c38297/torch-2.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c1f844f1c750e87df4b68bc3afbc0e2b0c7ef19d7b8f666e48bdcf6a0c4f0056", size = 127303200, upload-time = "2026-09-02T13:43:20.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/ec44bacf2c8886b85ba4ca2285e8b09f2dff5d9c99e6a031326954082cb5/torch-2.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ada340e62591d06a2bcc2d68170f20f45f0b0665d372dc510a8ed7eb3b1d609a", size = 454010251, upload-time = "2026-09-02T13:44:24.927Z" }, + { url = "https://files.pythonhosted.org/packages/15/71/49399acd41f750a906c686bd23c08a2001ccb8dd25f2971003c2ed89c1dd/torch-2.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fecffb58f51fd643d213acd68da21cc3fc19bea05a3bc64b4ee55128f47a4963", size = 554620488, upload-time = "2026-09-02T13:44:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9489b137112040f9911d7527e452854f21cc4e499ce0da79864e6a7451a7/torch-2.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:cad84f41bbdf3dcf333ce394aeeaf25237c4d94fd6b659ba5eb813c829978823", size = 124114011, upload-time = "2026-09-02T13:43:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/0db773452c2a62b37761d3f418acf933d381f9e87077036fb57c2a386c37/torch-2.14.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9d4b1022a5d9b71282ec67ad0d9e7235870096b8a246dc1c32d6ea1fc83dc998", size = 127311393, upload-time = "2026-09-02T13:43:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/13/36/537fd9da2adad49e7b2bb20741398625bee548493158274e87369a8eed56/torch-2.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:731b9ebdea402b8b1996d4c2ae613b16660e559b19e47bdc45d970568bc91c53", size = 454010525, upload-time = "2026-09-02T13:45:03.719Z" }, + { url = "https://files.pythonhosted.org/packages/21/f1/39bd13b21f57d1982b7f3ddf663f01c7266e2957714880744eba9e8c8d11/torch-2.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84bf384779a10c02fc3c6bdbab71a9cb66b0dd93c652d1ed5d6dfc0cb37e5962", size = 554619993, upload-time = "2026-09-02T13:45:28.209Z" }, + { url = "https://files.pythonhosted.org/packages/89/a8/683d9c44737554b67ca76dd2db4f42258a0f014246cb511293e51e0154bd/torch-2.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0e7cf18cb0d8bd666b6120932e29c7aef3502b61a08b44da4839580c539a7cdb", size = 124113865, upload-time = "2026-09-02T13:44:33.705Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/1241e7db5ccc2455f8735bd6b1becfad39916206ad18001c4c0014d139e2/torch-2.14.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:860423e970f2ce02c4476e8e2d1350131b1c5c5a5e4912180e78b50b53241efa", size = 127321431, upload-time = "2026-09-02T13:44:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/bbeba65e3fbb4b8f61ea19cf4ebea9f4268cc6fe64e6f7d38bfb6cc152eb/torch-2.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b985c7defeb8d28691b7513ebaecc64946c5f68ec770e9f4ddba39688864f46a", size = 454027631, upload-time = "2026-09-02T13:45:43.73Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/8d2b9a7e724759470c209489b79260cac537f091cc9aac8001c8d2bc845c/torch-2.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b2cd92bce63d40bf6fc2e5d840fc2f0063bd180a242daa761cb6088cc2f46e27", size = 554623549, upload-time = "2026-09-02T13:46:04.252Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/a5475c00b0555e686333e6b4036f2213e7cbea021a772ce6f9ced4dcbd2f/torch-2.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:44b044b9f6f633d982839422a57433d6a1da520037fd88e0c8a47efde589b3b8", size = 124110863, upload-time = "2026-09-02T13:45:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/bbaee76337742a42d2c5b0296ea62252567050e1d821bc2283b2e72ba6fb/torch-2.14.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:553aec938d37d77b783bcf801e638cad068870e7643c47eadac21eb180f551ed", size = 127653463, upload-time = "2026-09-02T13:45:17.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/6cc7a511bab384fbe8f4b8ddeecdd22e724b2162d6f15076f07a0a7ef15b/torch-2.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cd8cd8f714d511ccdca907282d1da3d9be8322d4e1520b9c3bce39a5c1318a4b", size = 454009927, upload-time = "2026-09-02T13:46:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/b6/da/0f04fe15fd05bf3f613a359b14acb44c35ae06fe69da0aefa8b5cdb4f9f9/torch-2.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d2526f71e6638133b97b3cd2881ece3df521460a4727030e8ae2a72a7d3ae31d", size = 554580530, upload-time = "2026-09-02T13:46:34.818Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c3/72ae1f02747b1f012e1975743e48cd608f83095d7f9ce58de78b79248b35/torch-2.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:731784e3914843c6bcc7aba3987ff7610ac57dbbc816a5d6b9b62e04c240a641", size = 124400194, upload-time = "2026-09-02T13:45:53.555Z" }, ] [[package]] @@ -3916,7 +4066,7 @@ wheels = [ [[package]] name = "torchvision" -version = "0.28.0" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, @@ -3925,26 +4075,26 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/b2/1e010052079e4c577007b789db336ea7075f1a426e84d17121fbc3745516/torchvision-0.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8", size = 1856017, upload-time = "2026-07-08T16:07:55.533Z" }, - { url = "https://files.pythonhosted.org/packages/27/be/1b9c5de9c655ca2df4a74100fa671a7b848532ff787e077ccde14a7dea2a/torchvision-0.28.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c", size = 7841822, upload-time = "2026-07-08T16:07:49.207Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9b/f1e68e861d4462e3e195a642c2b448e7b7d3fad5f209487162b9a2133d9b/torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002", size = 7670718, upload-time = "2026-07-08T16:07:46.525Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/1494610ff54cbb154beb55033cc2cd50f3de04dac132fa2dd00e4f2b2556/torchvision-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37", size = 3814319, upload-time = "2026-07-08T16:07:37.153Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" }, - { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" }, - { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" }, - { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" }, - { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" }, - { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" }, - { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/6b32740f7ae0ecf8772297f7e61e842b127b11de5c2c07efd9aeeaed0690/torchvision-0.29.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bbe15455b59a6c9d822584fd3faaaac50ee972101d7bb7e2887da456e148a3ea", size = 1813735, upload-time = "2026-09-02T13:46:58.108Z" }, + { url = "https://files.pythonhosted.org/packages/59/0a/2c5114537cbf4ac374607a41d976911657db9f656bfa42a278470ca8d886/torchvision-0.29.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5717feef9a0b430b052895db71f1d695939575b74344da981120e9f4da7620", size = 7585959, upload-time = "2026-09-02T13:46:59.536Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2d/a656bfa09b98d01d4baf30114a5befca1de2f7a16133d94aae01d7cd6942/torchvision-0.29.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:85fa54bec1f7d9227b5e4e201ed4bac92247ee10fb19148d92f9836e3e91d5c3", size = 7430717, upload-time = "2026-09-02T13:47:01.281Z" }, + { url = "https://files.pythonhosted.org/packages/78/26/dc21cec3eace48d944258a437e293093c69ddb56684e9dd1b5016f7e2154/torchvision-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:a292ca7044236a2702ec3d4664de135e01fc9fb3ae9422db3fecc819546e831c", size = 1376792, upload-time = "2026-09-02T13:47:02.645Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d2/9daad500db2ab880eca0f0a569afd73e1afdd49f22c94560c85546dd87c2/torchvision-0.29.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:994687b818cac0e6d34cb407a41458e2c3262cb7db927465841c9522ba7ebb92", size = 1813740, upload-time = "2026-09-02T13:47:03.933Z" }, + { url = "https://files.pythonhosted.org/packages/41/14/702cd035fab1b8dfe944d06827cc4628c3b18e5d7ddaa676bf72c9be1c2c/torchvision-0.29.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b7a736eaa6b2e22476c95ceb62986d195f3e600cdd7d196d7329aa2dfc951994", size = 7586112, upload-time = "2026-09-02T13:47:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/3cf1992414fd712af4865ae5f8f5758d9003c1ed82624908d54f7afb4342/torchvision-0.29.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0e631a2f8b24732672d35224d2574ff89a78d56bef5364ef6094a625fa5f1771", size = 7430773, upload-time = "2026-09-02T13:47:07.178Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a8/59152b945c09840f582c40e38aa4128777625af7f2883ce2d77133f96a46/torchvision-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3b99c62f7f095153887330c2c60c9ebc2da84dfbe08eea564a41ec361629cf", size = 1376791, upload-time = "2026-09-02T13:47:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/81/56/4e122b6b59269cb2e6a8f7ef969cff7b1eca1499c48784805ad156d3720b/torchvision-0.29.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:28a3964a9e6db34354d4ca440d2c2038deeef24e7219e8d6f49d9d174f44830c", size = 1813739, upload-time = "2026-09-02T13:47:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/d7/da/1418b42ed642521262d2b276ffb698772f06827202dca9065a66824f69bf/torchvision-0.29.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:83db0299170502b038640444214c99d7b54bad72db447f0554deb8d2c4f02f94", size = 7523336, upload-time = "2026-09-02T13:47:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/34/78/afa8f85e1f3cb276202ad3af9e36765feee1f0c5ec2942d8522b321e5afc/torchvision-0.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:942a3b3fb4e981e1abb7846679ee3882dfc733533a30b68df0dadf6c06f238a9", size = 7430683, upload-time = "2026-09-02T13:47:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1e/72fa361d60e46b4f003ebdc68508467f46d4ef1011650a80258b40d7cf46/torchvision-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:ebf54f6744556ebd8d7b5f9b6515e2060e5d3156723970dc257bf093f63b2170", size = 1376791, upload-time = "2026-09-02T13:47:12.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6a/ccdf22f6ee57e862aed41ab70e735f6302b8330bf304d8125f60d84413ba/torchvision-0.29.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:01a029fa1b2eacac27e1e4f9e0dba24d112bcbd523261b294e4b4a1c7f8330bc", size = 1813734, upload-time = "2026-09-02T13:47:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/3cffb013454af4c126a40e97cff7ac91dff692b749bcf80754ae97ada225/torchvision-0.29.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f9e9627d5036cac8e6de76aeb2f1acc67841ff5ff4b63c4cf78ff954efeb073e", size = 7523354, upload-time = "2026-09-02T13:47:13.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/ae1d76e76c5d97b70539cd38df309777d66020d33b346301e79a7a8c132a/torchvision-0.29.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e8fdf234d76dca6fc47bc5f4af86ecf1ef57fab6285a7b21681bdcbb494e4c1b", size = 7430722, upload-time = "2026-09-02T13:47:21.359Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f5571d0c2aedc3d363f9b818de8fbb810a0d2b2ab0293de6003f4291ce15/torchvision-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:bb74d08d65785b51c61d4d4ec167bfa50a5ea644b57555a33ba2f4435b0d05dd", size = 1376792, upload-time = "2026-09-02T13:47:18.279Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/bc564a8ed409db4d7ce72215a676c3cbda9559779a53e8ecc477ad11d2c5/torchvision-0.29.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:80789f9b50f5277302e7020c8128344e0269b12f6e3b5855034e56bb7081b874", size = 1813737, upload-time = "2026-09-02T13:47:22.801Z" }, + { url = "https://files.pythonhosted.org/packages/80/55/b39dba8e3d574428e7f00f212a3ee5a2c2d0ab5a25a95338a7c724a87850/torchvision-0.29.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9b99a7385da8d706f2dc14cacffb8cbf653d48fc37013a38c41a36659acc3731", size = 7523376, upload-time = "2026-09-02T13:47:19.631Z" }, + { url = "https://files.pythonhosted.org/packages/84/b3/67692c3785702c24d6c9c95026a558791366f2dc48823aec2321f23a0858/torchvision-0.29.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:2428a13706a354ee3901fd63e3f1ef8c2a59f6e20d24cce13210b4e61be08f7a", size = 7430679, upload-time = "2026-09-02T13:47:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/8e/0e3682cad7f6b2aba93585bc3b792f8087e4e3fb916c0f6638eacdb3170c/torchvision-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd12e152640d1024fca15deaf6f39c53702e12eb398db4b2b038a5a97ab85f7f", size = 1376794, upload-time = "2026-09-02T13:47:25.923Z" }, ] [[package]] @@ -3982,24 +4132,24 @@ wheels = [ [[package]] name = "triton" -version = "3.7.1" +version = "3.8.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, - { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, - { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, + { url = "https://files.pythonhosted.org/packages/a3/cf/d21c9b1a4d1df9ba3aed218069f24f762a4b9565c5a362bef6f110c08e72/triton-3.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:372285307d4c44ee74cee32de0b4f04bd157e071427e38c6f4ee3e3beb2194f4", size = 226467015, upload-time = "2026-08-28T16:08:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/2c3d3823726d5ad5359f7065b02b984925158b74611c1e3987f6a37461fb/triton-3.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68988ac85d5e7086baeda0ddc175af9667db7529b3c5e11a5c0601b8bef2200a", size = 247945226, upload-time = "2026-08-28T15:55:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/07/0f8cd8e8db0472334253efdaaab3d0819fea27aa99bf0e7f1aeea4ceb5ae/triton-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9c404c69ed4a39e8ec632eaf6b9fe058a060bf98979c177f6ef666f06bb8d50", size = 226474486, upload-time = "2026-08-28T16:08:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/b7012e5bfae67640f268aa584caa80fe1674f6b0da949046b679972c33e3/triton-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91ffa46d095b252248297292dd22bcbacd53a125a0c2eefbbbf74925a320bc3", size = 247972921, upload-time = "2026-08-28T15:55:53.157Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/4c564374bcdadb166fccbf3e45aee0d4a473f88d341761bd2fefe3b8e8c1/triton-3.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7004666652f500ed854a86988e4b3d69d247188b5d2092b5df1e44f4a954099", size = 226476793, upload-time = "2026-08-28T16:08:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/3394d5548404c1cabd1dadadd28d0b3f9478db1dff8180da53bb3f0a1e19/triton-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0497218e26b7d79773ad9c2a3fa3b539ee69f587a13fac2e552b1d322a8015", size = 247975122, upload-time = "2026-08-28T15:56:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b8/59/bf0e9493118bb353ab59a5d6a65db3618d9b314417cc1459f0121e0ec5c9/triton-3.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f6b48d0591929a3867973acac3dccd4e058585f91bfb41022de496c9ffab304", size = 226488654, upload-time = "2026-08-28T16:08:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/08c75f3459f19ad00425b564058e40efa4bcd79b816064cf27499303ea42/triton-3.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387dae4cb0089a7b6ba1a428ae0782b65c4c58f57d94617cb22ca8593d8ccbca", size = 247972313, upload-time = "2026-08-28T15:56:14.007Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/429c5592181cfb7361a0a8e0bff218e7224b726709d75da2472b3e819f70/triton-3.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b84e7d512490ba529111260fa6f7cad8b254a6bb5fbdf41d5ef9a5e57f52d0a", size = 226591133, upload-time = "2026-08-28T16:09:02.271Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/aa8a3e935c37efee7945984fdb64d7e0851bf6d920afd97b2d21f9d23360/triton-3.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74217bb56ed8692759227758e4c4b3bd2d608a209c1a7a081bf361fb4c2c1bf9", size = 248077577, upload-time = "2026-08-28T15:56:24.94Z" }, ] [[package]] name = "trustmark" -version = "0.9.1" +version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "einops", marker = "python_full_version < '3.13'" }, @@ -4010,7 +4160,7 @@ dependencies = [ { name = "torch", marker = "python_full_version < '3.13'" }, { name = "torchvision", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/0a/0a4232030c6a62d12b6a02ae73bdce6e99c8532bc8f05a5a2e6ce103da82/trustmark-0.9.1.tar.gz", hash = "sha256:dc79e3fb070f5d94765acf8868a51f50a612cc05b53223cf1e6b605d4ff1e0ae", size = 63949, upload-time = "2026-04-09T08:59:52.472Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/41/32e84596af94e8696c55060d55eb9eed9d99722a673d12212a1c0c4ef1d2/trustmark-0.9.2.tar.gz", hash = "sha256:bcaa3bc38ead1c0558df81932672f34e306efaed31389da57dbed100e236b28a", size = 63780, upload-time = "2026-09-08T10:58:11.601Z" } [[package]] name = "truststore" @@ -4023,7 +4173,7 @@ wheels = [ [[package]] name = "typer" -version = "0.27.1" +version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -4031,9 +4181,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, ] [[package]] @@ -4164,23 +4314,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.8.2" +version = "0.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, -] - -[[package]] -name = "wheel" -version = "0.48.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" }, ] [[package]]