Compare commits

..
7 Commits
Author SHA1 Message Date
Victor Kuznetsov dcc1bf5e07 Emit the Content Seal as its own signal on standalone-AI-tag files
The strength router already bets that a file whose only provenance is
the standalone AI digital-source tag is Meta Muse output (C2PA issuers
win first), and Muse stamps every output with the invisible Content
Seal. identify now surfaces that same bet as evidence: the additive
content_seal signal (medium confidence - an attribution, not a decode,
since no public decoder exists) plus the watermark string, emitted on
exactly the standalone_iptc condition that routes the cohort. Clients
select pixel removal from the signal list instead of parsing caveats,
the way InvisMark is additive over soft_binding. The API's invisible
gate already runs on ai_from_metadata, so all/invisible behavior is
unchanged; has_invisible_target needs no edit.
2026-08-27 16:25:11 -07:00
Victor Kuznetsov d8fcd0f79b Stop double-counting a named forensic mark as SynthID provenance
A manifest that names its own forensic soft-binding algorithm carries
that vendor's mark; the generic watermark-action vendor-token inference
must not add a second, differently-attributed invisible watermark from
the same bytes. Microsoft Designer manifests triggered exactly that:
signed by Microsoft, watermarked by InvisMark, with the generation
agent named "Azure OpenAI ImageGen" - the OpenAI issuer token inside
that service name plus the InvisMark watermarked action satisfied the
OpenAI SynthID-evidence rule, and identify reported one forensic mark
as two paid pixel watermarks.

Three changes, one rule at every inference site (the verdict-scan
comment's own lesson: a rule that lives in only one copy is a rule the
others silently lack):

- c2pa.py structured path: SynthID evidence now scopes to the
  signer/generator identity strings only (signature issuer, claim
  generator), never the raw chain, and is suppressed entirely when a
  soft-binding algorithm is named.
- c2pa.py byte fallback and metadata.py synthid_source: suppressed when
  the scan names a soft-binding algorithm.
- identify.py verdict scan: same suppression.

Gemini and ChatGPT originals keep their provenance-asserted SynthID
strings; the Designer regression is pinned by
test_designer_synthid_suppression.py (agent name alone is not the
vendor's provenance, and a named soft binding suppresses the
inference).
2026-08-27 15:20:45 -07:00
Victor Kuznetsov 0a3b227f2c Merge pull request #87 from wiltodelta/dependabot/uv/minor-and-patch-c54089dda1
Bump the minor-and-patch group with 5 updates
2026-08-27 11:50:53 -07:00
Victor Kuznetsov f91f9fd9fc Record the public Microsoft Content Provenance check URL
The InvisMark strength ladder was measured against Microsoft's public
Content Provenance page, but no doc or comment carried its URL - the
API how-to link was the only address recorded anywhere, and the page
is what a human can actually check without an Azure account. Record
https://ai.azure.com/nextgen/validate in supported-signals.md, the
watermarking landscape, and the strength derivation comment, with the
honest caveat that its collapsed verdict tops out at Inconclusive
rather than the API's separate watermark-negative result.
2026-08-27 11:21:08 -07:00
Victor KuznetsovandClaude Fable 4.5 c4aecfbbdf Release 0.32.0
Co-Authored-By: Claude Fable 4.5 <noreply@anthropic.com>
2026-08-26 23:03:04 -07:00
Victor KuznetsovandClaude Fable 4.5 ab528ec0e8 Ship the measured Meta Content Seal cohort with auto routing and --vendor override
Full Meta Muse Image support in the invisible-removal path:

- QWEN_ZIMAGE_META_STRENGTH = 0.1: derived by the standard
  worst-boundary-plus-cross-source-spread method over five oracle-bracketed
  generations (data/contentseal/manifest.csv)
- Auto mode: vendor_for_strength routes a file whose only provenance is the
  standalone AI IPTC trainedAlgorithmicMedia tag onto the meta cohort; C2PA
  issuers win first, so Google/OpenAI/Microsoft routing is unchanged. Muse
  WebP outputs place the XMP in a tail chunk, so the scan uses the shared
  chunk-aware metadata.scan_head rather than a plain head read
- Explicit override: --vendor on invisible/all/batch and
  InvisibleOptions.vendor name the cohort on stripped files; naming a cohort
  asserts the watermark is present, so the no-signal gate treats it like
  --force at both the CLI and API seams
- sdxl-zimage has no measured Meta rung: an explicit meta vendor falls to
  the conservative unknown 0.25 rather than inventing one
- identify emits a Content Seal caveat pointing at the removal path
- The legacy visible 'Imagined with AI' mark stays unregistered: a dedicated
  sample hunt (newsroom mockups, community posts, press screenshots, dead
  imagine.meta.com, broken Wayback captures) found no pixel-verifiable
  capture, and the registry rule forbids encoding a corner without one.
  erase --region remains its removal path; outcome recorded in the landscape

Co-Authored-By: Claude Fable 4.5 <noreply@anthropic.com>
2026-08-26 22:59:23 -07:00
dependabot[bot] 2810b53585 Bump the minor-and-patch group with 5 updates
Bumps the minor-and-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [diffusers](https://github.com/huggingface/diffusers) | `0.39.0` | `0.40.0` |
| [transformers](https://github.com/huggingface/transformers) | `5.15.0` | `5.15.1` |
| [huggingface-hub](https://github.com/huggingface/huggingface_hub) | `1.27.0` | `1.28.0` |
| [onnxruntime](https://github.com/microsoft/onnxruntime) | `1.28.0` | `1.29.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.16.3` | `0.16.4` |


Updates `diffusers` from 0.39.0 to 0.40.0
- [Release notes](https://github.com/huggingface/diffusers/releases)
- [Commits](https://github.com/huggingface/diffusers/compare/v0.39.0...v0.40.0)

Updates `transformers` from 5.15.0 to 5.15.1
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.15.0...v5.15.1)

Updates `huggingface-hub` from 1.27.0 to 1.28.0
- [Release notes](https://github.com/huggingface/huggingface_hub/releases)
- [Commits](https://github.com/huggingface/huggingface_hub/compare/v1.27.0...v1.28.0)

Updates `onnxruntime` from 1.28.0 to 1.29.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.28.0...v1.29.0)

Updates `ruff` from 0.16.3 to 0.16.4
- [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.3...0.16.4)

---
updated-dependencies:
- dependency-name: diffusers
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: transformers
  dependency-version: 5.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: huggingface-hub
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: onnxruntime
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: ruff
  dependency-version: 0.16.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 08:48:21 +00:00
19 changed files with 514 additions and 115 deletions

No files matched your search

+3 -4
View File
@@ -78,10 +78,9 @@ Measured (2026-08-26/27, oracle `meta.ai/identification`):
between easiest and hardest).
- Derived Meta floor by the existing worst-boundary-plus-cross-source-spread
method: 0.06 + (0.0525 - 0.015) = 0.0975, rounded to **0.1**.
- Not shipped as a constant: no provenance signal routes Muse outputs onto a
vendor cohort, so the default curve stays authoritative and 0.1 is recorded
as the floor to encode if an explicit Meta override is ever added.
- Shipped as `QWEN_ZIMAGE_META_STRENGTH`: auto mode routes standalone-AI-IPTC
files onto the cohort, and `--vendor meta` / `InvisibleOptions.vendor`
names it explicitly on stripped files (implying the scrub runs).
## Regeneration
API key is not stored in this repository. Regenerate with the script pattern
+17
View File
@@ -387,6 +387,23 @@ detected. Use `--force` when you know the image should be processed:
remove-ai-watermarks invisible image.png -o clean.png --force
```
### Choose a strength cohort
`--vendor` selects the cohort the default strength resolves from. `auto`
(the default) derives it from provenance: the C2PA issuer for
OpenAI/Google/Microsoft, and the standalone AI IPTC tag for Meta Content Seal
(Muse output carries no C2PA; the tag is a standard code, so C2PA evidence
always wins first). An explicit value both names the cohort for stripped files
and implies the scrub runs -- naming the cohort asserts the pixel watermark is
present -- exactly like `--force` plus a measured floor:
```bash
remove-ai-watermarks invisible muse_output.webp -o clean.png --vendor meta
```
The same option exists on `all` and `batch`, and as
`InvisibleOptions(vendor="meta")` in the Python API.
### Choose a pipeline
| Pipeline | When to use it |
+16 -12
View File
@@ -1170,18 +1170,22 @@ unreachable constant would be dead code.
Full spread: worst first-clean boundary (0.0525, 0.06] on lighthouse, easiest
source already clean at 0.015. Following the same derivation as the OpenAI and
Microsoft floors (worst clean boundary plus one full observed cross-source
spread): 0.06 + (0.0525 - 0.015) = 0.0975, rounded up to **0.1**. This is a
candidate `meta` floor, not yet a shipped constant: Muse outputs carry no C2PA,
so `vendor_for_strength` has no signal that could route them onto the flat
vendor map automatically. Encoding 0.1 as `QWEN_ZIMAGE_META_STRENGTH` makes
sense only once the tool gains an explicit way to opt into a Meta cohort (for
example a CLI `--vendor meta` override); the default resolution-adaptive curve
(~0.1305 at 2.56 MP) already clears every measured source, so default behavior
needs no change. Oracle verdicts carry a per-generation ID and creation timestamp
embedded in the watermark payload; both survived the 512 px resize and JPEG q85
rows, so payload recovery outlives the detection threshold. Oracle session
limits are per-IP, server-side, and sliding-window: clearing cookies and storage
does not reset them, and a burst exhausts the window minutes after it reopens.
spread): 0.06 + (0.0525 - 0.015) = 0.0975, rounded up to **0.1**. Shipped as
`QWEN_ZIMAGE_META_STRENGTH` with two routing paths: auto mode routes a file
whose only provenance is the standalone AI IPTC tag onto the cohort
(`vendor_for_strength` checks C2PA issuers first, so Google/OpenAI/Microsoft
evidence always wins, and the tag's other users ship no invisible watermark
this profile targets), and `--vendor meta` / `InvisibleOptions.vendor` names the
cohort explicitly on stripped files, implying the scrub runs (naming the cohort
asserts the watermark is present). sdxl-zimage has no measured Meta rung and an
explicit meta vendor there falls to the conservative unknown 0.25. The default
resolution-adaptive curve (~0.1305 at 2.56 MP) also clears every measured
source, so default behavior needed no change. Oracle verdicts carry a
generation ID and creation timestamp embedded in the watermark payload; both
survived the 512 px resize and JPEG q85 rows, so payload recovery outlives the
detection threshold. Oracle session limits are per-IP, server-side, and
sliding-window: clearing cookies and storage does not reset them, and a burst
exhausts the window minutes after it reopens.
### Static prompt embeddings
+12
View File
@@ -112,6 +112,18 @@ raiw.remove_all(
)
```
`vendor="meta"` names a strength cohort explicitly (the measured Content Seal
floor) for a stripped file whose provenance no longer carries the AI IPTC tag,
and implies the scrub runs:
```python
raiw.remove_all(
"muse_output.webp",
"clean.png",
invisible=InvisibleOptions(vendor="meta"),
)
```
`InvisibleOptions` carries only what `InvisibleEngine` itself takes, and uses the
engine's own parameter names and defaults. `force`, which decides whether the
engine runs at all, is a parameter of `remove_all` and `remove_batch` alongside
+20 -5
View File
@@ -152,7 +152,11 @@ inspection cannot independently verify the output. Microsoft's official
[Content Provenance Detection API](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/how-to/how-to-provenance-detection)
is the external oracle: it reports pixel `Watermark` and embedded `C2PA` results
separately; a control-positive, output-negative pair is the available per-file
verification path.
verification path. The API needs Azure credentials; the page a human can check
without an account is <https://ai.azure.com/nextgen/validate>. Its verdict is
weaker than the API's: it collapses watermark and C2PA into one rendered result
and tops out at `Inconclusive` rather than a watermark-negative, so treat
`Inconclusive` on a processed file as "not confirmed", not as "detected still".
Meta Muse Image stamps every output with Content Seal, a proprietary invisible
pixel watermark, and ships no visible mark (the legacy `Imagined with AI`
@@ -161,7 +165,12 @@ This project has no local Content Seal decoder. Meta Model API outputs and
Meta CDN copies carry an XMP `iptcExt:DigitalSourceType =
trainedAlgorithmicMedia` companion tag, which `identify` reports through the
existing Made-with-AI path; that IPTC code is a standard, not a Meta-exclusive
signal, so it cannot key a strength cohort the way the C2PA issuer does. The
signal, so it cannot key a strength cohort the way the C2PA issuer does. Since
0.33.0 a standalone AI digital-source tag (no C2PA manifest) additionally emits
the additive `content_seal` signal - the strength router's Meta bet as evidence,
medium confidence, with the same caveat - so clients select pixel removal from
the signal list exactly the way InvisMark is additive over `soft_binding`. It
is an attribution, never a decode. The
external oracle is `https://meta.ai/identification`: anonymous, no login,
accepts image, video, and audio, enforces an unspecified daily identification
limit, and answers with model attribution (`Muse Image 1 - Meta`) plus a
@@ -172,9 +181,15 @@ documentation never mentions the seal. The
default `qwen-zimage` profile clears Content Seal at the default
resolution-adaptive strength (oracle-verified on 2.56 MP generations); measured
strength boundaries are recorded in `data/contentseal/manifest.csv` and
[module internals](module-internals.md#meta-content-seal-boundaries-for-qwen-zimage)
(derived Meta floor 0.1 by the standard spread method, not shipped as a constant
because no provenance signal can route Muse output onto a vendor cohort).
[module internals](module-internals.md#meta-content-seal-boundaries-for-qwen-zimage).
The derived Meta floor (0.1 by the standard spread method) ships as a measured
cohort: auto mode routes a file whose only provenance is the standalone AI IPTC
tag to it (the tag is not Meta-exclusive; other tag users ship no invisible
watermark this profile targets, and Google/OpenAI/Microsoft C2PA evidence
always wins first), and `invisible --vendor meta` (also `all` and `batch`, and
`InvisibleOptions.vendor` in the API) names the cohort explicitly on stripped
files. An explicit vendor implies the scrub runs: naming the cohort asserts the
pixel watermark is present.
The seal survives resizing, JPEG recompression, and metadata stripping; it dies
to center crops of a third to a half, matching the Reuters 2026-07-11 finding
that Meta's detector missed 55% of cropped Muse images.
+3 -3
View File
@@ -66,7 +66,7 @@ payloads. Removal remuxes either container through ffmpeg with stream copy.
encoder and the carrier remains decodable.
- **Invisible but NOT locally detectable (proprietary, API/oracle only — same wall as SynthID):** Amazon Titan Image Generator + Nova Canvas (Bedrock `DetectGeneratedContent` API), Kakao (new SynthID image adopter, May 2026), NVIDIA Cosmos (SynthID video). No local detector possible; treat like SynthID.
- **C2PA 2.4 "Durable Content Credentials" (April 2026; verified against the spec) raise the bar for metadata stripping.** 2.4 defines soft bindings (an invisible watermark or a content fingerprint) plus a server-side manifest repository and a new `c2pa.repository-receipt` assertion. Per the spec: "if a C2PA manifest is removed from an asset, but a copy of that manifest remains in a provenance store elsewhere, the manifest and asset may be matched using available soft bindings." So our local `metadata --remove` deletes the *embedded* manifest, but a fingerprint/watermark soft binding can still re-link the image to its manifest in a repository server-side. Stripping the file is becoming necessary-but-not-sufficient against durable provenance. (Our parsers target the stable embedded-manifest format documented in C2PA 2.1 §11; that format is unchanged in 2.4 -- the new pieces are repository/soft-binding infra, not the on-file box layout, so no parser change is implied.) Spec: https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html We now READ the soft-binding `alg` (`C2PA_SOFT_BINDINGS` / `soft_binding_vendors_in`) to name the forensic-watermark vendor, preserve the structured assertion's signed `value`, and locally DECODE the one open scheme, Adobe TrustMark (`trustmark_detector`); the rest stay name-only unless a compatible decoder is independently verified.
- **Microsoft Paint and Photos InvisMark (reverse-engineered 2026-08-20):** Paint receives a per-generation GUID from remote prompt moderation, embeds it into locally generated pixels, and records the same value in `c2pa.soft-binding` under `com.microsoft.invismark.1`. The C2PA soft-binding registry independently identifies that algorithm as Microsoft Responsible AI InvisMark for image and video. Paint's 144-bit writer framing does not match the public repository's 100-bit pretrained checkpoint interface, so compatibility is not assumed. The parser reports the signed identifier; there is no validated local pixel decoder. Microsoft's external Content Provenance Detection API is the removal oracle because it reports `Watermark` separately from `C2PA`; a metadata-stripped, pixel-identical control must remain watermark-positive before an output-negative result is attributed to pixel removal. Sources: https://xusheng.dev/posts/reversing/mspaint_invisible_watermark/main/, https://github.com/c2pa-org/softbinding-algorithm-list/blob/main/softbinding-algorithm-list.json, and https://learn.microsoft.com/en-us/azure/ai-services/content-safety/how-to/how-to-provenance-detection
- **Microsoft Paint and Photos InvisMark (reverse-engineered 2026-08-20):** Paint receives a per-generation GUID from remote prompt moderation, embeds it into locally generated pixels, and records the same value in `c2pa.soft-binding` under `com.microsoft.invismark.1`. The C2PA soft-binding registry independently identifies that algorithm as Microsoft Responsible AI InvisMark for image and video. Paint's 144-bit writer framing does not match the public repository's 100-bit pretrained checkpoint interface, so compatibility is not assumed. The parser reports the signed identifier; there is no validated local pixel decoder. Microsoft's external Content Provenance Detection API is the removal oracle because it reports `Watermark` separately from `C2PA`; a metadata-stripped, pixel-identical control must remain watermark-positive before an output-negative result is attributed to pixel removal. The no-account web page for the same check is <https://ai.azure.com/nextgen/validate> (its `Inconclusive` verdict is weaker than the API's separate watermark result). Sources: https://xusheng.dev/posts/reversing/mspaint_invisible_watermark/main/, https://github.com/c2pa-org/softbinding-algorithm-list/blob/main/softbinding-algorithm-list.json, and https://learn.microsoft.com/en-us/azure/ai-services/content-safety/how-to/how-to-provenance-detection
- **Built in the dated batch:** soft-binding vendor detection, IPTC Photo
Metadata AI-disclosure fields, C2PA detection and stripping for supported
ISOBMFF video, the optional Adobe TrustMark decoder, and temporally stabilized
@@ -161,9 +161,9 @@ Two consequences we can exploit: (1) the 5% floor is a **scale prior** -- a comp
**Tencent Yuanbao: RESOLVED 2026-07-25, registered (`yuanbao_engine.py`).** The standard mark is a compact two-line italic `元宝` over `AI生成` block at bottom-right. It switches between light and dark strokes with the scene, so detection uses polarity-independent local contrast rather than a white top-hat. The separate one-line overlay variant remains evidence-limited to one example.
**Meta Muse Image / Content Seal: current state VERIFIED 2026-08-26 against live artifacts.** Muse Image (launched 2026-07-07, first image model from Meta Superintelligence Labs) puts no visible mark on output. Every output carries Content Seal, a proprietary invisible pixel watermark; research lineage is open (`github.com/facebookresearch/content-seal`, Pixel Seal / VideoSeal / Watermark Anything), but the deployed implementation is proprietary and unpublished. API outputs and Meta CDN copies also carry XMP `iptcExt:DigitalSourceType = trainedAlgorithmicMedia` (a standard IPTC code, not Meta-exclusive). The only reader is the anonymous web oracle `meta.ai/identification`, which returns model attribution plus a per-generation ID and timestamp embedded in the payload. Verified robustness (our corpus, `data/contentseal/`): seal survives CDN WebP transcode, 512 px resize, JPEG q85, and full metadata stripping; it is lost to center crops of 33-50% linear size (Reuters measured 55% detector misses on cropped images, 2026-07-11). The default `qwen-zimage` profile clears it (oracle-verified). Legacy paragraph retained below.
**Meta Muse Image / Content Seal: current state VERIFIED 2026-08-26 against live artifacts.** Muse Image (launched 2026-07-07, first image model from Meta Superintelligence Labs) puts no visible mark on output. Every output carries Content Seal, a proprietary invisible pixel watermark; research lineage is open (`github.com/facebookresearch/content-seal`, Pixel Seal / VideoSeal / Watermark Anything), but the deployed implementation is proprietary and unpublished. API outputs and Meta CDN copies also carry XMP `iptcExt:DigitalSourceType = trainedAlgorithmicMedia` (a standard IPTC code, not Meta-exclusive). The only reader is the anonymous web oracle `meta.ai/identification`, which returns model attribution plus a per-generation ID and timestamp embedded in the payload. Verified robustness (our corpus, `data/contentseal/`): seal survives CDN WebP transcode, 512 px resize, JPEG q85, and full metadata stripping; it is lost to center crops of 33-50% linear size (Reuters measured 55% detector misses on cropped images, 2026-07-11). Removal is fully supported and oracle-verified on the default profile, with a measured Meta strength floor (0.1) shipped behind auto IPTC routing and an explicit `--vendor meta` override; see [supported signals](supported-signals.md) and [module internals](module-internals.md). Legacy paragraph retained below.
**Meta `Imagined with AI` (string VERIFIED (a) from Meta's own newsroom; POSITION NOT VERIFIED).** Sources conflict on placement. Do not encode a corner without a verified sample. `identify` reads the supported IPTC disclosure; it does not decode Meta's proprietary invisible watermark. Source: `https://about.fb.com/news/2024/02/labeling-ai-generated-images-on-facebook-instagram-and-threads/`.
**Meta `Imagined with AI` (string VERIFIED (a) from Meta's own newsroom; POSITION NOT VERIFIED).** Sources conflict on placement. Do not encode a corner without a verified sample. A dedicated 2026-08-27 sample hunt failed to obtain one: the Feb-2024 newsroom images are UI mockups whose photos carry no in-pixel mark; community posts (Threads `C8_rS_MuEId` titled "lower left corner", a Facebook share) corroborate bottom-LEFT verbally but their files defeat pixel verification; press screenshots predate the mark (Dec 2023); `imagine.meta.com` is dead (redirects to meta.ai) and its Wayback captures are broken SPA error pages. The mark's generator no longer exists (Muse output has no visible mark), so no fresh sample can be made. Removal stays on the generic `erase --region` path until a legacy capture surfaces. Source: `https://about.fb.com/news/2024/02/labeling-ai-generated-images-on-facebook-instagram-and-threads/`.
**Samsung English/other locales: still not established.** Samsung's own support page says only that "A Galaxy AI watermark will appear on AI-generated images" -- no string, no corner. Every community thread carrying the exact English string returned HTTP 403 to WebFetch, so the search paraphrase (bottom-left) is deliberately NOT recorded as fact. Feature-tier detail (b): the mark is applied by Generative Edit / sketch-to-image but reportedly NOT by Object Eraser, so Samsung absence is feature-dependent. The four-star icon variant: nothing found.
+1 -1
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.31.2"
version = "0.33.0"
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
readme = "README.md"
requires-python = ">=3.11,<3.15"
+1 -1
View File
@@ -32,7 +32,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
__version__ = "0.31.2"
__version__ = "0.33.0"
__all__ = [
"BatchSummary",
+21 -4
View File
@@ -426,6 +426,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
source_types: list[str] = []
soft_binding_algorithms: list[str] = []
soft_binding_values: list[str] = []
# Raw signer/generator identity strings, used to scope SynthID evidence to
# the vendor that actually asserted the manifest. A vendor token appearing
# anywhere else in the chain (e.g. Microsoft Designer's "Azure OpenAI
# ImageGen" softwareAgent) is a service name, not that vendor's provenance.
identity_strings: list[str] = []
claim_generator_asserts_ai = False
def add_tool_matches(value: str, *, asserts_ai: bool = False) -> None:
@@ -443,9 +448,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
value = signature.get(key)
if isinstance(value, str):
issuers.extend(_ordered_matches(value.encode(), C2PA_ISSUERS))
identity_strings.append(value)
direct_generator = manifest.get("claim_generator")
if isinstance(direct_generator, str):
identity_strings.append(direct_generator)
add_tool_matches(direct_generator, asserts_ai=True)
candidates = manifest.get("claim_generator_info")
@@ -456,6 +463,7 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
name = cast("dict[object, object]", candidate_value).get("name")
if isinstance(name, str):
add_tool_matches(name, asserts_ai=True)
identity_strings.append(name)
assertions = manifest.get("assertions")
if not isinstance(assertions, list):
@@ -528,9 +536,14 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
has_watermark_action = any(action.startswith("watermarked") for action in actions)
if has_watermark_action:
info["watermarked"] = True
if info.get("ai_source_kind"):
selected_bytes = json.dumps(chain, ensure_ascii=False).encode()
synthid = synthid_evidence_vendors_in(selected_bytes, has_watermark_action=has_watermark_action)
if info.get("ai_source_kind") and not soft_binding_algorithms:
# Evidence scope: only the signer/generator identity strings above, never
# the whole chain - a vendor named inside another vendor's manifest (the
# Designer case) must not turn into that vendor's SynthID provenance. A
# manifest that names its own forensic soft-binding algorithm carries
# that vendor's mark and is excluded from the generic inference entirely.
identity_bytes = json.dumps(identity_strings, ensure_ascii=False).encode()
synthid = synthid_evidence_vendors_in(identity_bytes, has_watermark_action=has_watermark_action)
if synthid:
info["synthid_vendors"] = synthid
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
@@ -611,7 +624,11 @@ def _populate_registry_fields(buffer: bytes, info: dict[str, Any]) -> bool:
if b"c2pa.watermarked" in buffer:
info["watermarked"] = True
synthid = synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
synthid = (
[]
if soft_binding_vendors_in(buffer)
else synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
)
if ai_source and synthid:
info["synthid_vendors"] = synthid
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
@@ -79,17 +79,34 @@ QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.27
# cross-source spread (0.00725) to the worst clean boundary: 0.0695 + 0.00725.
QWEN_ZIMAGE_OPENAI_STRENGTH = 0.07675
# Microsoft's public detector returned Inconclusive rather than an API-level
# watermark-negative verdict. Three valid Paint sources first cleared at 0.04125,
# Microsoft's public detector (https://ai.azure.com/nextgen/validate) returned
# Inconclusive rather than an API-level watermark-negative verdict. Three valid
# Paint sources first cleared at 0.04125,
# 0.055, and 0.095. Add one full observed cross-source spread to the worst clean
# boundary: 0.095 + (0.095 - 0.04125) = 0.14875, rounded up to 0.15. This is a
# measured corpus margin, not a universal InvisMark threshold.
QWEN_ZIMAGE_MICROSOFT_STRENGTH = 0.15
# Meta Muse Image stamps every output with Content Seal, but no provenance signal
# survives to route it: the outputs carry no C2PA, and their IPTC
# trainedAlgorithmicMedia companion tag is a standard code many platforms use, so
# it cannot key this cohort the way an issuer keys the others. The floor is
# therefore selected by an explicit --vendor meta override, never by detection.
# Derivation (oracle meta.ai/identification, 2026-08-26/27, corpus in
# data/contentseal/): five independent 2.56 MP generations bracketed at
# lighthouse (0.0525, 0.06], fox (0.03, 0.0375], night_city (0.03, 0.0375],
# mug <= 0.03, text <= 0.015. Worst clean boundary plus one full observed
# cross-source spread: 0.06 + (0.0525 - 0.015) = 0.0975, rounded up to 0.1.
# sdxl-zimage has no measured Meta floor; its vendor map stays without a meta
# entry so an explicit --vendor meta there falls to the unknown 0.25, which is
# above this floor and therefore conservative.
QWEN_ZIMAGE_META_STRENGTH = 0.1
_QWEN_ZIMAGE_FLAT_STRENGTH_BY_VENDOR: dict[str, float] = {
"google": QWEN_ZIMAGE_GOOGLE_STRENGTH,
"openai": QWEN_ZIMAGE_OPENAI_STRENGTH,
"microsoft": QWEN_ZIMAGE_MICROSOFT_STRENGTH,
"meta": QWEN_ZIMAGE_META_STRENGTH,
}
@@ -129,7 +146,8 @@ def strength_default_help() -> str:
return (
"profile-adaptive (qwen-zimage uses resolution-adaptive denoise, with a "
f"flat OpenAI {QWEN_ZIMAGE_OPENAI_STRENGTH} / Google {QWEN_ZIMAGE_GOOGLE_STRENGTH} / "
f"Microsoft InvisMark {QWEN_ZIMAGE_MICROSOFT_STRENGTH} floors; sdxl-zimage "
f"Microsoft InvisMark {QWEN_ZIMAGE_MICROSOFT_STRENGTH} / Meta Content Seal "
f"{QWEN_ZIMAGE_META_STRENGTH} floors; sdxl-zimage "
f"uses OpenAI {SDXL_ZIMAGE_OPENAI_STRENGTH} / Google {SDXL_ZIMAGE_GEMINI_STRENGTH} / "
f"unknown {SDXL_ZIMAGE_UNKNOWN_STRENGTH}, from the C2PA issuer)"
)
@@ -167,8 +185,18 @@ def resolve_strength(
return resolution_adaptive_denoise(*size)
def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "microsoft"] | None:
"""Select the strength cohort from non-invalid pixel-watermark provenance."""
def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "microsoft", "meta"] | None:
"""Select the strength cohort from non-invalid pixel-watermark provenance.
OpenAI / Google / Microsoft come from their C2PA issuers. Meta is the
fallback cohort: Muse Image carries no C2PA at all, and its only readable
companion is the IPTC ``trainedAlgorithmicMedia`` XMP tag -- a standard code
other platforms also use. Attributing that tag to Meta is a measured bet,
not an identification: the other tag users in this project's model (ByteDance
products, X) ship no invisible pixel watermark this profile targets, so the
worst misroute spends the Meta floor (0.1) where the resolution curve would
have spent a similar amount, and Google/OpenAI files never reach this arm
because their C2PA matched first."""
try:
from remove_ai_watermarks._internal.c2pa import (
c2pa_info_has_invalid_credential,
@@ -187,4 +215,25 @@ def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "micros
return "openai"
if not c2pa_info_has_invalid_credential(info) and c2pa_info_has_invismark(info):
return "microsoft"
if _standalone_iptc_ai_tag(image_path):
return "meta"
return None
def _standalone_iptc_ai_tag(image_path: Path) -> bool:
"""True when the file carries an AI IPTC marker with no C2PA around it.
Mirrors identify's ``standalone_iptc`` condition (the tag is only
trustworthy as platform evidence when no manifest supersedes it) without
importing the heavy identify module: the shared chunk-aware
:func:`metadata.scan_head` window -- Muse WebP outputs place their XMP
packet in a tail chunk up to hundreds of KB past a plain head read, which
is exactly what scan_head's extensions exist to catch.
"""
try:
from remove_ai_watermarks.metadata import IPTC_AI_MARKERS, c2pa_marker_in, scan_head
scan = scan_head(image_path)
except Exception:
return False
return any(marker in scan for marker in IPTC_AI_MARKERS) and not c2pa_marker_in(scan)
+7 -2
View File
@@ -232,6 +232,7 @@ class InvisibleOptions:
strength: float | None = None
pipeline: str = "qwen-zimage"
vendor: str | None = None
seed: int | None = None
hf_token: str | None = None
humanize: float = 0.0
@@ -487,13 +488,17 @@ def _run_invisible(
if not is_available():
say("invisible", "unavailable")
return "unavailable"
if not (force or evidence.has_invisible_target()):
if not (force or opts.vendor is not None or evidence.has_invisible_target()):
say("invisible", "no-signal")
return "no-signal"
from remove_ai_watermarks._internal.watermark_profiles import resolve_strength, vendor_for_strength
vendor = vendor_for_strength(vendor_source)
# An explicit vendor override wins over detection and implies the scrub runs:
# naming the cohort (e.g. "meta" for Muse Image Content Seal, which carries no
# provenance to detect) asserts the pixel watermark is present, so the no-signal
# gate must not skip it.
vendor = opts.vendor or vendor_for_strength(vendor_source)
# Report the strength the engine will actually execute, resolved the same way it
# resolves it, so the reported value cannot drift from the executed one.
with suppress(Exception):
+53 -6
View File
@@ -274,6 +274,32 @@ _strength_option = click.option(
default=None,
help=f"Denoising strength (0.0-1.0). Default: {strength_default_help()}.",
)
# Explicit strength-cohort override. Auto-detection reads the C2PA issuer, so it
# covers OpenAI / Google / Microsoft; Meta Content Seal has no provenance signal
# (no C2PA; the IPTC tag is a standard code), and an unknown or stripped manifest
# also leaves the resolution-adaptive curve in charge -- this flag is the way to
# name the cohort when the user knows what the file does not say.
_vendor_option = click.option(
"--vendor",
type=click.Choice(["auto", "openai", "google", "microsoft", "meta"]),
default="auto",
help=(
"Strength cohort for the invisible-removal default, and it implies the scrub "
"runs even without a local signal: naming the cohort asserts the pixel "
"watermark is present. auto: derive from C2PA provenance, else "
"resolution-adaptive. Set explicitly when the source is known but unreadable "
"(e.g. meta for Muse Image Content Seal, which never carries C2PA)."
),
)
def _explicit_vendor(vendor: str | None) -> str | None:
"""Normalize --vendor's ``auto`` default to None for the engine/API seam.
One helper so the three diffusion commands cannot drift on the spelling."""
return None if vendor in (None, "auto") else vendor
_seed_option = click.option(
"--seed",
type=int,
@@ -796,6 +822,7 @@ def cmd_erase(
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@_output_option
@_strength_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -815,6 +842,7 @@ def cmd_invisible(
source: Path,
output: Path | None,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -851,11 +879,17 @@ def cmd_invisible(
if output is None:
output = source.with_stem(source.stem + "_clean")
# An explicit --vendor wins over detection (see the option help) and implies the
# scrub runs: naming the cohort asserts the pixel watermark is present, so the
# no-signal gate must not skip it. Resolved BEFORE the gate for the same reason.
resolved_vendor = _explicit_vendor(vendor)
# Gate BEFORE building the engine: skip the destructive regeneration when no
# invisible AI watermark is locally detectable (it would only degrade a clean
# image -- dominant paid score-0 cause), so the common skip path pays nothing for
# engine construction. A skip never claims the image is clean; --force overrides.
if _should_skip_invisible_scrub(force, source):
# engine construction. A skip never claims the image is clean; --force and an
# explicit --vendor override.
if _should_skip_invisible_scrub(force or resolved_vendor is not None, source):
_no_invisible_signal_exit(source)
def progress_cb(msg: str) -> None:
@@ -870,11 +904,18 @@ def cmd_invisible(
)
# Detect the SynthID vendor from the ORIGINAL (before processing strips C2PA) so the
# displayed and executed strength agree on the vendor-adaptive default.
vendor = vendor_for_strength(source)
# displayed and executed strength agree on the vendor-adaptive default. An explicit
# --vendor override wins over detection: it names a cohort the file cannot prove
# (Meta Content Seal never carries C2PA; a stripped manifest proves nothing).
detected_vendor = vendor_for_strength(source) if resolved_vendor is None else None
vendor_label = resolved_vendor or detected_vendor
vendor_note = " (override)" if resolved_vendor else ""
console.print(f" Input: {source.name}")
console.print(f" Pipeline: {pipeline}")
console.print(f" Strength: {_resolved_strength_for_display(source, strength, vendor, pipeline)}")
console.print(
f" Strength: {_resolved_strength_for_display(source, strength, vendor_label, pipeline)}"
+ (f" [vendor: {vendor_label}{vendor_note}]" if vendor_label else "")
)
t0 = time.monotonic()
try:
@@ -887,7 +928,7 @@ def cmd_invisible(
unsharp=unsharp,
adaptive_polish=adaptive_polish,
max_resolution=max_resolution,
vendor=vendor,
vendor=vendor_label,
tile=tile,
tile_size=tile_size,
tile_overlap=tile_overlap,
@@ -1448,6 +1489,7 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo
@_visible_backend_option
@_visible_sensitivity_option
@_strength_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -1469,6 +1511,7 @@ def cmd_all(
backend: str,
sensitivity: str,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -1549,6 +1592,7 @@ def cmd_all(
sensitivity=_parse_sensitivity(sensitivity),
invisible=InvisibleOptions(
strength=strength,
vendor=_explicit_vendor(vendor),
pipeline=pipeline,
seed=seed,
hf_token=hf_token,
@@ -1643,6 +1687,7 @@ def _batch_engine(mode: str, options: InvisibleOptions) -> object | None:
@_visible_backend_option
@_visible_sensitivity_option
@_humanize_option
@_vendor_option
@_pipeline_option
@_seed_option
@_hf_token_option
@@ -1660,6 +1705,7 @@ def cmd_batch(
mode: str,
output_dir: Path | None,
strength: float | None,
vendor: str | None,
pipeline: str,
seed: int | None,
hf_token: str | None,
@@ -1697,6 +1743,7 @@ def cmd_batch(
invisible_options = InvisibleOptions(
strength=strength,
vendor=_explicit_vendor(vendor),
pipeline=pipeline,
seed=seed,
hf_token=hf_token,
+35 -2
View File
@@ -135,6 +135,11 @@ _C2PA_INVALID_CAVEAT = (
"are retained only as removal hints, not as verified provenance."
)
_IPTC_ONLY_CAVEAT = "The IPTC 'Made with AI' tag flags AI provenance but does not identify the specific platform."
_CONTENT_SEAL_CAVEAT = (
"Meta Muse Image outputs carry the invisible Content Seal pixel watermark, which has no "
"local decoder; `invisible` removes it (auto when this tag is present, or `--vendor meta` "
"on stripped files) and meta.ai/identification verifies it."
)
_INVISIBLE_WM_CAVEAT = (
"The open invisible watermark is fragile: it does not survive JPEG re-encoding "
"or resizing, so it confirms origin only on a pristine (un-re-encoded) file."
@@ -1268,7 +1273,19 @@ def _identify_from_evidence(
# reusing the derived `has_c2pa` / `source_kind` above, which are broader:
# the file path's answer must not move.
trained_source = b"trainedAlgorithmicMedia" in head or b"TrainedAlgorithmicMedia" in head
if not synthid and trained_source and c2pa_marker_in(head) and (vendors := synthid_evidence_vendors_in(region)):
# Same suppression as every other inference site: bytes that name their own
# forensic soft-binding algorithm carry that vendor's mark, and the generic
# vendor-token inference must not add a second, differently-attributed
# invisible watermark (Microsoft Designer: "Azure OpenAI ImageGen" agent +
# the InvisMark watermarked action read as "SynthID per OpenAI").
soft_binding_vendors = soft_binding_vendors_in(region)
if (
not synthid
and trained_source
and c2pa_marker_in(head)
and not soft_binding_vendors
and (vendors := synthid_evidence_vendors_in(region))
):
synthid = synthid_verdict(", ".join(vendors))
if synthid:
watermarks.append(
@@ -1283,7 +1300,7 @@ def _identify_from_evidence(
# ── C2PA soft-binding: a named forensic/third-party watermark vendor ─
# (Adobe TrustMark, Digimarc, Imatag, ...). Present in the manifest even when
# the watermark itself can't be decoded; names whose watermark stamped the pixels.
soft_binding = meta.get("soft_binding") or (", ".join(v) if (v := soft_binding_vendors_in(region)) else None)
soft_binding = meta.get("soft_binding") or (", ".join(soft_binding_vendors) if soft_binding_vendors else None)
if soft_binding:
soft_binding_algorithm = meta.get("soft_binding_algorithm") or info.get("soft_binding_algorithm")
soft_binding_value = meta.get("soft_binding_value") or info.get("soft_binding_value")
@@ -1317,7 +1334,23 @@ def _identify_from_evidence(
if standalone_iptc:
signals.append(Signal("iptc", "digitalSourceType (Made with AI)", "high"))
watermarks.append("IPTC digitalSourceType (Made with AI)")
# Muse Image stamps every output with the invisible Content Seal, and this
# tag is the only provenance such a file carries - the same measured bet
# the strength router makes (vendor_for_strength -> "meta"). Emit the seal
# as its own stable signal, the way InvisMark is additive over
# soft_binding, so clients select pixel removal from the signal list
# instead of parsing caveats. It is an attribution, not a decode: no
# public Content Seal decoder exists, hence "medium".
signals.append(
Signal(
"content_seal",
"Meta Muse Content Seal pixel watermark (attributed by the standalone AI digital-source tag)",
"medium",
)
)
watermarks.append("Invisible Content Seal watermark (Meta Muse attribution)")
caveats.append(_IPTC_ONLY_CAVEAT)
caveats.append(_CONTENT_SEAL_CAVEAT)
if platform is None:
# Apple Photos Clean Up (Apple Intelligence object removal) marks
# the edit with photoshop:Credit / IPTC "Apple Photos Clean Up"
+7
View File
@@ -839,6 +839,13 @@ def synthid_source(image_path: Path, *, c2pa_info: dict[str, Any] | None = None)
ai_source = b"trainedAlgorithmicMedia" in data or b"TrainedAlgorithmicMedia" in data
if not (has_c2pa and ai_source):
return None
from remove_ai_watermarks._internal.c2pa import soft_binding_vendors_in
# A scan that names its own forensic soft-binding algorithm carries that
# vendor's mark; the generic vendor-token inference must not add a second,
# differently-attributed invisible watermark from the same bytes.
if soft_binding_vendors_in(data):
return None
matched = synthid_evidence_vendors_in(data)
return ", ".join(matched) if matched else None
+33
View File
@@ -426,6 +426,39 @@ class TestInvisibleCommand:
assert result.exit_code == 0, result.output
mock_engine.remove_watermark.assert_called_once()
def test_invisible_explicit_vendor_implies_force_and_sets_cohort(self, runner, sample_png, tmp_path):
"""--vendor meta names a cohort the file cannot prove (Content Seal carries
no C2PA), so it must both bypass the no-signal skip and arrive at the engine
as the vendor, where it resolves to the measured Meta floor (not the
area-curve value the same size would otherwise get)."""
mock_cls, mock_engine = _mock_invisible_engine()
output = tmp_path / "clean.png"
with (
patch("remove_ai_watermarks.invisible_engine.is_available", return_value=True),
patch("remove_ai_watermarks.cli.InvisibleEngine", mock_cls, create=True),
patch("remove_ai_watermarks.invisible_engine.InvisibleEngine", mock_cls),
):
result = runner.invoke(main, ["invisible", str(sample_png), "-o", str(output), "--vendor", "meta"])
assert result.exit_code == 0, result.output
kwargs = mock_engine.remove_watermark.call_args.kwargs
assert kwargs["vendor"] == "meta"
assert "0.1" in result.output # the resolved Meta floor, printed
assert "0.094" not in result.output # not the sample's area-curve value (200x200 -> ~0.0944)
def test_invisible_vendor_auto_keeps_detection_semantics(self, runner, sample_png, tmp_path):
"""--vendor auto is the default spelled out: detection still runs and a
no-signal file still skips."""
mock_cls, mock_engine = _mock_invisible_engine()
output = tmp_path / "clean.png"
with (
patch("remove_ai_watermarks.invisible_engine.is_available", return_value=True),
patch("remove_ai_watermarks.cli.InvisibleEngine", mock_cls, create=True),
patch("remove_ai_watermarks.invisible_engine.InvisibleEngine", mock_cls),
):
result = runner.invoke(main, ["invisible", str(sample_png), "-o", str(output), "--vendor", "auto"])
assert result.exit_code == 2, result.output
mock_engine.remove_watermark.assert_not_called()
def test_invisible_runs_without_force_when_signal_present(self, runner, tmp_path):
"""An image carrying an AI metadata signal IS a scrub target, so the run
proceeds with no --force needed."""
@@ -0,0 +1,85 @@
"""Regression: a manifest that names its own forensic soft binding must not
also report a SynthID watermark from the generic vendor-token inference.
Microsoft Designer manifests sign as Microsoft, carry the InvisMark
``c2pa.watermarked`` action, and name their generation agent
"Azure OpenAI ImageGen". The OpenAI issuer token inside that agent name plus
the watermarked action used to satisfy the OpenAI SynthID-evidence rule,
double-counting one forensic mark as two pixel watermarks.
"""
from __future__ import annotations
from remove_ai_watermarks._internal.c2pa import c2pa_info_from_manifest_store
DESIGNER_STORE = {
"active_manifest": "designer",
"manifests": {
"designer": {
"signature_info": {"issuer": "Microsoft Corporation", "common_name": "Microsoft Corporation"},
"claim_generator_info": [{"name": "Microsoft Responsible AI Provenance", "version": "1.0"}],
"assertions": [
{
"label": "c2pa.actions",
"data": {
"actions": [
{
"action": "c2pa.created",
"softwareAgent": {"name": "Azure OpenAI ImageGen"},
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
},
{"action": "c2pa.watermarked"},
]
},
},
{
"label": "c2pa.soft-binding",
"data": {"alg": "com.microsoft.invismark.1", "blocks": [{"value": "bf7a2993-cc1f-47e1-b1f0-cd8839aabb22"}]},
},
],
}
},
}
def test_named_soft_binding_suppresses_generic_synthid_evidence() -> None:
info = c2pa_info_from_manifest_store(DESIGNER_STORE)
assert info["ai_source_kind"] == "generated"
assert info["soft_binding_algorithm"] == "com.microsoft.invismark.1"
assert info.get("synthid_watermark") is None
assert info.get("synthid_vendors") is None
def test_vendor_agent_name_alone_is_not_the_vendors_provenance() -> None:
"""The identity-scoped inference must not fire on a service name either.
Same manifest without the soft binding: the "Azure OpenAI ImageGen" agent
is not an OpenAI signature or claim generator, so no OpenAI SynthID
evidence may be derived from it.
"""
store = {
"active_manifest": "designer",
"manifests": {
"designer": {
"signature_info": {"issuer": "Microsoft Corporation", "common_name": "Microsoft Corporation"},
"claim_generator_info": [{"name": "Microsoft Responsible AI Provenance", "version": "1.0"}],
"assertions": [
{
"label": "c2pa.actions",
"data": {
"actions": [
{
"action": "c2pa.created",
"softwareAgent": {"name": "Azure OpenAI ImageGen"},
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
},
{"action": "c2pa.watermarked"},
]
},
}
],
}
},
}
info = c2pa_info_from_manifest_store(store)
assert info.get("synthid_watermark") is None
+29
View File
@@ -666,6 +666,35 @@ class TestIdentifyRealSamples:
assert r.is_ai_generated is True
assert r.ai_source_kind == "enhanced"
def test_standalone_ai_tag_attributes_the_content_seal(self, tmp_path: Path):
"""A standalone AI digital-source tag emits the seal as its own signal.
Muse Image outputs carry no C2PA; this tag is their only provenance, and
Muse stamps every output with the invisible Content Seal. The signal is
the strength router's Meta bet as evidence - an attribution, not a decode
(no public decoder exists), so its confidence is medium and the caveat
still points at the oracle.
"""
p = tmp_path / "muse-tag.jpg"
p.write_bytes(
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
)
r = identify(p, check_visible=False, check_invisible=False)
names = [s.name for s in r.signals]
assert "iptc" in names
assert "content_seal" in names
seal = next(s for s in r.signals if s.name == "content_seal")
assert seal.confidence == "medium"
assert "Invisible Content Seal watermark (Meta Muse attribution)" in r.watermarks
assert any("meta.ai/identification" in c for c in r.caveats)
def test_c2pa_backed_file_gets_no_content_seal_attribution(self):
"""C2PA issuers win first: a manifest-backed file is not Meta-routed."""
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False, check_invisible=False)
assert "content_seal" not in [s.name for s in r.signals]
def test_flux_bfl_c2pa_png(self):
# flux-1.png: real Black Forest Labs FLUX.2 Playground output (signed C2PA).
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False)
+51 -1
View File
@@ -6,6 +6,7 @@ answer and a clean refusal rather than a fallback ladder.
from __future__ import annotations
import struct
from pathlib import Path
from unittest.mock import MagicMock, patch
@@ -15,6 +16,7 @@ from remove_ai_watermarks._internal.utils import get_image_format, is_supported_
from remove_ai_watermarks._internal.watermark_profiles import (
PROFILE_CHOICES,
QWEN_ZIMAGE_GOOGLE_STRENGTH,
QWEN_ZIMAGE_META_STRENGTH,
QWEN_ZIMAGE_OPENAI_STRENGTH,
REMOVAL_MODULES,
SDXL_ZIMAGE_GEMINI_STRENGTH,
@@ -198,6 +200,30 @@ class TestNoReembeddedWatermark:
assert "add_watermarker" not in calls["controlnet"]
# Minimal WebP stub whose XMP chunk carries the IPTC trainedAlgorithmicMedia
# tag exactly as Muse outputs place it (built inline so the test has no binary
# fixture dependency).
_XMP_PAYLOAD = (
b'<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax#">'
b'<rdf:Description rdf:about="" xmlns:iptcExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/" '
b'iptcExt:DigitalSourceType="http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"/>'
b"</rdf:RDF></x:xmpmeta>"
)
def _webp_stub(xmp: bytes | None) -> bytes:
def chunk(cid: bytes, data: bytes) -> bytes:
return cid + struct.pack("<I", len(data)) + data + (b"\x00" if len(data) % 2 else b"")
body = b"WEBP" + chunk(b"VP8L", b"\x00" * 20)
if xmp is not None:
body += chunk(b"XMP ", xmp)
return b"RIFF" + struct.pack("<I", len(body)) + body
_MUSE_WEBP_WITH_IPTC_TAG = _webp_stub(_XMP_PAYLOAD)
class TestResolveStrength:
"""resolve_strength owns the qwen-zimage and sdxl-zimage policies."""
@@ -222,12 +248,36 @@ class TestResolveStrength:
with pytest.raises(ValueError, match="size is required"):
resolve_strength(None, "openai", "qwen-zimage")
@pytest.mark.parametrize(("vendor", "expected"), [("microsoft", 0.15), ("openai", 0.07675)])
@pytest.mark.parametrize(("vendor", "expected"), [("microsoft", 0.15), ("openai", 0.07675), ("meta", 0.1)])
def test_qwen_zimage_measured_vendors_use_flat_cross_source_margins(self, vendor, expected):
"""Measured providers must not fall below their operating points on small files."""
assert resolve_strength(None, vendor, "qwen-zimage", size=(600, 500)) == pytest.approx(expected)
assert resolve_strength(None, vendor, "qwen-zimage", size=(4000, 3000)) == pytest.approx(expected)
def test_meta_floor_is_size_independent_and_sdxl_falls_to_unknown(self):
"""The Meta floor bypasses the area curve at every size (Content Seal removal
was bracketed on 2.56 MP generations and derived as a flat cross-source
margin, like the other measured cohorts), while sdxl-zimage has no measured
Meta rung and must fall to the conservative unknown value, not invent one."""
for size in ((600, 500), (1600, 1600), (1920, 1280), (4000, 3000)):
assert resolve_strength(None, "meta", "qwen-zimage", size=size) == pytest.approx(QWEN_ZIMAGE_META_STRENGTH)
assert resolve_strength(None, "meta", "sdxl-zimage") == SDXL_ZIMAGE_UNKNOWN_STRENGTH
def test_vendor_for_strength_routes_standalone_iptc_to_meta(self, tmp_path):
"""Auto mode: a file whose only provenance is the AI IPTC tag routes to the
meta cohort (Muse carries no C2PA; the tag is its fallback companion), while
a file without the tag stays on the resolution curve and a C2PA issuer still
wins over the tag."""
from remove_ai_watermarks._internal.watermark_profiles import vendor_for_strength
tagged = tmp_path / "tagged.webp"
tagged.write_bytes(_MUSE_WEBP_WITH_IPTC_TAG)
assert vendor_for_strength(tagged) == "meta"
stripped = tmp_path / "stripped.webp"
stripped.write_bytes(b"RIFF\x24\x00\x00\x00WEBPVP8 \x18\x00\x00\x00" + b"\x00" * 16)
assert vendor_for_strength(stripped) is None
def test_sdxl_zimage_uses_its_flat_vendor_ladder(self):
assert SDXL_ZIMAGE_OPENAI_STRENGTH == 0.15
Generated
+66 -69
View File
@@ -288,7 +288,7 @@ wheels = [
[[package]]
name = "c2pa-python"
version = "0.37.7"
version = "0.37.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
@@ -298,15 +298,15 @@ dependencies = [
{ name = "toml" },
{ name = "wheel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/74/291b19f26fe628ca6bf001be741dfe634f8689756cf8495499e02ab2f631/c2pa_python-0.37.7.tar.gz", hash = "sha256:66114c3bc6b073249cb33043e491cf292e052ff770c1aefaed66a5ca2d8aaa0c", size = 112987, upload-time = "2026-08-13T21:25:27.464Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/32/e93959d17db5af4078532f587b9638e87b6429ca0b51348a8ac6ad99e471/c2pa_python-0.37.8.tar.gz", hash = "sha256:c52eb07244558a5997fe61b6ed8d879b4f46f38642dd11956bcedf888c58649c", size = 113008, upload-time = "2026-08-27T20:40:06.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/ee/be1b567bd85563ee72f7826df23c1fa13886f086b58714dda9bb2f9491a7/c2pa_python-0.37.7-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fde655c31e31015b1474a06f85728e878258e1fe7f2fff6cf4924e4985479da9", size = 16187849, upload-time = "2026-08-13T21:25:06.779Z" },
{ url = "https://files.pythonhosted.org/packages/51/69/50dea7bcbfd68ce7f6734735a1fb9e80ac15498520946a2ff1598d7fef5b/c2pa_python-0.37.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d2ee0682f00c3a361d6401a6d2ae249bf953c2375c6945e5ebcf3cbd8022e8e4", size = 14228094, upload-time = "2026-08-13T21:25:09.687Z" },
{ url = "https://files.pythonhosted.org/packages/c0/c2/2c8a58a88488b30f9f96454eee26f815622cfca00b965d60b7a639c7d94d/c2pa_python-0.37.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dae0e1177919a2b89b8d951d525421b35d67b21bc38049d544dbcfd881555a44", size = 13869736, upload-time = "2026-08-13T21:25:12.051Z" },
{ url = "https://files.pythonhosted.org/packages/dc/a6/9a4eafdcc836694542eaa3bade9e994248fa777ae4c0eac2bc4f1f199121/c2pa_python-0.37.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c6b2e2b4cb7571e4b8ee76b6f9c472c44aa5650702207db06057d03a14ca2fd2", size = 14085896, upload-time = "2026-08-13T21:25:14.349Z" },
{ url = "https://files.pythonhosted.org/packages/76/c2/983bcdc449e2c4384f0a7299859ec578365c54a2c3ca42030724ab21751e/c2pa_python-0.37.7-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:36bb8eff8904869d8fdf00f6aabe7ba41389d3dbffba02bae324c690450019c2", size = 14714332, upload-time = "2026-08-13T21:25:16.718Z" },
{ url = "https://files.pythonhosted.org/packages/3a/0e/86d47b5dba0a960bac3225e16f7aaa202ea894efed683801a9b99fc33092/c2pa_python-0.37.7-py3-none-win_amd64.whl", hash = "sha256:8054383a53d50dd86197f391bb2e91871284eaecf94410e6d3050803d02fd5e3", size = 86460712, upload-time = "2026-08-13T21:25:20.269Z" },
{ url = "https://files.pythonhosted.org/packages/be/30/4e95f1a9be67bb66690cde0b6143f8761d294d67919cf99016850d7963d7/c2pa_python-0.37.7-py3-none-win_arm64.whl", hash = "sha256:a0980f7d94459a2569c0338af9cdd5b7301ded6578eab2701ed61f8590d3fca4", size = 83941581, upload-time = "2026-08-13T21:25:24.573Z" },
{ url = "https://files.pythonhosted.org/packages/67/6a/252d8d072ac4192df8da28771be64a54cb4ea73f08f3ca1f2b7fafa823a5/c2pa_python-0.37.8-py3-none-macosx_10_9_universal2.whl", hash = "sha256:8dae7d198dd583de3963529b60665e611e36ddca677f85700147d3bdbc885b27", size = 16186835, upload-time = "2026-08-27T20:39:35.421Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b7/3519bbb3735adce56eb0836d25fcf4e00c36c4e5cd480ee5277aadba18ed/c2pa_python-0.37.8-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d3ff8da9be41c6f74218872f8ebf0b0020dbc00202bb30fda751422418f679c5", size = 14230208, upload-time = "2026-08-27T20:39:38.587Z" },
{ url = "https://files.pythonhosted.org/packages/24/5c/ed3000ca3de522c17797482a90cd64f57cdd2fb9b21a49910c04327954bf/c2pa_python-0.37.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ee4fbbe3b98dd4349cc9c60db3c4fb1c349c65b5c72fb89a0de403ca778d3d0d", size = 13865982, upload-time = "2026-08-27T20:39:41.837Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bd/934002a34aa76e5136508b524734dc3c4719f6d87b5a98cf93cbf4917d05/c2pa_python-0.37.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e8c04f4220194a3392dda290be96181ffc94ee5008e87e1fa96c390ec4f9c53c", size = 14077063, upload-time = "2026-08-27T20:39:45.069Z" },
{ url = "https://files.pythonhosted.org/packages/8c/e8/ca03c21e253e95a3aaab472697f1c0c766af852cb643879b4928f6c57298/c2pa_python-0.37.8-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20742bd702d09476390dc73a375883990291ea458af8f3e61ef48fa9928f2313", size = 14715089, upload-time = "2026-08-27T20:39:48.275Z" },
{ url = "https://files.pythonhosted.org/packages/44/cd/376d90d3073ad975d38a42bdbcb42cec071bff3b6831dd5a6b807526a1df/c2pa_python-0.37.8-py3-none-win_amd64.whl", hash = "sha256:5cf1dadd1c8de7da298db9c62c432180d7185a31e81a96dfae7bce7944b21ddd", size = 86478089, upload-time = "2026-08-27T20:39:55.511Z" },
{ url = "https://files.pythonhosted.org/packages/e1/df/3e59b7813be65678d28f88a7a0c7a13ba8c983fe2a1ecf406959dc7b88df/c2pa_python-0.37.8-py3-none-win_arm64.whl", hash = "sha256:af827698ead013ca20e829e3c9fe1846b548ae77d1ab84762118fbde340e3f0a", size = 83936189, upload-time = "2026-08-27T20:40:03.148Z" },
]
[[package]]
@@ -546,14 +546,11 @@ wheels = [
[[package]]
name = "click"
version = "8.4.2"
version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[package]]
@@ -919,7 +916,7 @@ wheels = [
[[package]]
name = "diffusers"
version = "0.39.0"
version = "0.40.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -933,9 +930,9 @@ dependencies = [
{ name = "requests" },
{ name = "safetensors" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/81/6095237b86a3116c4789f28c4435d5296c00c0fc74ffde99008fd6b3a36c/diffusers-0.39.0.tar.gz", hash = "sha256:14bb1d98c85a0e463d734c99aaa73b480a7bc9bad22af30fbf730ef8f09c1d67", size = 4651240, upload-time = "2026-07-03T08:48:47.904Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/32/b9c6a198812e2a7d2c2828a850dba384306f9af285eac23e35e89d5b008a/diffusers-0.40.0.tar.gz", hash = "sha256:49f112ce52ff6d332ab68afec01f79a53e6489b42cb69104444c98b4a2f64af8", size = 4886420, upload-time = "2026-08-20T14:30:40.923Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/3f/7469c46e9d22307ea686bab687d70e6bf328722952f9d10339f5e913e608/diffusers-0.39.0-py3-none-any.whl", hash = "sha256:912aca51b5787365110806e984d5555735bf8a461073bb8459029d0bca7870ef", size = 5631176, upload-time = "2026-07-03T08:48:45.337Z" },
{ url = "https://files.pythonhosted.org/packages/db/df/ffb593ebed2a068d2d6be44261283f39a6b809c0fcdfdcafbd448cbeec77/diffusers-0.40.0-py3-none-any.whl", hash = "sha256:5b5da7c3ddb62152fa4afc577f02e050af688c797375c34fb2d01006da3f3541", size = 5911654, upload-time = "2026-08-20T14:30:38.422Z" },
]
[[package]]
@@ -1185,7 +1182,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "1.27.0"
version = "1.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -1198,9 +1195,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c6/ae/222a91937ebee7f62c0ca8f5ee0afd97577caf24c0abb927d1f5c7e9f6d2/huggingface_hub-1.28.0.tar.gz", hash = "sha256:46a2e950c09234de54093d587d1675382f0d08dbd600d9fb599b5932f5b2c6cb", size = 959609, upload-time = "2026-08-18T12:27:15.101Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
{ url = "https://files.pythonhosted.org/packages/51/0e/eafef18f1a75e125e68395db21131db0cf868a128ecd2fce69b4df6c584b/huggingface_hub-1.28.0-py3-none-any.whl", hash = "sha256:58a8bacb03072edfc38067065e9dc24bbb34805410fcd36a1632de0b329660bb", size = 793202, upload-time = "2026-08-18T12:27:12.719Z" },
]
[[package]]
@@ -1925,7 +1922,7 @@ wheels = [
[[package]]
name = "onnxruntime"
version = "1.28.0"
version = "1.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flatbuffers" },
@@ -1935,30 +1932,30 @@ dependencies = [
{ name = "protobuf" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/4d/5014667e2a3a77d6e1b74cc3d88948d06163b8e0a33a84c85073322b5dec/onnxruntime-1.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f5c5daabd28aad610f83fdcf32acec8fb57e6adc6c6a39fe2a3c755db957b410", size = 19130506, upload-time = "2026-07-25T01:22:34.489Z" },
{ url = "https://files.pythonhosted.org/packages/ea/97/b7ce1bc8bb6048b5fe9129f55d6506dc19499068ef2e0a0af1ae3c8aa4e7/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66f9ceb29909c70839e4e4fb3435c7b490050d8f162bd5f3aba4ca01ee517f", size = 17039880, upload-time = "2026-07-25T01:21:37.538Z" },
{ url = "https://files.pythonhosted.org/packages/f3/17/4e5ecd8764f87573c495d834ce79e61ecca47f7a01d1e444a606e570edcb/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a166b78ee04f3a37fa1ef82034b6a3ce96d9684e582d4d30b296de83e9998bb5", size = 19193162, upload-time = "2026-07-25T01:21:59.151Z" },
{ url = "https://files.pythonhosted.org/packages/9f/10/3d946d5d5f2cdcc3c8da36cae63190c516d16349edaffd944bda60ca4c3e/onnxruntime-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d650aeee29368414367b65529e90afe4bf1bab76254789063b8b2f7ea3013c8", size = 13752539, upload-time = "2026-07-25T01:22:24.524Z" },
{ url = "https://files.pythonhosted.org/packages/8f/74/1c440be7af1e026280b139caa1be5d11bd4dc368011ddbe8f5362b58e12f/onnxruntime-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:0faf85fb447a663c9cdadc39bd6b19bdf7bedded6699e45731b9b36c46fd993d", size = 13449940, upload-time = "2026-07-25T01:22:14.97Z" },
{ url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362, upload-time = "2026-07-25T01:22:36.965Z" },
{ url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628, upload-time = "2026-07-25T01:21:40.481Z" },
{ url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257, upload-time = "2026-07-25T01:22:01.695Z" },
{ url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036, upload-time = "2026-07-25T01:22:26.89Z" },
{ url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462, upload-time = "2026-07-25T01:22:17.38Z" },
{ url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759, upload-time = "2026-07-25T01:21:53.765Z" },
{ url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339, upload-time = "2026-07-25T01:21:43.005Z" },
{ url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329, upload-time = "2026-07-25T01:22:04.133Z" },
{ url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033, upload-time = "2026-07-25T01:22:29.302Z" },
{ url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175, upload-time = "2026-07-25T01:22:19.912Z" },
{ url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307, upload-time = "2026-07-25T01:21:45.492Z" },
{ url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954, upload-time = "2026-07-25T01:22:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748, upload-time = "2026-07-25T01:21:56.297Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950, upload-time = "2026-07-25T01:21:48.606Z" },
{ url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924, upload-time = "2026-07-25T01:22:09.727Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738, upload-time = "2026-07-25T01:22:31.629Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117, upload-time = "2026-07-25T01:22:22.387Z" },
{ url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518, upload-time = "2026-07-25T01:21:51.08Z" },
{ url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976, upload-time = "2026-07-25T01:22:12.474Z" },
{ 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" },
]
[[package]]
@@ -3172,7 +3169,7 @@ wheels = [
[[package]]
name = "remove-ai-watermarks"
version = "0.31.2"
version = "0.33.0"
source = { editable = "." }
dependencies = [
{ name = "c2pa-python" },
@@ -3392,27 +3389,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.3"
version = "0.16.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" }
sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" },
{ url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" },
{ url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" },
{ url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" },
{ url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" },
{ url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" },
{ url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" },
{ url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" },
{ url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" },
{ url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" },
{ url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" },
{ url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" },
{ url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" },
{ url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" },
{ url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" },
{ url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" },
{ url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" },
{ url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" },
{ url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" },
{ url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" },
{ url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" },
{ url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" },
{ url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" },
{ url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" },
]
[[package]]
@@ -3782,7 +3779,7 @@ wheels = [
[[package]]
name = "transformers"
version = "5.15.0"
version = "5.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
@@ -3796,9 +3793,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/3f/d89353267d511e18f137dfd7769d07837350c11b88408ce1dfe2e93e56c7/transformers-5.15.0.tar.gz", hash = "sha256:bbf98f57b2ddd7c4ecbccfa2c0069017aa6fd01cc204bd50cbc0eeadcf2a13b8", size = 9377983, upload-time = "2026-08-10T10:27:23.261Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/92/c50c61da7046bbb59a4d011291aeadcfb4d7980ab36fdb31e93823a3fb93/transformers-5.15.1.tar.gz", hash = "sha256:27c996bd9075ddc82d40f8590dfdc81ea45f611bfca477e0db5d7fd257a482f7", size = 9378434, upload-time = "2026-08-19T11:28:20.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/43/81355710a4c84e9420e11a86d41a5364deb561f2ef36dfdf254a07371bbb/transformers-5.15.0-py3-none-any.whl", hash = "sha256:d7f007736f67749ae9490c4f8cb5d30b452ae2d68c8675e50ba8d63ea7feb107", size = 11749280, upload-time = "2026-08-10T10:27:20.416Z" },
{ url = "https://files.pythonhosted.org/packages/41/c4/a12e1d9b387fb0c40a57116db82b457e8c771cb419163cda29204d74a595/transformers-5.15.1-py3-none-any.whl", hash = "sha256:b7cdf238ff583e3a58dbc7fa34da1aaf091ce063141f65a30538160bd5afe93f", size = 11749582, upload-time = "2026-08-19T11:28:16.726Z" },
]
[[package]]