Compare commits

...
Author SHA1 Message Date
Victor Kuznetsov 804735aa48 Release 0.41.1 2026-09-16 10:42:15 -07:00
Victor Kuznetsov e3f714dcd2 Merge pull request #109 from wiltodelta/dependabot/uv/minor-and-patch-138d877835
Bump the minor-and-patch group with 6 updates
2026-09-16 09:25:05 -07:00
dependabot[bot] 7169780dd0 Bump the minor-and-patch group with 6 updates
Bumps the minor-and-patch group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [accelerate](https://github.com/huggingface/accelerate) | `1.14.0` | `1.15.0` |
| [huggingface-hub](https://github.com/huggingface/huggingface_hub) | `1.30.0` | `1.31.0` |
| [onnxruntime](https://github.com/microsoft/onnxruntime) | `1.29.0` | `1.30.0` |
| [openai](https://github.com/openai/openai-python) | `3.9.0` | `3.13.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.16.6` | `0.16.7` |
| [pyright](https://github.com/RobertCraigie/pyright-python) | `1.1.411` | `1.1.414` |


Updates `accelerate` from 1.14.0 to 1.15.0
- [Release notes](https://github.com/huggingface/accelerate/releases)
- [Commits](https://github.com/huggingface/accelerate/compare/v1.14.0...v1.15.0)

Updates `huggingface-hub` from 1.30.0 to 1.31.0
- [Release notes](https://github.com/huggingface/huggingface_hub/releases)
- [Commits](https://github.com/huggingface/huggingface_hub/compare/v1.30.0...v1.31.0)

Updates `onnxruntime` from 1.29.0 to 1.30.0
- [Release notes](https://github.com/microsoft/onnxruntime/releases)
- [Changelog](https://github.com/microsoft/onnxruntime/blob/main/docs/ReleaseNotesWorkflow.md)
- [Commits](https://github.com/microsoft/onnxruntime/compare/v1.29.0...v1.30.0)

Updates `openai` from 3.9.0 to 3.13.0
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v3.9.0...v3.13.0)

Updates `ruff` from 0.16.6 to 0.16.7
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.6...0.16.7)

Updates `pyright` from 1.1.411 to 1.1.414
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.411...v1.1.414)

---
updated-dependencies:
- dependency-name: accelerate
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: huggingface-hub
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: onnxruntime
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: openai
  dependency-version: 3.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: ruff
  dependency-version: 0.16.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: pyright
  dependency-version: 1.1.414
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-16 08:49:01 +00:00
Victor Kuznetsov c7021b900b Add source pipeline classifier runtime 2026-09-15 23:17:48 -07:00
Victor Kuznetsov 3f55df6f99 Correct source classifier activation schema 2026-09-15 22:55:46 -07:00
Victor Kuznetsov 4795149d34 Publish source classifier model card 2026-09-15 22:36:34 -07:00
30 changed files with 1080 additions and 217 deletions

No files matched your search

+4
View File
@@ -129,6 +129,10 @@ Do not classify an entire module as untestable because its main path downloads a
- the photo-classify gate in `test_classify.py`: DEFINITELY is ridge AND MLP,
POSSIBLY does not emit a provider, and `identify` does not import
`remove_ai_watermarks.classify`.
- the source-export classifier in `test_source_classify.py`: the Hub revision
and artifact hash are immutable, the pickle-free schema and exact GELU are
validated, threshold mutation changes the real decision, and `identify`
never consumes the result as watermark evidence.
- the `InvisibleOptions` defaults, in `test_api.py`. When one signature promises to
mirror another, compare them field by field rather than pinning the values you happen
to know about, so the next field added on one side and not the other fails at the
@@ -0,0 +1,64 @@
name: Publish source classifier to Hugging Face
# Manual only. The derivative model weights live in a GitHub model-freeze
# release; the private training corpus and caches never enter this repository.
on:
workflow_dispatch:
inputs:
confirm:
description: "Type publish"
required: true
weights_tag:
description: "GitHub Release tag holding source-pipeline-mlp.npz"
required: true
default: source-classify-freeze-2026-09-16-r2
jobs:
publish:
if: ${{ github.event.inputs.confirm == 'publish' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- name: Download derivative model weights
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.inputs.weights_tag }}
run: |
set -euo pipefail
mkdir -p weights
gh release download "$TAG" \
--repo "${{ github.repository }}" \
--pattern source-pipeline-mlp.npz \
--dir weights
test -s weights/source-pipeline-mlp.npz
- name: Verify the frozen artifact
run: |
set -euo pipefail
expected="$(jq -r .artifact_sha256 docs/source-classify-hf/metrics.json)"
test "${#expected}" -eq 64
echo "$expected weights/source-pipeline-mlp.npz" | sha256sum --check
- name: Publish to Hugging Face
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [ -z "${HF_TOKEN:-}" ]; then
echo "HF_TOKEN secret is not set" >&2
exit 1
fi
uv run --no-project --with huggingface-hub \
python scripts/publish_source_classify_hf.py \
--src weights \
--message "Source-classify freeze (GitHub ${SHA:0:7})"
+1 -1
View File
@@ -43,7 +43,7 @@ The published Agent Skill is [`skills/remove-ai-watermarks/`](skills/remove-ai-w
[`docs/module-internals.md`](docs/module-internals.md) is the canonical per-module map, including design decisions, thresholds, calibration history, incident records, and regression guards. Read the relevant section before changing a subsystem.
Research and current constraints are routed through [`docs/index.md`](docs/index.md), especially [`docs/known-limitations.md`](docs/known-limitations.md), [`docs/supported-signals.md`](docs/supported-signals.md), [`docs/synthid.md`](docs/synthid.md), and [`docs/watermarking-landscape.md`](docs/watermarking-landscape.md). Pixel photo classification is [`docs/photo-classify.md`](docs/photo-classify.md); it is not `identify`. Classifier research is split between general [`AI-generated image classifiers`](docs/ai-generated-image-classifiers.md) and [`SynthID source classifiers`](docs/synthid-classifiers.md). Other SynthID campaign logs are [`docs/synthid-detector-research.md`](docs/synthid-detector-research.md) and [`docs/synthid-removal-research.md`](docs/synthid-removal-research.md). The pre-split chronological archive is [`docs/synthid-detector-removal-plan.md`](docs/synthid-detector-removal-plan.md). The benchmark kernel and its pinned local oracles (AudioSeal, VideoSeal) cover the audio/video watermark cohorts and research studies: [`docs/benchmark-kernel.md`](docs/benchmark-kernel.md).
Research and current constraints are routed through [`docs/index.md`](docs/index.md), especially [`docs/known-limitations.md`](docs/known-limitations.md), [`docs/supported-signals.md`](docs/supported-signals.md), [`docs/synthid.md`](docs/synthid.md), and [`docs/watermarking-landscape.md`](docs/watermarking-landscape.md). Pixel photo classification is [`docs/photo-classify.md`](docs/photo-classify.md); the separate OpenAI/Google/unknown export-pipeline signal is [`docs/source-classify.md`](docs/source-classify.md). Neither runs inside `identify` or constitutes SynthID detection. Classifier research is split between general [`AI-generated image classifiers`](docs/ai-generated-image-classifiers.md) and [`SynthID source classifiers`](docs/synthid-classifiers.md). Other SynthID campaign logs are [`docs/synthid-detector-research.md`](docs/synthid-detector-research.md) and [`docs/synthid-removal-research.md`](docs/synthid-removal-research.md). The pre-split chronological archive is [`docs/synthid-detector-removal-plan.md`](docs/synthid-detector-removal-plan.md). The benchmark kernel and its pinned local oracles (AudioSeal, VideoSeal) cover the audio/video watermark cohorts and research studies: [`docs/benchmark-kernel.md`](docs/benchmark-kernel.md).
## Data safety
+15 -1
View File
@@ -59,6 +59,7 @@ specialized Python API can inspect and disrupt the validated local
| --- | --- |
| Metadata inspection and stripping | `remove-ai-watermarks` |
| Photograph AI-versus-camera classification | `remove-ai-watermarks[classify]` |
| OpenAI/Google/unknown source-export classification | `remove-ai-watermarks[source-classify]` |
| Visible detection and removal | `remove-ai-watermarks[visible]` |
| Visible video processing | `remove-ai-watermarks[video]` |
| Video SynthID removal | `remove-ai-watermarks[video,diffusion]` |
@@ -68,7 +69,7 @@ specialized Python API can inspect and disrupt the validated local
| Every production feature available on the active Python | `remove-ai-watermarks[all]` |
Lower-level and specialized extras include `pixels`, `heif`, `trustmark`,
`migan`, `lama`, `diffusion`, and `classify-onnx`. The
`migan`, `lama`, `diffusion`, `classify-onnx`, and `source-classify`. The
[installation guide](docs/installation.md#feature-extras) documents their exact
dependency composition, Python compatibility, and model requirements.
@@ -96,6 +97,19 @@ remove-ai-watermarks classify image.png
Guide: [photo pixel classification](docs/photo-classify.md).
For a lightweight, abstaining OpenAI/Google/unknown source-export signal after
metadata removal, use the separate Python API. It is not a SynthID detector:
```python
import remove_ai_watermarks as raiw
result = raiw.classify_source("image.png")
print(result.label, result.reason)
```
Install `remove-ai-watermarks[source-classify]`. Guide:
[source-pipeline classification](docs/source-classify.md).
Signed provenance is the supported route for SynthID and `identify` reads it.
There is no local SynthID pixel detector in the package. Research on a
periodic lattice expert is in `scripts/synthid_runtime/` and
+2
View File
@@ -13,6 +13,7 @@ to run the tool. Use the maintainer references only when changing the code.
| [Supported signals](supported-signals.md) | You need to know which visible marks, metadata formats, and invisible signals are covered. |
| [Known limitations](known-limitations.md) | You need the quality, device, format, or verification boundaries. |
| [Photo pixel classification](photo-classify.md) | You want `classify` / `classify_pixels`: AI versus camera, optional provider. |
| [Source-pipeline classification](source-classify.md) | You want an abstaining OpenAI/Google/unknown export-pipeline signal after metadata removal. |
| [Photo classifier training](photo-classify-training.md) | You need the retrain pack, public sources, and what is not published. |
| [Scope, safety, and legal notes](legal-and-safety.md) | You need the intended use and legal context. |
@@ -42,6 +43,7 @@ The current behavior is defined by the code, tests, README, and user guides.
- [AI-generated image classifiers](ai-generated-image-classifiers.md) (photo AI-versus-camera freeze consumed by `classify_pixels`, not `identify`)
- [Photo classifier Hugging Face card](photo-classify-hf/README.md) (Hub publication text of the 2026-08-31 freeze)
- [SynthID source classifiers](synthid-classifiers.md) (metadata-free OpenAI/Gemini source finding and provider-lineage experiments)
- [Source-pipeline classifier model card](source-classify-hf/README.md) (published derivative weights, aggregate evaluation, and intended-use boundary)
- [SynthID local detector research](synthid-detector-research.md)
- [SynthID mark removal research](synthid-removal-research.md)
- [SynthID identity research](synthid-robust-identity-research.md)
+2
View File
@@ -129,6 +129,7 @@ application actually uses:
| `trustmark` | Adobe TrustMark detection on Python 3.11-3.12 | trustmark | Yes |
| `classify` | Metadata-free photo AI-versus-camera classifier plus gated provider | `pixels`, Torch, Transformers | Yes |
| `classify-onnx` | Optional CPU-only ONNX vision runtime for `classify` | `classify`, ONNX Runtime | Yes |
| `source-classify` | Abstaining OpenAI/Google/unknown source-export classifier, not SynthID detection | `pixels`, Hugging Face Hub | Model download, no Torch |
| `diffusion` | Torch and Diffusers runtime; video SynthID regeneration | `pixels`, Torch, Diffusers | Yes |
| `migan` | MI-GAN ONNX fill backend | `visible`, ONNX Runtime | Model download, no Torch |
| `lama` | big-LaMa ONNX fill backend | `visible`, ONNX Runtime | Model download, no Torch |
@@ -156,6 +157,7 @@ flowchart LR
trustmark
classify --> pixels
onnx["classify-onnx"] --> classify
source["source-classify"] --> pixels
```
`heif`, `trustmark`, and `text-draft` are independent branches. Combine them
+8
View File
@@ -20,6 +20,14 @@ The command is never started by `identify`. A no-signal provenance result stays
unknown. This is not a clean verdict and it does not run cleanup. Full guide:
[photo pixel classification](photo-classify.md).
`classify_source` is a different, lightweight OpenAI/Google/unknown
source-export classifier. It is useful only while the pixels still resemble an
original export. A 95% resize reduced measured Google recall to 1%, and a JPEG
quality-75 round trip made every public evaluation row abstain. Its provider
label is not evidence that SynthID is present, and `unknown` is not evidence
that SynthID is absent. It is also never started by `identify`. Full guide:
[source-pipeline classification](source-classify.md).
## Visible removal
### Fill quality depends on the background
+24
View File
@@ -1116,6 +1116,30 @@ both backends explicitly (`tests/test_classify.py` pins the seam). User guide:
[photo-classify.md](photo-classify.md). Hub card:
[photo-classify-hf/README.md](photo-classify-hf/README.md).
### Source-pipeline classifier
[`source_classify.py`](../src/remove_ai_watermarks/source_classify.py) is a
separate, lightweight OpenAI/Google/unknown source-export classifier. It fuses
the existing 124-dimensional patch descriptor with the 768-dimensional
phase-free spectrum in
[`_internal/source_spectral.py`](../src/remove_ai_watermarks/_internal/source_spectral.py),
then evaluates the frozen 892 -> 128 -> 64 -> 3 GELU MLP in NumPy. Provider
labels clear independent calibrated margins; every other row is `unknown`.
The NumPy decision treats values within `1e-5` of the stored margin as the
inclusive training threshold, absorbing float32 BLAS differences; this matches
the frozen training-runtime decisions on all 27,811 cached internal and
external feature rows.
The runtime pins both the immutable Hub revision and the NPZ SHA-256, loads
with `allow_pickle=False`, and rejects any field, shape, class, activation, or
non-finite-value drift. `RAIW_SOURCE_CLASSIFY_WEIGHTS` is the offline override.
The `source-classify` extra adds `pixels` and Hugging Face Hub but no Torch or
Transformers. `identify` never imports or runs this path. Its output names a
complete source/export pipeline and is not SynthID detection. Unit contracts
and the threshold mutation guard live in `tests/test_source_classify.py`;
evaluation and attack limits live in
[`synthid-classifiers.md`](synthid-classifiers.md).
The CLIP loader suppresses discarded random parameter initialization with
Transformers' `no_init_weights` context before applying the complete frozen
state dict. Normal `CLIPModel(config)` construction initialized 427 million
+18 -1
View File
@@ -6,12 +6,29 @@ and pipeline modules are intended for maintainers and specialized workflows.
Dependency groups are identical for the CLI and Python API. The default install
covers metadata extraction, normalization, verdict logic, and stripping.
Array/pixel APIs use `pixels`; visible removal uses `visible`; DWT-DCT detection
uses `detect`; pixel photo classification uses `classify`; invisible image removal uses `qwen-zimage` and an NVIDIA GPU; and
uses `detect`; pixel photo classification uses `classify`; lightweight source-pipeline classification uses `source-classify`; invisible image removal uses `qwen-zimage` and an NVIDIA GPU; and
visible video processing uses `video`. The video-pixel SynthID-removal profile is
a separate VAE path that still runs on CPU and combines `video` and `diffusion`. Add `heif`
independently when path-based pixel APIs must decode HEIC, HEIF, or AVIF. See
the complete [feature-extra matrix](installation.md#feature-extras).
## Classify a source/export pipeline
Install `remove-ai-watermarks[source-classify]`, then call the explicit API:
```python
import remove_ai_watermarks as raiw
result = raiw.classify_source("metadata-stripped.png")
print(result.label) # "openai" | "google" | "unknown"
print(result.reason) # "classified" | "abstained" | "feature_unavailable"
```
This signal classifies a complete image-generation and export pipeline. It is
not a SynthID detector and does not establish watermark presence or absence.
See [source-pipeline classification](source-classify.md) for the result fields,
model pin, evaluation boundary, and offline weights override.
## Remove visible marks
Install `remove-ai-watermarks[visible]` before using the visible-removal API.
+11 -1
View File
@@ -92,7 +92,17 @@ alone does not upload new heads -- bump the pinned Hub revision when a new
head must reach installed clients. Pin a Hub revision in production so a
later freeze cannot silently change installed clients.
The workflow can also be started manually with an optional version input.
The separate `wiltodelta/raiw-source-classify` model is published with
`.github/workflows/publish-source-classify-hf.yml`. The workflow downloads the
derivative `source-pipeline-mlp.npz` artifact from the declared GitHub model
freeze, verifies its SHA-256, and uploads only that file plus the tracked card
and aggregate metrics under `docs/source-classify-hf/`. The private corpus,
catalog, paths, hashes, embeddings, and feature caches are outside this flow.
The workflow is manual and requires the model-freeze release tag plus an
explicit `publish` confirmation. Keep model-freeze GitHub releases marked as
pre-releases so a model artifact cannot replace the latest package release in
GitHub's release UI.
If a distribution job fails because a repository or Hugging Face credential is
invalid, rotate the corresponding GitHub secret and rerun the failed job. A
+65
View File
@@ -0,0 +1,65 @@
---
license: apache-2.0
library_name: numpy
pipeline_tag: image-classification
tags:
- image-forensics
- source-attribution
- open-set-classification
---
# RAIW source-pipeline classifier
This is a compact, abstaining pixel-forensics classifier for three labels:
`openai`, `google`, and `unknown`. It is intended for original-export-looking
images whose metadata may have been removed without changing the pixels.
This model is **not a SynthID detector**. It does not decode or verify a
watermark payload. Its labels describe complete generation and export
pipelines, so renderer, geometry, encoding, post-processing, and watermark
state may be confounded. `unknown` does not mean that SynthID is absent, and a
provider label does not prove that SynthID is present.
## Model
- Input: a 124-dimensional residual/spectral forensic feature vector plus a
768-dimensional phase-free spectral vector.
- Head: 892 -> 128 -> 64 -> 3 MLP with GELU activations.
- Output: `openai`, `google`, or abstain as `unknown` using independent
provider margins.
- Artifact: `source-pipeline-mlp.npz`
- SHA-256: `6430b0738869ff07b6845b7c6149c2a83bc3f3785878082886c241c6f7ca0c4a`
The artifact contains only learned parameters, normalization constants,
labels, and thresholds. It does not contain training images, paths, image
hashes, embeddings, or a training catalog.
## Evaluation
The operating point was selected on an internal calibration split. The locked
internal test and the public `bigdatamark/synthid-research` dataset were not
used for architecture, checkpoint, or threshold selection.
| Evaluation | Class | Recall | Precision |
|---|---|---:|---:|
| Locked internal test (5,369 images) | OpenAI | 60.4% | 84.9% |
| Locked internal test (5,369 images) | Google | 57.6% | 87.9% |
| Locked internal test (5,369 images) | Unknown | 98.6% | 93.7% |
| Public external originals (900 images) | OpenAI | 68.7% | 100.0% |
| Public external originals (900 images) | Google | 81.3% | 98.8% |
| Public external originals (900 images) | Unknown/Flux | 99.0% | 66.4% |
On the public external set, a 95% resize reduced Google recall to 1.0%, and a
JPEG quality-75 round trip caused the model to abstain on every image. These
are expected abstentions, not evidence that a watermark was removed.
## Intended use
Use this model as one probabilistic signal in a broader forensic report or to
suggest an appropriate processing path. Do not use it to claim watermark
presence, authorship, policy compliance, fraud, or provenance. Applications
should expose the abstention and the model's narrow source-pipeline claim.
Feature extraction and inference are versioned in the
`remove-ai-watermarks` Python project rather than in this model repository.
Use its explicit `classify_source` API; `identify` does not run this model.
+22
View File
@@ -0,0 +1,22 @@
{
"claim": "abstaining OpenAI/Google source-pipeline classification; not SynthID detection",
"artifact_sha256": "6430b0738869ff07b6845b7c6149c2a83bc3f3785878082886c241c6f7ca0c4a",
"classes": ["openai", "google", "unknown"],
"locked_internal_test": {
"n": 5369,
"confusion_actual_by_predicted": [[230, 6, 145], [0, 217, 160], [41, 24, 4546]],
"recall": {"openai": 0.6036745407, "google": 0.5755968170, "unknown": 0.9859032748},
"precision": {"openai": 0.8487084871, "google": 0.8785425101, "unknown": 0.9371263657}
},
"public_external_originals": {
"dataset": "bigdatamark/synthid-research",
"n": 900,
"confusion_actual_by_predicted": [[206, 0, 94], [0, 244, 56], [0, 3, 297]],
"recall": {"openai": 0.6866666667, "google": 0.8133333333, "unknown": 0.99},
"precision": {"openai": 1.0, "google": 0.9878542510, "unknown": 0.6644295302}
},
"selection": {
"minimum_calibration_precision": {"openai": 0.99, "google": 0.95},
"maximum_calibration_unknown_false_positive_rate": 0.01
}
}
+42
View File
@@ -0,0 +1,42 @@
# Source-pipeline classification
`classify_source` is an opt-in, metadata-free pixel classifier for images that
still resemble an original provider export. It returns `openai`, `google`, or
an explicit `unknown` abstention.
This is not a SynthID detector. It does not decode or verify a watermark
payload, and it must not be used to claim that SynthID is present or absent.
The model describes the complete source/export pipeline, where renderer,
geometry, encoding, post-processing, and watermark state can be confounded.
Install the lightweight runtime:
```bash
uv add "remove-ai-watermarks[source-classify]"
```
Call the Python API explicitly:
```python
import remove_ai_watermarks as raiw
result = raiw.classify_source("metadata-stripped.png")
print(result.label) # "openai" | "google" | "unknown"
print(result.reason) # "classified" | "abstained" | "feature_unavailable"
print(result.margins) # provider margins before thresholding
print(result.thresholds) # frozen provider thresholds
```
`feature_unavailable` is returned for images below the required 256x256
geometry or for a degenerate feature vector. `abstained` means neither provider
cleared its independent calibrated margin. Both are honest `unknown` results.
The first call downloads the exact pinned revision of
[`wiltodelta/raiw-source-classify`](https://huggingface.co/wiltodelta/raiw-source-classify).
Set `RAIW_SOURCE_CLASSIFY_WEIGHTS` to the NPZ itself or a directory containing
`source-pipeline-mlp.npz` for an offline deployment.
The model is useful as a routing suggestion for a processing workflow or as one
probabilistic signal in a broader forensic report. Do not use it as evidence of
authorship, fraud, policy compliance, provenance, or watermark presence. The
published model card records the evaluation and robustness limits.
+4
View File
@@ -121,6 +121,10 @@ entries enter the watermark inventory, while a fingerprint remains a
re-linkability warning, never selects pixel regeneration, and does not suppress
independent SynthID evidence.
The opt-in `classify_source` API is not an `identify` signal. It can suggest an
OpenAI or Google source/export pipeline after metadata removal, but it neither
detects nor decodes a watermark and never changes the provenance verdict.
## File and container formats
Pixel based image commands discover these extensions:
+35
View File
@@ -112,6 +112,41 @@ and a causal marked/unmarked contrast. Mark's same-prompt triples remain an
external source/export test, not evidence that the fitted features read the
SynthID payload.
### Full-corpus source-pipeline model, 2026-09-16
The next experiment trained a compact nonlinear head on the complete available
private corpus, including target-provider exports, photographs, and foreign
generator negatives. The corpus itself is not published. The released
`source-pipeline-mlp.npz` is a derivative artifact containing only learned
parameters, normalization constants, labels, and abstention thresholds; it
contains no images, paths, image hashes, embeddings, or training catalog.
The model joins the 124-dimensional forensic descriptor with the
768-dimensional phase-free spectral descriptor and applies a
892 -> 128 -> 64 -> 3 GELU MLP. Checkpoint and thresholds were selected on a
separate calibration partition. The 5,369-image hash-disjoint locked test and
Mark's public 900-image originals were not used for architecture, checkpoint,
or threshold selection.
| Evaluation | Class | Recall | Precision |
| --- | --- | ---: | ---: |
| Locked internal test (5,369 images) | OpenAI | 60.4% | 84.9% |
| Locked internal test (5,369 images) | Google | 57.6% | 87.9% |
| Locked internal test (5,369 images) | Unknown | 98.6% | 93.7% |
| Mark public originals (900 images) | OpenAI | 68.7% | 100.0% |
| Mark public originals (900 images) | Google | 81.3% | 98.8% |
| Mark public originals (900 images) | Unknown/FLUX | 99.0% | 66.4% |
A 95% resize reduced Google recall on the public set to 1%, and a JPEG
quality-75 round trip made every row abstain. This behavior matches the narrow
claim: the model classifies original-looking source/export pipelines after
metadata removal. It neither decodes nor detects SynthID, and its `unknown`
label does not mean that SynthID is absent. The model card and aggregate
metrics are tracked under
[`docs/source-classify-hf/`](source-classify-hf/README.md). Runtime integration
is provided by the explicit `classify_source` library API and never feeds the
`identify` watermark verdict.
Krawetz's Gemini-chat TPR critique is a verifier-quality claim, not a
feature we can ship. [Lead Stories, 2026-07](https://leadstories.com/analysis/2026/07/google-gemini-synthid-detector-confuses-results-within-same-chat.html)
documented Gemini repeating the first file's SynthID verdict inside a
+9 -2
View File
@@ -4,7 +4,7 @@
# on it, including the ComfyUI node package. The console script below carries
# the same weight, since users have it on PATH.
name = "remove-ai-watermarks"
version = "0.40.6"
version = "0.41.1"
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
readme = "README.md"
requires-python = ">=3.11,<3.15"
@@ -160,6 +160,13 @@ classify-onnx = [
"remove-ai-watermarks[classify]",
"onnxruntime>=1.24.0",
]
# Compact OpenAI/Google/unknown source-export classifier. This is intentionally
# separate from the heavier photo AI-versus-camera classifier and is not a
# SynthID detector. Weights download on first use.
source-classify = [
"remove-ai-watermarks[pixels]",
"huggingface-hub>=0.20.0",
]
# Universal region eraser backend -- big-LaMa via onnxruntime (Carve/LaMa-ONNX,
# Apache-2.0). CPU, no torch. Model (~200 MB) is downloaded on first use and
# cached by huggingface_hub; it is never bundled in this repo. The default cv2
@@ -205,7 +212,7 @@ dev = [
]
# ``qwen-zimage`` already pulls ``diffusion``; naming both would suggest diffusion is
# independently sufficient for a removal, which it is not.
all = ["remove-ai-watermarks[video,heif,detect,trustmark,text-restoration,migan,classify]"]
all = ["remove-ai-watermarks[video,heif,detect,trustmark,text-restoration,migan,classify,source-classify]"]
[project.scripts]
remove-ai-watermarks = "remove_ai_watermarks.cli:main"
+3
View File
@@ -55,6 +55,7 @@ explicitly names a tracked canonical result.
| `watermark_benchmark_real_cohort.py` | Build a publication-cleared, provider-paired and content-stratified real-image cohort. |
| `retrain_photo_classify.py` | CPU-retrain the 2026-08-31 photo heads from a sha256-keyed cache pack, no images. |
| `publish_photo_classify_hf.py` | Upload the photo-classify card and freeze weights to Hugging Face `wiltodelta/raiw-photo-classify`. Manual; the Action `publish-photo-classify-hf.yml` is the write-token path. |
| `publish_source_classify_hf.py` | Upload the derivative source-pipeline model and its public card to Hugging Face `wiltodelta/raiw-source-classify`. Manual; the Action `publish-source-classify-hf.yml` is the write-token path. |
| `verify_engine_selection_fixtures.py` | Verify hashes, dimensions, and prompt pairing in the tracked auto-engine content matrix. |
## Research and diagnostic prototypes
@@ -87,5 +88,7 @@ running the sync command without `--check` is the explicit snapshot update.
`_plain_console.py` provides plain-text fallbacks for Rich output, and
`_text_eval.py` contains normalization helpers shared by text-evaluation scripts.
`hf_publish.py` owns the common authenticated, allowlisted Hugging Face model
repository upload used by the two model publishers.
`engine_selection_manifest.py` owns the content-matrix schema and validation
shared by its integrity check and the real-image benchmark builder.
+31
View File
@@ -0,0 +1,31 @@
"""Shared Hugging Face model-repository publication helper."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable
from pathlib import Path
def publish_model_folder(
stage: Path,
*,
repo_id: str,
token: str,
message: str,
allow_patterns: Iterable[str],
) -> str:
"""Upload an explicit file allowlist and return the Hub revision."""
from huggingface_hub import HfApi, create_repo
create_repo(repo_id, repo_type="model", exist_ok=True, private=False, token=token)
commit = HfApi(token=token).upload_folder(
folder_path=str(stage),
repo_id=repo_id,
repo_type="model",
commit_message=message,
allow_patterns=list(allow_patterns),
)
return getattr(commit, "oid", "") or ""
+6 -9
View File
@@ -24,6 +24,8 @@ import shutil
import tempfile
from pathlib import Path
from hf_publish import publish_model_folder
log = logging.getLogger(__name__)
HUB_REPO = "wiltodelta/raiw-photo-classify"
@@ -92,18 +94,13 @@ def stage_weights(dest: Path, src: Path) -> None:
def publish(stage: Path, *, token: str, message: str) -> str:
from huggingface_hub import HfApi, create_repo
create_repo(HUB_REPO, repo_type="model", exist_ok=True, private=False, token=token)
api = HfApi(token=token)
commit = api.upload_folder(
folder_path=str(stage),
return publish_model_folder(
stage,
repo_id=HUB_REPO,
repo_type="model",
commit_message=message,
token=token,
message=message,
allow_patterns=["README.md", "operating-point.json", *WEIGHT_FILES, GATE_FILE, GATE_STABLE_FILE],
)
return getattr(commit, "oid", "") or ""
def main() -> int:
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Publish the source-pipeline model to Hugging Face.
The derivative weight file is downloaded by the GitHub Action from a model
freeze release. The private corpus, feature caches, and training catalog never
enter this repository or the Hub staging directory.
"""
from __future__ import annotations
import argparse
import logging
import os
import shutil
import tempfile
from pathlib import Path
from hf_publish import publish_model_folder
log = logging.getLogger(__name__)
HUB_REPO = "wiltodelta/raiw-source-classify"
MODEL_FILE = "source-pipeline-mlp.npz"
REPO_ROOT = Path(__file__).resolve().parents[1]
CARD_DIR = REPO_ROOT / "docs" / "source-classify-hf"
PUBLIC_FILES = ("README.md", "metrics.json", MODEL_FILE)
def stage_release(dest: Path, src: Path) -> None:
"""Stage the two tracked documents and one derivative model artifact."""
sources = {
"README.md": CARD_DIR / "README.md",
"metrics.json": CARD_DIR / "metrics.json",
MODEL_FILE: src.expanduser().resolve() / MODEL_FILE,
}
missing = [str(path) for path in sources.values() if not path.is_file()]
if missing:
raise SystemExit(f"missing publication files: {', '.join(missing)}")
dest.mkdir(parents=True, exist_ok=True)
for name, path in sources.items():
shutil.copy2(path, dest / name)
def publish(stage: Path, *, token: str, message: str) -> str:
"""Upload the exact staged allowlist and return the Hub revision."""
return publish_model_folder(
stage,
repo_id=HUB_REPO,
token=token,
message=message,
allow_patterns=PUBLIC_FILES,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--src", type=Path, required=True)
parser.add_argument("--message", default="Publish source-pipeline classifier")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(message)s")
token = os.environ.get("HF_TOKEN", "").strip()
if not token:
raise SystemExit("HF_TOKEN is not set (write role required)")
with tempfile.TemporaryDirectory(prefix="raiw-source-hf-") as tmp:
stage = Path(tmp)
stage_release(stage, args.src)
revision = publish(stage, token=token, message=args.message)
log.info("published %s revision=%s", HUB_REPO, revision or "(unknown)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+9 -59
View File
@@ -30,6 +30,14 @@ import numpy as np
from numpy.typing import NDArray
from PIL import Image, ImageFilter
from remove_ai_watermarks._internal import source_spectral as _spectral
SpectralGeometry = _spectral.SpectralGeometry
make_geometry = _spectral.make_geometry
phase_free_log_spectrum = _spectral.phase_free_log_spectrum
spectral_feature = _spectral.spectral_feature
_source_feature_from_image = _spectral.feature_from_image
log = logging.getLogger(__name__)
IMAGE_SUFFIXES = frozenset({".jpg", ".jpeg", ".png", ".webp"})
@@ -40,17 +48,6 @@ IntArray = NDArray[np.int64]
BoolArray = NDArray[np.bool_]
@dataclass(frozen=True)
class SpectralGeometry:
"""Precomputed coordinates for one phase-free spectral representation."""
size: int
grid: int
radius: NDArray[np.int32]
radius_counts: IntArray
passband: BoolArray
@dataclass(frozen=True)
class FoldModel:
"""One standardized dual-ridge model and its held-out rows."""
@@ -62,51 +59,6 @@ class FoldModel:
weights: FloatArray
def make_geometry(size: int = 256, grid: int = 32) -> SpectralGeometry:
"""Build the radial detrending and pooling geometry."""
if size < 16 or grid < 2 or size % grid:
raise ValueError("size must be at least 16 and divisible by grid")
yy, xx = np.mgrid[:size, :size]
radius = np.rint(np.hypot(yy - size // 2, xx - size // 2)).astype(np.int32)
radius_counts = np.bincount(radius.ravel())
passband = (radius >= 4) & (radius <= size * 0.48)
return SpectralGeometry(size, grid, radius, radius_counts, passband)
def phase_free_log_spectrum(channel: FloatArray) -> FloatArray:
"""Return centered log FFT magnitude, deliberately discarding phase."""
if channel.ndim != 2:
raise ValueError("channel must be a two-dimensional array")
centered = np.asarray(channel, dtype=np.float64) - float(np.mean(channel))
return np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(centered))))
def spectral_feature(pixels: FloatArray, geometry: SpectralGeometry) -> FloatArray:
"""Extract pooled radial-residual spectra from luma and opponent color."""
if pixels.shape != (geometry.size, geometry.size, 3):
raise ValueError(f"pixels must have shape {(geometry.size, geometry.size, 3)}, got {pixels.shape}")
red, green, blue = np.moveaxis(np.asarray(pixels, dtype=np.float64), 2, 0)
channels = (
0.299 * red + 0.587 * green + 0.114 * blue,
red - green,
blue - (red + green) / 2.0,
)
block = geometry.size // geometry.grid
features: list[FloatArray] = []
for channel in channels:
spectrum = phase_free_log_spectrum(channel)
radial_sum = np.bincount(geometry.radius.ravel(), weights=spectrum.ravel())
radial_mean = radial_sum / geometry.radius_counts
residual = np.where(
geometry.passband,
spectrum - radial_mean[geometry.radius],
0.0,
)
pooled = residual.reshape(geometry.grid, block, geometry.grid, block).mean(axis=(1, 3))
features.append(pooled.ravel())
return np.concatenate(features)
def _apply_attack(image: Image.Image, attack: str) -> Image.Image:
"""Apply one declared robustness transform before canonical resizing."""
if attack == "original":
@@ -140,9 +92,7 @@ def feature_from_path(path: Path, geometry: SpectralGeometry, attack: str) -> Fl
def feature_from_image(image: Image.Image, geometry: SpectralGeometry, attack: str) -> FloatArray:
"""Extract one attacked feature from an already decoded RGB image."""
attacked = _apply_attack(image, attack)
canonical = attacked.resize((geometry.size, geometry.size), Image.Resampling.LANCZOS)
pixels = np.asarray(canonical, dtype=np.float64) / 255.0
return spectral_feature(pixels, geometry)
return _source_feature_from_image(attacked, geometry)
def discover_grouped_paths(
+9 -1
View File
@@ -14,6 +14,7 @@ High-level API (lazy, so ``import remove_ai_watermarks`` stays cheap)::
raiw.remove_video_metadata("in.mp4", "out.mp4") # verified metadata strip
raiw.remove_video_invisible("in.mp4", "out.mp4") # calibrated video-pixel regeneration
raiw.remove_video_visible("in.mp4", "out.mp4") # stable visible video-mark removal
raiw.classify_source("in.png") # source pipeline, not SynthID detection
For a provenance verdict use the ``identify`` submodule::
@@ -33,7 +34,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
__version__ = "0.40.6"
__version__ = "0.41.1"
__all__ = [
"BatchItemResult",
@@ -41,8 +42,10 @@ __all__ = [
"InvisibleOptions",
"MetadataStripIncomplete",
"RemoveAllResult",
"SourceClassification",
"VisibleRemovalResult",
"__version__",
"classify_source",
"identify_video",
"inspect_video_metadata",
"remove_all",
@@ -71,6 +74,7 @@ if TYPE_CHECKING:
remove_visible_detailed,
visible_provenance,
)
from remove_ai_watermarks.source_classify import SourceClassification, classify_source
from remove_ai_watermarks.video import (
identify_video,
inspect_video_metadata,
@@ -101,6 +105,10 @@ def __getattr__(name: str) -> object:
from remove_ai_watermarks import api
return getattr(api, name)
if name in ("SourceClassification", "classify_source"):
from remove_ai_watermarks import source_classify
return getattr(source_classify, name)
if name in (
"identify_video",
"inspect_video_metadata",
@@ -1,7 +1,7 @@
"""Generated C2PA soft-binding registry; do not edit by hand.
Source: https://raw.githubusercontent.com/c2pa-org/softbinding-algorithm-list/main/softbinding-algorithm-list.json
Revision: c0218cd628c9163ad62a08c3deff5a10fa0e0d0b
Revision: 1b3ef637890ea8228bca13d8941cfd54da941867
Source license: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
Changes: validated, ordered, and reduced to fields used at runtime; descriptions
are shortened to their first sentence for display.
@@ -12,7 +12,7 @@ from __future__ import annotations
C2PA_SOFT_BINDING_SOURCE_URL = (
"https://raw.githubusercontent.com/c2pa-org/softbinding-algorithm-list/main/softbinding-algorithm-list.json"
)
C2PA_SOFT_BINDING_SOURCE_REVISION = "c0218cd628c9163ad62a08c3deff5a10fa0e0d0b"
C2PA_SOFT_BINDING_SOURCE_REVISION = "1b3ef637890ea8228bca13d8941cfd54da941867"
C2PA_SOFT_BINDING_SOURCE_LICENSE = "CC BY 4.0"
C2PA_SOFT_BINDING_ROWS: tuple[
@@ -387,7 +387,7 @@ C2PA_SOFT_BINDING_ROWS: tuple[
"eu.sasha.softbinding.v1",
"watermark",
("image",),
("image/jpeg", "image/png"),
("image/jpeg", "image/png", "image/webp"),
(
"Invisible watermarking technology for image and video assets, enabling C2PA manifest "
"recovery and content provenance verification after metadata stripping or transcoding"
@@ -0,0 +1,78 @@
"""Phase-free spectral features for source/export-pipeline classification."""
from __future__ import annotations
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING
import numpy as np
from PIL import Image
if TYPE_CHECKING:
from numpy.typing import NDArray
@dataclass(frozen=True)
class SpectralGeometry:
"""Precomputed coordinates for one phase-free spectral representation."""
size: int
grid: int
radius: NDArray[np.int32]
radius_counts: NDArray[np.int64]
passband: NDArray[np.bool_]
@lru_cache(maxsize=4)
def make_geometry(size: int = 256, grid: int = 16) -> SpectralGeometry:
"""Build radial detrending and pooling geometry."""
if size < 16 or grid < 2 or size % grid:
raise ValueError("size must be at least 16 and divisible by grid")
yy, xx = np.mgrid[:size, :size]
radius = np.rint(np.hypot(yy - size // 2, xx - size // 2)).astype(np.int32)
radius_counts = np.bincount(radius.ravel())
passband = (radius >= 4) & (radius <= size * 0.48)
return SpectralGeometry(size, grid, radius, radius_counts, passband)
def phase_free_log_spectrum(channel: NDArray[np.float64]) -> NDArray[np.float64]:
"""Return centered log FFT magnitude, deliberately discarding phase."""
if channel.ndim != 2:
raise ValueError("channel must be a two-dimensional array")
centered = channel - float(np.mean(channel))
return np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(centered))))
def spectral_feature(pixels: NDArray[np.float64], geometry: SpectralGeometry) -> NDArray[np.float64]:
"""Extract pooled radial-residual spectra from luma and opponent color."""
expected = (geometry.size, geometry.size, 3)
if pixels.shape != expected:
raise ValueError(f"pixels must have shape {expected}, got {pixels.shape}")
red, green, blue = np.moveaxis(pixels, 2, 0)
channels = (
0.299 * red + 0.587 * green + 0.114 * blue,
red - green,
blue - (red + green) / 2.0,
)
block = geometry.size // geometry.grid
features: list[NDArray[np.float64]] = []
for channel in channels:
spectrum = phase_free_log_spectrum(channel)
radial_sum = np.bincount(geometry.radius.ravel(), weights=spectrum.ravel())
radial_mean = radial_sum / geometry.radius_counts
residual = np.where(
geometry.passband,
spectrum - radial_mean[geometry.radius],
0.0,
)
pooled = residual.reshape(geometry.grid, block, geometry.grid, block).mean(axis=(1, 3))
features.append(pooled.ravel())
return np.concatenate(features)
def feature_from_image(image: Image.Image, geometry: SpectralGeometry) -> NDArray[np.float64]:
"""Resize an RGB image canonically and extract its phase-free feature."""
canonical = image.resize((geometry.size, geometry.size), Image.Resampling.LANCZOS)
pixels = np.asarray(canonical, dtype=np.float64) / 255.0
return spectral_feature(pixels, geometry)
+238
View File
@@ -0,0 +1,238 @@
"""Abstaining source/export-pipeline classification from image pixels.
This module does not detect or decode SynthID. It labels original-export-looking
pixel pipelines as OpenAI, Google, or unknown after metadata may have been
removed. Call it explicitly; ``identify`` never imports or runs it.
"""
from __future__ import annotations
import math
import os
from dataclasses import dataclass
from functools import lru_cache
from hashlib import sha256
from importlib import import_module
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from numpy.typing import NDArray
SOURCE_CLASSIFY_EXTRA = "'remove-ai-watermarks[source-classify]'"
WEIGHTS_ENV = "RAIW_SOURCE_CLASSIFY_WEIGHTS"
WEIGHTS_REPO = "wiltodelta/raiw-source-classify"
WEIGHTS_REVISION = "4b8b408d03dac20231d46217977fd164414b6b9e"
MODEL_FILE = "source-pipeline-mlp.npz"
MODEL_SHA256 = "6430b0738869ff07b6845b7c6149c2a83bc3f3785878082886c241c6f7ca0c4a"
CLASSES = ("openai", "google", "unknown")
PROVIDERS = CLASSES[:2]
FEATURE_WIDTH = 892
SPECTRAL_GRID = 16
CLAIM = "source/export pipeline classification; not SynthID detection"
# NumPy and the training runtime can differ by a few float32 ULPs at a matrix
# boundary. Treat values within this tolerance as the stored threshold so the
# exported runtime preserves the training runtime's >= decision.
THRESHOLD_TOLERANCE = 1e-5
SourceLabel = Literal["openai", "google", "unknown"]
SourceReason = Literal["classified", "abstained", "feature_unavailable"]
@dataclass(frozen=True)
class SourceClassification:
"""One narrow source-pipeline result with an explicit abstention."""
label: SourceLabel
reason: SourceReason
scores: dict[str, float]
margins: dict[str, float]
thresholds: dict[str, float]
claim: str = CLAIM
model_revision: str = WEIGHTS_REVISION
def to_dict(self) -> dict[str, Any]:
return {
"label": self.label,
"reason": self.reason,
"scores": self.scores,
"margins": self.margins,
"thresholds": self.thresholds,
"claim": self.claim,
"model_revision": self.model_revision,
}
@dataclass(frozen=True)
class _Model:
mean: NDArray[Any]
scale: NDArray[Any]
margins: NDArray[Any]
layer0_weight: NDArray[Any]
layer0_bias: NDArray[Any]
layer1_weight: NDArray[Any]
layer1_bias: NDArray[Any]
output_weight: NDArray[Any]
output_bias: NDArray[Any]
def is_available() -> bool:
"""Return whether the lightweight source-classify runtime is installed."""
from remove_ai_watermarks.optional_deps import module_available
return module_available("numpy", "cv2", "huggingface_hub")
def _model_path() -> Path:
override = os.environ.get(WEIGHTS_ENV, "").strip()
if override:
path = Path(override).expanduser()
candidate = path / MODEL_FILE if path.is_dir() else path
if not candidate.is_file():
raise RuntimeError(f"{WEIGHTS_ENV} does not contain {MODEL_FILE}: {path}")
return candidate
try:
hub: Any = import_module("huggingface_hub")
except ModuleNotFoundError as error:
raise RuntimeError(f"Install {SOURCE_CLASSIFY_EXTRA} to download the source classifier") from error
return Path(
hub.hf_hub_download(
repo_id=WEIGHTS_REPO,
filename=MODEL_FILE,
revision=WEIGHTS_REVISION,
)
)
@lru_cache(maxsize=2)
def _load_model(path: Path, expected_sha256: str | None = MODEL_SHA256) -> _Model:
import numpy as np
if expected_sha256 is not None:
digest = sha256(path.read_bytes()).hexdigest()
if digest != expected_sha256:
raise ValueError(f"model SHA-256 mismatch: expected {expected_sha256}, got {digest}")
required = {
"classes",
"kind",
"mean",
"scale",
"margins",
"layer0_weight",
"layer0_bias",
"layer1_weight",
"layer1_bias",
"output_weight",
"output_bias",
"activation",
}
with np.load(path, allow_pickle=False) as payload:
if set(payload.files) != required:
raise ValueError(f"model fields must be exactly {sorted(required)}")
if tuple(str(value) for value in payload["classes"]) != CLASSES:
raise ValueError(f"model classes must be {CLASSES}")
if str(payload["kind"]) != "fused":
raise ValueError("model kind must be fused")
if str(payload["activation"]) != "gelu_exact":
raise ValueError("model activation must be gelu_exact")
arrays = {
name: np.asarray(
payload[name],
dtype=np.float64 if name in {"mean", "scale", "margins"} else np.float32,
)
for name in required - {"classes", "kind", "activation"}
}
shapes = {
"mean": (FEATURE_WIDTH,),
"scale": (FEATURE_WIDTH,),
"margins": (len(PROVIDERS),),
"layer0_weight": (128, FEATURE_WIDTH),
"layer0_bias": (128,),
"layer1_weight": (64, 128),
"layer1_bias": (64,),
"output_weight": (len(CLASSES), 64),
"output_bias": (len(CLASSES),),
}
for name, expected in shapes.items():
if arrays[name].shape != expected:
raise ValueError(f"model field {name} must have shape {expected}")
if not np.all(np.isfinite(arrays[name])):
raise ValueError(f"model field {name} must be finite")
if np.any(arrays["scale"] <= 0) or np.any(arrays["margins"] < 0):
raise ValueError("model scales must be positive and margins non-negative")
return _Model(**arrays)
def _gelu_exact(values: NDArray[Any]) -> NDArray[Any]:
import numpy as np
flat = values.ravel()
errors = np.fromiter((math.erf(float(value) / math.sqrt(2.0)) for value in flat), dtype=np.float32)
return (0.5 * flat * (1.0 + errors)).reshape(values.shape).astype(np.float32)
def _scores(features: NDArray[Any], model: _Model) -> NDArray[Any]:
import numpy as np
values = np.asarray((features - model.mean) / model.scale, dtype=np.float32)
hidden0 = _gelu_exact(model.layer0_weight @ values + model.layer0_bias)
hidden1 = _gelu_exact(model.layer1_weight @ hidden0 + model.layer1_bias)
return np.asarray(model.output_weight @ hidden1 + model.output_bias, dtype=np.float64)
def _result(raw_scores: NDArray[Any], thresholds: NDArray[Any]) -> SourceClassification:
import numpy as np
margins = np.empty(len(PROVIDERS), dtype=np.float64)
for index in range(len(PROVIDERS)):
competitors = np.delete(raw_scores, index)
margins[index] = raw_scores[index] - float(np.max(competitors))
excess = margins - thresholds
best = int(np.argmax(excess))
accepted = bool(excess[best] >= -THRESHOLD_TOLERANCE)
label: SourceLabel = PROVIDERS[best] if accepted else "unknown"
return SourceClassification(
label=label,
reason="classified" if accepted else "abstained",
scores={name: float(raw_scores[index]) for index, name in enumerate(CLASSES)},
margins={name: float(margins[index]) for index, name in enumerate(PROVIDERS)},
thresholds={name: float(thresholds[index]) for index, name in enumerate(PROVIDERS)},
)
def _unavailable(model: _Model) -> SourceClassification:
return SourceClassification(
label="unknown",
reason="feature_unavailable",
scores={},
margins={},
thresholds={name: float(model.margins[index]) for index, name in enumerate(PROVIDERS)},
)
def classify_source(source: str | os.PathLike[str]) -> SourceClassification:
"""Classify an original-looking image export, abstaining as ``unknown``.
The result describes a complete source/export pipeline. It is not evidence
that SynthID is present or absent.
"""
import numpy as np
from PIL import Image
from remove_ai_watermarks import image_io
from remove_ai_watermarks._internal.forensic_124d import image_features
from remove_ai_watermarks._internal.source_spectral import feature_from_image, make_geometry
model = _load_model(_model_path(), expected_sha256=MODEL_SHA256)
image_io._register_heif() # pyright: ignore[reportPrivateUsage]
with Image.open(source) as opened:
image = opened.convert("RGB")
pixels = np.asarray(image, dtype=np.uint8)
forensic = image_features(pixels)
if forensic is None:
return _unavailable(model)
spectral = feature_from_image(image, make_geometry(grid=SPECTRAL_GRID))
features = np.concatenate((forensic, spectral))
if features.shape != (FEATURE_WIDTH,) or not np.all(np.isfinite(features)):
return _unavailable(model)
return _result(_scores(features, model), model.margins)
+4
View File
@@ -23,6 +23,10 @@ class TestTopLevelExports:
assert raiw.remove_visible_detailed is api.remove_visible_detailed
assert raiw.VisibleRemovalResult is api.VisibleRemovalResult
assert raiw.visible_provenance is api.visible_provenance
from remove_ai_watermarks import source_classify
assert raiw.classify_source is source_classify.classify_source
assert raiw.SourceClassification is source_classify.SourceClassification
def test_unknown_attribute_raises(self):
with pytest.raises(AttributeError):
+8 -16
View File
@@ -2,29 +2,21 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from typing import Any
import pytest
def _publish() -> Any:
path = Path(__file__).resolve().parents[1] / "scripts" / "publish_photo_classify_hf.py"
spec = importlib.util.spec_from_file_location("publish_photo_classify_hf", path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
import publish_photo_classify_hf as publish # noqa: E402
def test_hub_id_is_the_photo_classify_repo() -> None:
assert _publish().HUB_REPO == "wiltodelta/raiw-photo-classify"
assert publish.HUB_REPO == "wiltodelta/raiw-photo-classify"
def test_publish_freeze_includes_both_clip_runtimes() -> None:
publish = _publish()
from remove_ai_watermarks.classify import _WEIGHT_FILES, CLIP_ONNX_FILE, WEIGHTS_ALLOW_PATTERNS
assert publish.CLIP_FILE in publish.WEIGHT_FILES
@@ -41,7 +33,7 @@ def test_weight_paths_accepts_run1_layout(tmp_path: Path) -> None:
run1.mkdir()
(run1 / "detector.pt").write_bytes(b"det")
(run1 / "provider.pt").write_bytes(b"prov")
found = _publish().weight_paths(tmp_path)
found = publish.weight_paths(tmp_path)
assert found["detector.pt"] == run1 / "detector.pt"
assert found["clip-l-ft.pt"] == tmp_path / "clip-l-ft.pt"
@@ -50,12 +42,12 @@ def test_weight_paths_requires_every_file(tmp_path: Path) -> None:
(tmp_path / "clip-l-ft.pt").write_bytes(b"clip")
(tmp_path / "clip-l-ft-vision-fp32.onnx").write_bytes(b"onnx")
with pytest.raises(SystemExit, match=r"missing probe-weights-clip-l-ft\.npz"):
_publish().weight_paths(tmp_path)
publish.weight_paths(tmp_path)
def test_stage_card_copies_tracked_files(tmp_path: Path) -> None:
dest = tmp_path / "stage"
_publish().stage_card(dest)
publish.stage_card(dest)
assert (dest / "README.md").is_file()
assert (dest / "operating-point.json").is_file()
text = (dest / "operating-point.json").read_text()
+37
View File
@@ -0,0 +1,37 @@
"""Local staging checks for the source-classifier Hub publisher."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
import publish_source_classify_hf as publish # noqa: E402
def test_hub_id_is_the_source_classify_repo() -> None:
assert publish.HUB_REPO == "wiltodelta/raiw-source-classify"
def test_stage_release_copies_only_public_artifacts(tmp_path: Path) -> None:
source = tmp_path / "source"
source.mkdir()
(source / "source-pipeline-mlp.npz").write_bytes(b"weights")
destination = tmp_path / "stage"
publish.stage_release(destination, source)
assert sorted(path.name for path in destination.iterdir()) == sorted(publish.PUBLIC_FILES)
def test_stage_release_requires_the_model(tmp_path: Path) -> None:
with pytest.raises(SystemExit, match=r"source-pipeline-mlp\.npz"):
publish.stage_release(tmp_path / "stage", tmp_path)
def test_card_sha_matches_the_metrics_manifest() -> None:
metrics = json.loads((publish.CARD_DIR / "metrics.json").read_text())
card = (publish.CARD_DIR / "README.md").read_text()
assert metrics["artifact_sha256"] in card
+123
View File
@@ -0,0 +1,123 @@
"""Unit contracts for the lightweight source-pipeline runtime."""
from __future__ import annotations
import re
from hashlib import sha256
from typing import TYPE_CHECKING
import numpy as np
import pytest
from remove_ai_watermarks import source_classify
if TYPE_CHECKING:
from pathlib import Path
def test_model_identity_is_immutable() -> None:
assert re.fullmatch(r"[0-9a-f]{40}", source_classify.WEIGHTS_REVISION)
assert re.fullmatch(r"[0-9a-f]{64}", source_classify.MODEL_SHA256)
def _artifact(path: Path, *, activation: str = "gelu_exact", output_bias: tuple[float, ...] = (0.0, 0.0, 0.0)) -> None:
np.savez_compressed(
path,
classes=np.asarray(source_classify.CLASSES),
kind=np.asarray("fused"),
mean=np.zeros(source_classify.FEATURE_WIDTH),
scale=np.ones(source_classify.FEATURE_WIDTH),
margins=np.asarray((0.5, 0.5)),
layer0_weight=np.zeros((128, source_classify.FEATURE_WIDTH), dtype=np.float32),
layer0_bias=np.zeros(128, dtype=np.float32),
layer1_weight=np.zeros((64, 128), dtype=np.float32),
layer1_bias=np.zeros(64, dtype=np.float32),
output_weight=np.zeros((3, 64), dtype=np.float32),
output_bias=np.asarray(output_bias, dtype=np.float32),
activation=np.asarray(activation),
)
def test_loader_accepts_the_exact_pickle_free_schema(tmp_path: Path) -> None:
path = tmp_path / source_classify.MODEL_FILE
_artifact(path)
model = source_classify._load_model(path, expected_sha256=None)
assert model.mean.shape == (source_classify.FEATURE_WIDTH,)
assert model.margins.tolist() == pytest.approx([0.5, 0.5])
def test_loader_rejects_the_mislabeled_activation(tmp_path: Path) -> None:
path = tmp_path / source_classify.MODEL_FILE
_artifact(path, activation="gelu_tanh")
with pytest.raises(ValueError, match="activation must be gelu_exact"):
source_classify._load_model(path, expected_sha256=None)
def test_provider_threshold_mutation_changes_the_real_decision(tmp_path: Path) -> None:
path = tmp_path / source_classify.MODEL_FILE
_artifact(path, output_bias=(2.0, 0.0, 0.0))
model = source_classify._load_model(path, expected_sha256=None)
features = np.zeros(source_classify.FEATURE_WIDTH, dtype=np.float32)
scores = source_classify._scores(features, model)
accepted = source_classify._result(scores, np.asarray((1.5, 0.5), dtype=np.float32))
rejected = source_classify._result(scores, np.asarray((2.5, 0.5), dtype=np.float32))
assert accepted.label == "openai"
assert accepted.reason == "classified"
assert rejected.label == "unknown"
assert rejected.reason == "abstained"
def test_a_float32_threshold_tie_preserves_the_inclusive_training_decision() -> None:
scores = np.asarray((2.0, 0.0, 0.0), dtype=np.float32)
result = source_classify._result(scores, np.asarray((2.0, 0.5), dtype=np.float32))
assert result.label == "openai"
assert result.reason == "classified"
def test_small_image_returns_honest_unknown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from PIL import Image
weights = tmp_path / "weights"
weights.mkdir()
_artifact(weights / source_classify.MODEL_FILE)
monkeypatch.setenv(source_classify.WEIGHTS_ENV, str(weights))
monkeypatch.setattr(
source_classify,
"MODEL_SHA256",
sha256((weights / source_classify.MODEL_FILE).read_bytes()).hexdigest(),
)
image = tmp_path / "small.png"
Image.new("RGB", (128, 128), "white").save(image)
result = source_classify.classify_source(image)
assert result.label == "unknown"
assert result.reason == "feature_unavailable"
assert result.scores == {}
def test_model_hash_is_a_runtime_precondition(tmp_path: Path) -> None:
path = tmp_path / source_classify.MODEL_FILE
_artifact(path)
with pytest.raises(ValueError, match="model SHA-256 mismatch"):
source_classify._load_model(path, expected_sha256="0" * 64)
def test_spectral_feature_is_finite_and_has_the_frozen_width() -> None:
from PIL import Image
from remove_ai_watermarks._internal.source_spectral import feature_from_image, make_geometry
rng = np.random.default_rng(20260916)
pixels = rng.integers(0, 256, size=(300, 400, 3), dtype=np.uint8)
feature = feature_from_image(Image.fromarray(pixels), make_geometry())
assert feature.shape == (768,)
assert np.all(np.isfinite(feature))
Generated
+132 -123
View File
@@ -26,7 +26,7 @@ resolution-markers = [
[[package]]
name = "accelerate"
version = "1.14.0"
version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
@@ -38,9 +38,9 @@ dependencies = [
{ name = "safetensors" },
{ name = "torch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/b5/1d3ed029ac71d3f2961346829a268da923698e9fd63f218f78841f216bfd/accelerate-1.15.0.tar.gz", hash = "sha256:5654f8c5eaa0d4fa68b33e287a97765da6849bf6d51dcac874e73fbbddfb6134", size = 422615, upload-time = "2026-09-09T13:04:49.078Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" },
{ url = "https://files.pythonhosted.org/packages/8a/4c/34f0450479d01195027260da68d8a3880683f1640c3ca5adf64acb3185f1/accelerate-1.15.0-py3-none-any.whl", hash = "sha256:97eacca0b73e45cb867dbf8c5d5d4dc32219544300e0c8992c7334dc2ef33cec", size = 394295, upload-time = "2026-09-09T13:04:47.331Z" },
]
[[package]]
@@ -800,7 +800,7 @@ name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-pathfinder" },
{ name = "cuda-pathfinder", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
@@ -833,43 +833,43 @@ wheels = [
[package.optional-dependencies]
cublas = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cufft = [
{ name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cufile = [
{ name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
curand = [
{ name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cusolver = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
[[package]]
@@ -1066,7 +1066,7 @@ wheels = [
[package.optional-dependencies]
http = [
{ name = "aiohttp" },
{ name = "aiohttp", marker = "python_full_version < '3.13'" },
]
[[package]]
@@ -1198,8 +1198,8 @@ name = "httpcore2"
version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
{ name = "h11", marker = "sys_platform != 'emscripten'" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
wheels = [
@@ -1249,7 +1249,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "1.30.0"
version = "1.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -1262,9 +1262,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
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" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/f0/61159db90b5cd275d55516fe27920828e7d3be4053fdbdb27c3f70e5f1ef/huggingface_hub-1.31.0.tar.gz", hash = "sha256:f8e9e710a210613fa5d0f26bba6da05ef4aef9fba5a0f23f508f5ac4d08b6f90", size = 968039, upload-time = "2026-09-10T10:27:22.724Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/c3/7f/3f886a625043b77312b80da2f2bf00b5ecbf5a73061af1aa0259cd258c9d/huggingface_hub-1.31.0-py3-none-any.whl", hash = "sha256:9dbb6a503cbe2494ea666695207e7262d410659e09134059deb83e5480864667", size = 798313, upload-time = "2026-09-10T10:27:20.798Z" },
]
[[package]]
@@ -1461,15 +1461,15 @@ name = "lightning"
version = "2.6.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fsspec", extra = ["http"] },
{ name = "lightning-utilities" },
{ name = "packaging" },
{ name = "pytorch-lightning" },
{ name = "pyyaml" },
{ name = "torch" },
{ name = "torchmetrics" },
{ name = "tqdm" },
{ name = "typing-extensions" },
{ name = "fsspec", extra = ["http"], marker = "python_full_version < '3.13'" },
{ name = "lightning-utilities", marker = "python_full_version < '3.13'" },
{ name = "packaging", marker = "python_full_version < '3.13'" },
{ name = "pytorch-lightning", marker = "python_full_version < '3.13'" },
{ name = "pyyaml", marker = "python_full_version < '3.13'" },
{ name = "torch", marker = "python_full_version < '3.13'" },
{ name = "torchmetrics", marker = "python_full_version < '3.13'" },
{ name = "tqdm", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/b8/fbf1469d917c3ed3b22e00ab3a852fb9a5f85b9dc7f7060eedc3449098e7/lightning-2.6.6.tar.gz", hash = "sha256:1c6329e241f8a85f29fedead89c1ee138b36caa08de60c260c6a7eaee81c8295", size = 659902, upload-time = "2026-09-10T09:41:03.009Z" }
wheels = [
@@ -1481,8 +1481,8 @@ name = "lightning-utilities"
version = "0.15.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "typing-extensions" },
{ name = "packaging", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/45/7fa8f56b17dc0f0a41ec70dd307ecd6787254483549843bef4c30ab5adce/lightning_utilities-0.15.3.tar.gz", hash = "sha256:792ae0204c79f6859721ac7f386c237a33b0ed06ba775009cb894e010a842033", size = 33553, upload-time = "2026-02-22T14:48:53.348Z" }
wheels = [
@@ -1896,7 +1896,7 @@ name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cuda-nvrtc" },
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
@@ -1935,7 +1935,7 @@ name = "nvidia-cudnn-cu13"
version = "9.24.0.43"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ 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" },
@@ -1947,7 +1947,7 @@ name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@@ -1977,9 +1977,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-cusparse", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@@ -1991,7 +1991,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@@ -2058,7 +2058,7 @@ wheels = [
[[package]]
name = "onnxruntime"
version = "1.29.0"
version = "1.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flatbuffers" },
@@ -2068,35 +2068,35 @@ dependencies = [
{ name = "protobuf" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/a8/0520890321b8ff40b908cf165a93eb58fbc8f85c14db637277ea866c9544/onnxruntime-1.29.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:07c5907474dec4a2792fd7626b753dc66707808385a6d9eecf993db0066a9d0f", size = 21420890, upload-time = "2026-08-17T22:53:33.429Z" },
{ url = "https://files.pythonhosted.org/packages/0b/77/8bd3e0008ff8d386305351109a7329ea57e51a3ab57bc92340f29c4a5b5d/onnxruntime-1.29.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:16925ef8497e2c07e4b5ae15b504079b3ab3f65e22c58efd10dde0f3caea969a", size = 20803602, upload-time = "2026-08-17T22:53:36.47Z" },
{ url = "https://files.pythonhosted.org/packages/3b/91/a66cd77f28379ede419672edda3184f1eb286db215dce1e7b976fae2d63b/onnxruntime-1.29.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:85f8e8406c52658735fe5c7fbfd3ebaa1ed340768324f6252e4274e374580a23", size = 23113193, upload-time = "2026-08-17T22:53:39.732Z" },
{ url = "https://files.pythonhosted.org/packages/1c/82/2da968405c42340f03de0bcdb63be09ae1004f820b2295590d48951b5cf2/onnxruntime-1.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d4f427afac434b0070fe992b540ddf20a7aff2265f760f314d91331935b6b98", size = 13999253, upload-time = "2026-08-17T22:53:43.184Z" },
{ url = "https://files.pythonhosted.org/packages/95/7a/70c9c893bf732ee66124c2d8de6a21fc9361ec62cf378f857043efcbf0eb/onnxruntime-1.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:4eae472cf7dc3107dec1bb53cd6d142d1964616d08aae48654cd4254b2363c4b", size = 13741410, upload-time = "2026-08-17T22:53:45.521Z" },
{ url = "https://files.pythonhosted.org/packages/d4/80/381c1e9efed9cc32d00aa7cab0547dc84116cec906c3ffe3613686d6963a/onnxruntime-1.29.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3a3814c041251d6a77fdf513fb282056538ee826d2f1178a0df3c549d3fff6ba", size = 21430049, upload-time = "2026-08-17T22:53:48.286Z" },
{ url = "https://files.pythonhosted.org/packages/30/12/4be0e345d38fe707a701ca07e8f63c05b152a2e6285d1e43a7faf63fedd2/onnxruntime-1.29.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d2fb19e848f7c33ed8d3182b52504aaa11c5e8da438bbb47296f85b133cbcf6b", size = 20816870, upload-time = "2026-08-17T22:53:51.169Z" },
{ url = "https://files.pythonhosted.org/packages/96/eb/e6968f5e41aac3125f2ff5708855f09cb0b70d85ed3115b625b0b58305ba/onnxruntime-1.29.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2b80d8c7ec2cc7438e4da3760b88c24568cba72c9ace96d668800a6c79419acb", size = 23136745, upload-time = "2026-08-17T22:53:53.92Z" },
{ url = "https://files.pythonhosted.org/packages/b4/80/5b28f1f1111210fc4a336ddbc6950f468ebf9a6a265420568f4f43fa33ce/onnxruntime-1.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:4acf2b4948b7ede87221ca6332344b8facdc8059d6ac751a7d367d04532b02dd", size = 14001407, upload-time = "2026-08-17T22:53:56.486Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d6/6883f89ea4b044e6e8447ebfaf9bcecdf457b7d80a683635e130b25498e0/onnxruntime-1.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc61a79cb39afd66ab3f01fd2c23591a7f01de89c1668e1fb6315067fc279164", size = 13746981, upload-time = "2026-08-17T22:53:58.977Z" },
{ url = "https://files.pythonhosted.org/packages/41/f8/d375facf60edaf41f5732f9f689c98a800fcc52df5cf6ddfb406703eb5a1/onnxruntime-1.29.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:be0f8ed688cfb1d4d5765a137193b7bfab0c8ea214eed99260b380bb525a3a7f", size = 21429708, upload-time = "2026-08-17T22:54:01.44Z" },
{ url = "https://files.pythonhosted.org/packages/c9/17/b9ad04051a8c4f504852ce0e8e10f9a6b2f1a331eedcdcc503df776dd0ea/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d67673c5367727860922c5262d724472f1b5539fb7ccf4c81a638f9b71719803", size = 20816263, upload-time = "2026-08-17T22:54:04.088Z" },
{ url = "https://files.pythonhosted.org/packages/83/2c/d8eb945d2a372149df9705a8d5c8d7c6c46c987c5446dbcea9e1ea7f6556/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e2128f31f449e922c62dbe5d8b6b7b079f0bcaf2d56a102fa203cb6e5bb5ab19", size = 23136817, upload-time = "2026-08-17T22:54:06.714Z" },
{ url = "https://files.pythonhosted.org/packages/e1/3b/66b424c63fa92dfaa48d1719efaae66fc8c256b9426a832eda51d8dfe1e9/onnxruntime-1.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:2945e1f82f81f27e88decea88c7861f45baea23818950d467bf3909aa303119e", size = 14001310, upload-time = "2026-08-17T22:54:09.13Z" },
{ url = "https://files.pythonhosted.org/packages/83/22/d6a700e3a6322fa3d56fbe7cee9ffc53f35e77ffcd6b7e97f4b7722a27ab/onnxruntime-1.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b940b0d777590c7e20bf298f5c16af1ea6ad1b400a1c822a6be192f64f4d954", size = 13747112, upload-time = "2026-08-17T22:54:11.608Z" },
{ url = "https://files.pythonhosted.org/packages/4a/89/c4af146de3d60a32c89fea48d5d34bfd044faaf8957270043a03bd1b462b/onnxruntime-1.29.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:533f8370ce124304e5cb08ab961836cf755631e3dd77adc5f3bbdab70c2b7d99", size = 20826136, upload-time = "2026-08-17T22:54:14.315Z" },
{ url = "https://files.pythonhosted.org/packages/9d/f2/e6bbacd11dfe8d070613261a758795ea128b9fc9bea391a2a7da2e4c7a08/onnxruntime-1.29.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c1ad3f437153fe77f9d01a08fbaac0beb030e09b8a80ace1603bcf69b6c95481", size = 23138951, upload-time = "2026-08-17T22:54:17.154Z" },
{ url = "https://files.pythonhosted.org/packages/ff/a3/718e1b83096a1bc7b0fc8014c23d4cf795559fe666961cfac4fc038a4871/onnxruntime-1.29.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e74b278af1d949876f5d91d1268fd6c680e79f2bac194967394eaba9fdf69e7e", size = 21431104, upload-time = "2026-08-17T22:54:20.118Z" },
{ url = "https://files.pythonhosted.org/packages/4e/17/c75e78ddc1fe69b6ebaef7fe88ac83f29bfe10955e3a0d2436d93473c91c/onnxruntime-1.29.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:939e5d65f332e6d399774b2bd0d3559fd8fa629c1e77833db29d968d2384f23d", size = 20818488, upload-time = "2026-08-17T22:54:23.147Z" },
{ url = "https://files.pythonhosted.org/packages/65/54/9f197c578d3d3d7bea16971e233e5483981228eec73748585cf7b5933403/onnxruntime-1.29.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6c0c37b92f67ed68dd36221ce0403e1d9bd4f7efce724439978a2597848530e5", size = 23136994, upload-time = "2026-08-17T22:54:26.321Z" },
{ url = "https://files.pythonhosted.org/packages/24/53/4616a55d2495679cfd0195f968feb3d74fe30e26467d168ee243ac97c089/onnxruntime-1.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:4a3129ae56e70d2618ff773920166916310370a7e3cacb60b9e0e8910092725f", size = 14350643, upload-time = "2026-08-17T22:54:28.794Z" },
{ url = "https://files.pythonhosted.org/packages/0a/0f/c338cb5500a522c7e671a3bb1276f4562404fbecce8a0e274565aa968484/onnxruntime-1.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:e417ef8628dcce310d2d53023e750ea298ec14d4341ae6dc3a572bfd9bc7fa97", size = 14124294, upload-time = "2026-08-17T22:54:31.015Z" },
{ url = "https://files.pythonhosted.org/packages/b8/e7/61064289a9a1301b25c1f0f574fe98aba31c2d388db3c1dbec664f78621f/onnxruntime-1.29.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:11264bb58f7b7cf6af835ab10d36838d73680580820fd6f51d90124a1ca8f449", size = 20826174, upload-time = "2026-08-17T22:54:34.283Z" },
{ url = "https://files.pythonhosted.org/packages/60/21/d0c04b561b46e9bff89b5f500fb7415b8ca0669f7902204f76ab06bb0c7e/onnxruntime-1.29.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1ea91cef3b971506e51ae9c37c16d027774ec64994a524ec1bdfb027d68a9832", size = 23138547, upload-time = "2026-08-17T22:54:37.491Z" },
{ url = "https://files.pythonhosted.org/packages/73/b6/e2fa6dfc8c3cd19639d452c3120fb0e02f5176b4e24ecabb24b5362be725/onnxruntime-1.30.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f36c87ec504f4f328e968ee322e4ba9682abe66280b3425625afcbed5333fed7", size = 21533037, upload-time = "2026-09-10T16:31:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/4f/1a/4d197d2676a866f6239cd2d06a95b6db40d47dc29d5bec9fb1493a13dd94/onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bac3aee7b40acd805659fd81a2acd6096312d5b0f014c97ddfe75fb4c1644627", size = 21332697, upload-time = "2026-09-10T16:31:08.118Z" },
{ url = "https://files.pythonhosted.org/packages/4f/2e/4c96278a99140d0307ccda6fd51e6e9d1ca7c9f6fdd1fa96ff375acfc713/onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd54b314ea385bcecac69ab431f020ba503e3878dad4ebb645fec5a24b041242", size = 23561046, upload-time = "2026-09-10T16:31:11.494Z" },
{ url = "https://files.pythonhosted.org/packages/66/68/b8aaade8c1883f569228f4b57201cce922bb7d4af6f977d619520a733430/onnxruntime-1.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:0edd0145a6e3fce8a1276491dc784d615e3c58bcb952c9b4e5c876d5c6a12ad7", size = 14309136, upload-time = "2026-09-10T16:31:14.181Z" },
{ url = "https://files.pythonhosted.org/packages/96/f1/fbda6fb08ac4db6c5c6f7c3e128e4d8fb398b52fe2fc3282c45cadedd18b/onnxruntime-1.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:16506cfa5d218850f2b46e0edce1b952121f2f84f8e0074dafe15f4787f0c8fb", size = 14167408, upload-time = "2026-09-10T16:31:16.505Z" },
{ url = "https://files.pythonhosted.org/packages/31/6f/48169f2e62b405bff5053cbd1d73fb5ce41ef7ecd13bb3bfcc191e689b8a/onnxruntime-1.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd", size = 21544867, upload-time = "2026-09-10T16:31:19.876Z" },
{ url = "https://files.pythonhosted.org/packages/16/bd/cbc5b8f91963689fdd622f463508c01d0aa95d3f944747b1e0b1eb2160b8/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83", size = 21345202, upload-time = "2026-09-10T16:31:23.361Z" },
{ url = "https://files.pythonhosted.org/packages/34/35/e7f862dbacbc99fadd9b14a614e49c99bf0f35fd9927a82f096e3de33531/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328", size = 23585654, upload-time = "2026-09-10T16:31:26.65Z" },
{ url = "https://files.pythonhosted.org/packages/a6/13/0f1699f6de549c9324bc9112a2a85b14c517904cd11b562a654643b755a1/onnxruntime-1.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38", size = 14311470, upload-time = "2026-09-10T16:31:31.273Z" },
{ url = "https://files.pythonhosted.org/packages/a2/17/02b13e5f51461f0453b18ab854e2d0bc1b6ec353241b05a2ee6b79e35d87/onnxruntime-1.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262", size = 14175072, upload-time = "2026-09-10T16:31:34.541Z" },
{ url = "https://files.pythonhosted.org/packages/f0/75/508454c5d01f31641dabc597fe559594c931a520a2673031179319d0afd8/onnxruntime-1.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134", size = 21544990, upload-time = "2026-09-10T16:31:37.685Z" },
{ url = "https://files.pythonhosted.org/packages/89/06/e603c71f43f4fe3fd156a053af79cbed6e27a2c649f0988a67d97fedd39f/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9", size = 21344996, upload-time = "2026-09-10T16:31:40.915Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a1/ede48ab5dc54907a2999362777f541e132639fb06628ded1932058aa8a36/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f", size = 23585560, upload-time = "2026-09-10T16:31:44.941Z" },
{ url = "https://files.pythonhosted.org/packages/3c/dd/c57c529dbc6dd55eca24b12cfbeab1b6a690de72083824eca689085f55b0/onnxruntime-1.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93", size = 14311378, upload-time = "2026-09-10T16:31:48.034Z" },
{ url = "https://files.pythonhosted.org/packages/11/2f/ef00b45b911e7f2115273a24ed1b43e73c8c6c9ec17f9bfa13f8492581f8/onnxruntime-1.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f", size = 14175134, upload-time = "2026-09-10T16:31:50.799Z" },
{ url = "https://files.pythonhosted.org/packages/92/0a/284fd6fe701c9a8aff39dbc119c37582ca40e9cff1ea05425a7cc8606a02/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e", size = 21349453, upload-time = "2026-09-10T16:31:53.898Z" },
{ url = "https://files.pythonhosted.org/packages/d6/72/4f4466f8fa1ec267a9ef5e2f3bd175c203d3da0facbdf4049dc93abbe91c/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca", size = 23579837, upload-time = "2026-09-10T16:31:56.82Z" },
{ url = "https://files.pythonhosted.org/packages/6a/03/05c9a9234688757d2876ddecf80bba908561ee11debf125bc1a427ae6f48/onnxruntime-1.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547", size = 21545313, upload-time = "2026-09-10T16:31:59.654Z" },
{ url = "https://files.pythonhosted.org/packages/c6/bc/1069e58b24779ba9d2fd479db5ecb3a15a6f49b585107c898819c0789558/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175", size = 21346830, upload-time = "2026-09-10T16:32:02.275Z" },
{ url = "https://files.pythonhosted.org/packages/f1/38/8138eed225c5bc6ddfc05879ecac7dacc63c34b9b6f99be72839c1f6dc49/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec", size = 23586333, upload-time = "2026-09-10T16:32:04.996Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0a/748b86000fbf9518b5dfc5bdc1b924eeaca976203740fd9d30649e568ee6/onnxruntime-1.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4", size = 14675319, upload-time = "2026-09-10T16:32:07.5Z" },
{ url = "https://files.pythonhosted.org/packages/2f/db/db8e4c0cf6f70f1311060560630fd21763648e01dfbba5195a2592c052ef/onnxruntime-1.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d", size = 14569360, upload-time = "2026-09-10T16:32:09.896Z" },
{ url = "https://files.pythonhosted.org/packages/24/0a/ec0a9d656e39b43887c378a5388b20c3b1e1ee43bded84580b0936466694/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294", size = 21348433, upload-time = "2026-09-10T16:32:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/91/f0/40f74b7c00077e1e25627067ed98a70df1fef5c0e21b82849190d312554e/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7", size = 23579340, upload-time = "2026-09-10T16:32:15.532Z" },
]
[[package]]
name = "openai"
version = "3.9.0"
version = "3.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -2106,9 +2106,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
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" }
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/4874c4f7db30121885e6a763e2670b2fe1d76c79ed4732f8acbb51c8d96e/openai-3.13.0.tar.gz", hash = "sha256:a8f87a9b3b9c08eb446d68bd0a80e8ec907c4c35fdea63f4265c7b34b2de3a60", size = 1624217, upload-time = "2026-09-10T19:38:27.597Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/63/7f/ee9ebb7c5ab7abf016969faec7c748a91c1fd893078bfe1e5ba5d82e96c2/openai-3.13.0-py3-none-any.whl", hash = "sha256:e35b1f6fe99245e86e37504d9fad1ad2a363807307c424232c1d849bd0666c8e", size = 2009683, upload-time = "2026-09-10T19:38:25.855Z" },
]
[[package]]
@@ -2146,7 +2146,7 @@ resolution-markers = [
"(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" } },
{ name = "numpy", version = "1.26.4", 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 = [
@@ -2175,7 +2175,7 @@ resolution-markers = [
"(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" } },
{ 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 = [
@@ -2206,7 +2206,7 @@ resolution-markers = [
"(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" } },
{ name = "numpy", version = "1.26.4", 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 = [
@@ -2235,7 +2235,7 @@ resolution-markers = [
"(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" } },
{ 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 = [
@@ -3009,15 +3009,15 @@ wheels = [
[[package]]
name = "pyright"
version = "1.1.411"
version = "1.1.414"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" },
]
[[package]]
@@ -3179,14 +3179,14 @@ name = "pytorch-lightning"
version = "2.6.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fsspec", extra = ["http"] },
{ name = "lightning-utilities" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "torch" },
{ name = "torchmetrics" },
{ name = "tqdm" },
{ name = "typing-extensions" },
{ name = "fsspec", extra = ["http"], marker = "python_full_version < '3.13'" },
{ name = "lightning-utilities", marker = "python_full_version < '3.13'" },
{ name = "packaging", marker = "python_full_version < '3.13'" },
{ name = "pyyaml", marker = "python_full_version < '3.13'" },
{ name = "torch", marker = "python_full_version < '3.13'" },
{ name = "torchmetrics", marker = "python_full_version < '3.13'" },
{ name = "tqdm", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/e4/5025cb42311b4f8cc6d1b12cd81399ca433159f8e3831ab229c5a6c5177e/pytorch_lightning-2.6.6.tar.gz", hash = "sha256:52c52b20522dd3d3feb3bb3aae2dbe3f291e8e836d4111a74cfd72e9df018834", size = 662507, upload-time = "2026-09-10T09:41:16.684Z" }
wheels = [
@@ -3205,7 +3205,7 @@ resolution-markers = [
"(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" } },
{ 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 = [
@@ -3281,7 +3281,7 @@ resolution-markers = [
"(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 = "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" }
@@ -3449,7 +3449,7 @@ wheels = [
[[package]]
name = "remove-ai-watermarks"
version = "0.40.6"
version = "0.41.1"
source = { editable = "." }
dependencies = [
{ name = "c2pa-python" },
@@ -3584,6 +3584,13 @@ qwen-zimage = [
{ name = "torchvision" },
{ name = "transformers" },
]
source-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", 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'" },
]
text-draft = [
{ name = "huggingface-hub" },
{ name = "paddleocr" },
@@ -3637,6 +3644,7 @@ requires-dist = [
{ name = "huggingface-hub", marker = "extra == 'dev'", specifier = ">=0.20.0" },
{ name = "huggingface-hub", marker = "extra == 'lama'", specifier = ">=0.20.0" },
{ name = "huggingface-hub", marker = "extra == 'migan'", specifier = ">=0.20.0" },
{ name = "huggingface-hub", marker = "extra == 'source-classify'", specifier = ">=0.20.0" },
{ name = "huggingface-hub", marker = "extra == 'text-draft'", specifier = ">=0.20.0" },
{ name = "invisible-watermark", marker = "extra == 'dev'", specifier = ">=0.2.0" },
{ name = "numpy", marker = "python_full_version >= '3.13' and extra == 'pixels'", specifier = ">=2" },
@@ -3665,10 +3673,11 @@ requires-dist = [
{ name = "remove-ai-watermarks", extras = ["pixels"], marker = "extra == 'classify'" },
{ name = "remove-ai-watermarks", extras = ["pixels"], marker = "extra == 'detect'" },
{ name = "remove-ai-watermarks", extras = ["pixels"], marker = "extra == 'diffusion'" },
{ name = "remove-ai-watermarks", extras = ["pixels"], marker = "extra == 'source-classify'" },
{ name = "remove-ai-watermarks", extras = ["pixels"], marker = "extra == 'visible'" },
{ name = "remove-ai-watermarks", extras = ["qwen-zimage", "lama"], marker = "extra == 'text-restoration'" },
{ name = "remove-ai-watermarks", extras = ["video"], marker = "extra == 'dev'" },
{ name = "remove-ai-watermarks", extras = ["video", "heif", "detect", "trustmark", "text-restoration", "migan", "classify"], marker = "extra == 'all'" },
{ name = "remove-ai-watermarks", extras = ["video", "heif", "detect", "trustmark", "text-restoration", "migan", "classify", "source-classify"], marker = "extra == 'all'" },
{ name = "remove-ai-watermarks", extras = ["visible"], marker = "extra == 'lama'" },
{ name = "remove-ai-watermarks", extras = ["visible"], marker = "extra == 'migan'" },
{ name = "remove-ai-watermarks", extras = ["visible"], marker = "extra == 'video'" },
@@ -3684,7 +3693,7 @@ requires-dist = [
{ name = "transformers", marker = "extra == 'diffusion'", specifier = ">=5,<6" },
{ name = "trustmark", marker = "python_full_version < '3.13' and extra == 'trustmark'", specifier = ">=0.8.0" },
]
provides-extras = ["pixels", "heif", "visible", "video", "detect", "diffusion", "qwen-zimage", "text-restoration", "text-draft", "trustmark", "classify", "classify-onnx", "lama", "migan", "dev", "all"]
provides-extras = ["pixels", "heif", "visible", "video", "detect", "diffusion", "qwen-zimage", "text-restoration", "text-draft", "trustmark", "classify", "classify-onnx", "source-classify", "lama", "migan", "dev", "all"]
[[package]]
name = "requests"
@@ -3725,27 +3734,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.6"
version = "0.16.7"
source = { registry = "https://pypi.org/simple" }
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" }
sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" },
{ url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" },
{ url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" },
{ url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" },
{ url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" },
{ url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" },
{ url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" },
{ url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" },
{ url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" },
{ url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" },
{ url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" },
{ url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" },
{ url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" },
{ url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" },
]
[[package]]
@@ -4067,10 +4076,10 @@ name = "torchmetrics"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lightning-utilities" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging" },
{ name = "torch" },
{ name = "lightning-utilities", marker = "python_full_version < '3.13'" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
{ name = "packaging", marker = "python_full_version < '3.13'" },
{ name = "torch", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/34/39b8b749333db56c0585d7a11fa62a283c087bb1dfc897d69fb8cedbefb1/torchmetrics-1.9.0.tar.gz", hash = "sha256:a488609948600df52d3db4fcdab02e62aab2a85ef34da67037dc3e65b8512faa", size = 581765, upload-time = "2026-03-09T17:41:22.443Z" }
wheels = [
@@ -4165,13 +4174,13 @@ name = "trustmark"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "einops" },
{ name = "lightning" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } },
{ name = "omegaconf" },
{ name = "six" },
{ name = "torch" },
{ name = "torchvision" },
{ name = "einops", marker = "python_full_version < '3.13'" },
{ name = "lightning", marker = "python_full_version < '3.13'" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
{ name = "omegaconf", marker = "python_full_version < '3.13'" },
{ name = "six", marker = "python_full_version < '3.13'" },
{ name = "torch", marker = "python_full_version < '3.13'" },
{ name = "torchvision", marker = "python_full_version < '3.13'" },
]
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" }