mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 05:20:46 +02:00
Stop attributing shared AI labels to vendors
This commit is contained in:
17 files changed
+122
-250
No files matched your search
+5
-5
@@ -465,10 +465,10 @@ 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
|
||||
(the default) derives it from product-specific C2PA provenance for
|
||||
OpenAI/Google/Microsoft. The standalone AI IPTC tag is a shared standard and
|
||||
does not select Meta; use `--vendor meta` only when the source is independently
|
||||
known. 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:
|
||||
|
||||
@@ -486,7 +486,7 @@ The same option exists on `all` and `batch`, and as
|
||||
| `qwen-zimage` | Default. Qwen-Image-2512 global pass plus a SAM-masked Z-Image face stage |
|
||||
| `sdxl-zimage` | The same recipe and face stage on an SDXL global pass, at a higher denoise |
|
||||
| `chroma-zimage` | The same face stage on an Apache-2.0 Chroma1 global pass; explicit OpenAI use needs a higher floor than qwen-zimage |
|
||||
| `auto` | Pick the engine from provenance: chroma-zimage for Microsoft, qwen-zimage for OpenAI, Google, Meta, and unknown |
|
||||
| `auto` | Pick the engine from provenance: chroma-zimage for Microsoft, qwen-zimage for OpenAI, Google, unknown, and an explicitly selected Meta cohort |
|
||||
|
||||
**All four are CUDA-only.** There is no CPU or MPS profile for invisible-watermark
|
||||
removal. The former `controlnet`, `sdxl`, `qwen` and `default` profiles were removed
|
||||
|
||||
@@ -1315,7 +1315,8 @@ A TC260 label relaxes the vendor its `ContentProducer` names, resolved through
|
||||
relax ByteDance's two products on every China-AIGC image -- which both risked a
|
||||
false fill on an image carrying some other vendor's mark and denied that vendor's
|
||||
own mark the relaxed gate its `provenance_ncc_factor` was calibrated for. An
|
||||
absent or unmapped producer still falls back to the ByteDance pair.
|
||||
absent or unmapped producer confirms no particular product and leaves every detector
|
||||
strict.
|
||||
|
||||
`remove_auto_marks` removes every selected mark, not only the strongest one.
|
||||
This matters for images that carry marks in more than one corner.
|
||||
@@ -2040,10 +2041,9 @@ so it is left alone.
|
||||
Measured 2026-08-26 against the anonymous oracle `meta.ai/identification`
|
||||
(verdicts read from the settled page text; corpus and full row-level provenance in
|
||||
`data/contentseal/manifest.csv`). The original measurement used the
|
||||
default resolution-adaptive curve. Current routing can infer the Meta cohort
|
||||
from a retained standalone AI IPTC tag, after checking C2PA vendor evidence
|
||||
first. Stripped files need explicit `--vendor meta` to select the measured
|
||||
Meta floor; absence of C2PA alone does not prevent automatic attribution.
|
||||
default resolution-adaptive curve. The retained standalone AI IPTC tag is a
|
||||
shared standard and no longer infers Meta. Files need explicit `--vendor meta`
|
||||
to select the measured Meta floor from independently known provenance.
|
||||
|
||||
| source (2.56 MP generation) | detected at | clean from |
|
||||
|---|---|---|
|
||||
@@ -2057,13 +2057,9 @@ 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**. 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
|
||||
`QWEN_ZIMAGE_META_STRENGTH`; `--vendor meta` / `InvisibleOptions.vendor` names
|
||||
the cohort explicitly, 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
|
||||
|
||||
@@ -247,6 +247,10 @@ import remove_ai_watermarks as raiw
|
||||
vendors = raiw.visible_provenance("input.png")
|
||||
```
|
||||
|
||||
A known TC260 `ContentProducer` maps to one registered product. A missing or
|
||||
unmapped producer, and the shared standalone IPTC AI tag, return no vendor key;
|
||||
neither standard identifies a manufacturer by itself.
|
||||
|
||||
Get the full provenance report:
|
||||
|
||||
```python
|
||||
|
||||
@@ -197,12 +197,7 @@ 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. 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
|
||||
signal, so it neither asserts Content Seal nor selects a strength cohort. 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
|
||||
@@ -215,12 +210,9 @@ 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).
|
||||
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
|
||||
cohort. `invisible --vendor meta` (also `all` and `batch`, and
|
||||
`InvisibleOptions.vendor` in the API) selects it when the source is independently
|
||||
known. 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
|
||||
|
||||
+2
-1
@@ -779,7 +779,8 @@ old OpenAI C2PA remains an origin signal without becoming a SynthID claim.
|
||||
|
||||
- **Kakao** (South Korea): SynthID adopter as of May 2026 (Google announcement)
|
||||
- **NVIDIA Cosmos**: SynthID for video (not still images; different pipeline)
|
||||
- **Meta AI**: does NOT use SynthID; uses IPTC `digitalSourceType` marker instead
|
||||
- **Meta AI**: does NOT use SynthID; Muse uses Content Seal and may retain a shared
|
||||
IPTC `digitalSourceType` companion tag that does not identify Meta by itself
|
||||
|
||||
### 4.4 Version evolution (v1 vs v2 hardening)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ AI generator values while retaining unrelated camera and editor EXIF. The
|
||||
shared `xai_signature_pair` helper is the single source of truth for the
|
||||
pair. On the ISOBMFF path, `blank_ai_exif_tokens` provides the corresponding
|
||||
in-place scrub for supported EXIF values, TC260 AIGC blocks, and the xAI pair.
|
||||
- **China TC260 AIGC label (caught by `AIGC_MARKERS` / `metadata.aigc_label`, surfaced by `identify` as the `aigc` signal):** China-served generators embed an XMP `<TC260:AIGC>{"Label":"1","ContentProducer":...}` block — China's mandatory AI-content labeling (TC260 namespace `tc260.org.cn/ns/AIGC`). The label says only "this is AI", but its `ContentProducer` names the signing entity — `001` + `1` + an 18-char Unified Social Credit Code + a 5-digit product suffix, normalized by `metadata.uscc_of`, or for a few generators a bare product name. `KnownMark.tc260_producer_codes` maps the codes settled per vendor by `scripts/vendor_cohort_harvest.py` to registry mark keys, so an AIGC image relaxes the detector of the vendor it actually carries rather than a guessed pair; an unmapped or absent producer falls back to ByteDance's two products. A code identifies a legal entity, not necessarily one brand, so a hosting or aggregating platform that signs for several apps is a recall bet rather than a proof.
|
||||
- **China TC260 AIGC label (caught by `AIGC_MARKERS` / `metadata.aigc_label`, surfaced by `identify` as the `aigc` signal):** China-served generators embed an XMP `<TC260:AIGC>{"Label":"1","ContentProducer":...}` block — China's mandatory AI-content labeling (TC260 namespace `tc260.org.cn/ns/AIGC`). The label says only "this is AI", but its `ContentProducer` names the signing entity — `001` + `1` + an 18-char Unified Social Credit Code + a 5-digit product suffix, normalized by `metadata.uscc_of`, or for a few generators a bare product name. `KnownMark.tc260_producer_codes` maps the codes settled per vendor by `scripts/vendor_cohort_harvest.py` to registry mark keys, so an AIGC image relaxes only the detector of the product it actually carries; an unmapped or absent producer leaves every product strict. The same mapping supplies the manufacturer for provenance-conflict checks. A code identifies a legal entity, not necessarily one brand, so a hosting or aggregating platform that signs for several apps is a recall bet rather than a proof.
|
||||
|
||||
**Doubao** (ByteDance) uses it (verified on a public issue sample; `ContentProducer` `001191110102MACQD9K64010000`, no C2PA/SynthID/imwatermark — the XMP block is the only signal; GitHub attachment upload did NOT strip it). The same standard is mandatory for Jimeng/Kling/Qwen/Ernie etc., so the one marker covers the whole China-AIGC-labeled ecosystem. `aigc_label` reads **four image serializations** through a shared `_parse` helper: the HTML-entity-encoded XMP `TC260:AIGC` block in **either RDF form** — the nested element `<TC260:AIGC>{...}</TC260:AIGC>` (Doubao) or the attribute `TC260:AIGC="{...}"` (**PicWish**, `ContentProducer="picwish"`, verified on compatible samples) — via a container-agnostic raw-byte scan (any JSON object accepted), a raw-JSON PNG `AIGC` tEXt chunk (Doubao also writes the label this way, no namespaced marker at all — confirmed on compatible samples, `ContentProducer="doubao"`), a bare raw-JSON `{"AIGC":{...}}` object embedded in **JPEG EXIF (UserComment)** by some China-served generators, brace-matched from the scan head with `json.JSONDecoder().raw_decode` (no namespaced marker, no PNG chunk — confirmed on compatible samples, `ContentProducer="001191440300708461136T1308L"`), **and** a bare `AIGC{...}` blob (the label glued straight to its JSON, no `"AIGC":` key wrapper) embedded in a **JPEG APP segment near the JFIF header** — confirmed on compatible samples. The two raw-JSON forms are scanned in one loop (`'"AIGC"'` then `AIGC{`) that **falls through on a non-TC260 / undecodable hit instead of returning** — a quoted `"AIGC"` can appear later in an XMP packet while the real label is a bare `AIGC{...}` earlier in the file, so an unconditional early return on the quoted form would shadow the bare form (the exact bug behind the 06-10 misses). Native MP4/MOV is a fifth serialization: TC260-PG-20257A stores an `AIGC` key in `moov.udta.meta.keys` and the raw JSON in the matching `ilst` item; Doubao's iOS export additionally writes two QuickTime-form variants (a bare `meta` box as a direct `moov` child, and a keyless `hdlr=mdir` metadata list whose `ilst` data items carry the JSON with no key name) — both verified on retained carriers (2026-08-17). The seeking parser reaches a tail `moov` without reading `mdat`; removal replaces the key with `free` and blanks the validated value at the same length so every box size and stream offset stays fixed. All generic forms are gated on at least one TC260 field (`TC260_AIGC_FIELDS`) so a generic `AIGC` key cannot false-positive; the namespaced XMP element is unambiguous and needs no gate. `TC260_AIGC_FIELDS` covers **two schemas**: the producer-side one (`Label` / `ContentProducer` / `ProduceID` / `ContentPropagator` / `PropagateID`, Doubao and most China gens) and the **service-provider** one (`ServiceProvider` / `ServiceUser`, plus generic `Time` / `ContentId` which are NOT gated on) — **Tencent Cloud's** AIGC variant (`ServiceProvider` = `腾讯云`), embedded in **EXIF `ImageDescription`**, verified on compatible samples. In `identify`, `aigc` fires on the parsed label **or** the `AIGC_MARKERS` byte scan (the latter preserves the laundering-tell case where the JSON payload is truncated).
|
||||
|
||||
@@ -221,7 +221,7 @@ 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). 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 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) selected through an explicit `--vendor meta` override; the shared IPTC tag alone neither identifies Meta nor asserts Content Seal. 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. 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/`.
|
||||
|
||||
|
||||
@@ -232,9 +232,8 @@ Output:
|
||||
- `classify` answers from pixels with no provenance behind it, so its `ai` is a
|
||||
guess and its `unknown` is an abstention. Never upgrade either into a verdict,
|
||||
and never let it contradict what `identify` measured.
|
||||
- A Meta AI (Muse Image) export can retain standalone IPTC metadata that enables
|
||||
heuristic attribution and automatic routing. The IPTC code is not unique to Meta
|
||||
and does not detect Content Seal in pixels. If metadata was stripped, use
|
||||
- A Meta AI (Muse Image) export can retain standalone IPTC metadata, but the shared
|
||||
code does not identify Meta or detect Content Seal in pixels. Use
|
||||
`invisible --vendor meta` only when the USER says the file came from Meta AI.
|
||||
Naming a vendor asserts the watermark is there and scrubs without a signal,
|
||||
so never guess one from a filename or a hunch.
|
||||
|
||||
@@ -78,10 +78,9 @@ cohort asserts the watermark IS present, so it also runs the scrub without a
|
||||
local signal, exactly like `--force`. Set it only when the USER says where the
|
||||
file came from. `--vendor` is on `invisible`, `all` and `batch`.
|
||||
|
||||
Meta Muse Image exports can retain standalone IPTC metadata that enables
|
||||
heuristic attribution and automatic routing. That generic code is not unique
|
||||
to Meta and does not detect Content Seal in pixels. After metadata is stripped,
|
||||
`--vendor meta` supplies the route only when the user confirms the source.
|
||||
Meta Muse Image exports can retain standalone IPTC metadata, but that generic
|
||||
code does not identify Meta or detect Content Seal in pixels. `--vendor meta`
|
||||
supplies the route only when the user confirms the source.
|
||||
There is no `--model`, `--steps`, or `--guidance-scale` flag, and no `--device`
|
||||
on the image path: auto-detection already finds the only device these profiles
|
||||
run on.
|
||||
|
||||
@@ -231,15 +231,3 @@ AI_GENERATOR_TOKENS = frozenset(
|
||||
|
||||
_C2PA_ACTION_NAMES = _tokens("created|converted|edited|filtered|cropped|resized|opened|placed|watermarked.unbound")
|
||||
C2PA_ACTIONS = {f"c2pa.{action}".encode(): action for action in _C2PA_ACTION_NAMES}
|
||||
|
||||
|
||||
# TC260 producer identity -> the mark key whose vendor signs with it now lives on the
|
||||
# registry rows (``KnownMark.tc260_producer_codes``, read through
|
||||
# ``watermark_registry.tc260_producer_vendors``). Keeping the codes beside the mark is
|
||||
# what stops a newly registered TC260 vendor from silently falling back to ByteDance.
|
||||
#
|
||||
# What a TC260 label confirms when its producer is absent or unmapped. Historical
|
||||
# behavior, kept as the fallback so an unrecognized producer never regresses to no
|
||||
# relaxation at all: ByteDance's two products are the ones the relaxed band was
|
||||
# calibrated on (see _text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR).
|
||||
TC260_FALLBACK_VENDORS: frozenset[str] = frozenset({"doubao", "jimeng"})
|
||||
@@ -113,10 +113,9 @@ QWEN_ZIMAGE_OPENAI_STRENGTH = 0.07675
|
||||
# measured corpus margin, not a universal InvisMark threshold.
|
||||
QWEN_ZIMAGE_MICROSOFT_STRENGTH = 0.15
|
||||
|
||||
# Meta Muse Image's retained standalone IPTC AI tag selects this floor through
|
||||
# vendor_for_strength. That shared standard tag is a routing heuristic, not a
|
||||
# vendor-unique signature or a Content Seal pixel detection. Stripped outputs
|
||||
# need an explicit --vendor meta override when their origin is known.
|
||||
# Meta Muse Image's retained standalone IPTC AI tag is not a vendor-unique
|
||||
# signature or a Content Seal pixel detection, so this floor requires an explicit
|
||||
# --vendor meta override when the origin is independently known.
|
||||
# Derivation (oracle meta.ai/identification, 2026-08-26/27, corpus in
|
||||
# data/contentseal/): five independent 2.56 MP generations bracketed at
|
||||
# lighthouse (0.0525, 0.06], fox (0.03, 0.0375], night_city (0.03, 0.0375],
|
||||
@@ -310,15 +309,9 @@ def resolve_strength(
|
||||
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."""
|
||||
OpenAI / Google / Microsoft come from product-specific C2PA evidence. Meta has no
|
||||
automatic route because its companion IPTC ``trainedAlgorithmicMedia`` value is a
|
||||
shared standard, not manufacturer evidence; callers can select it explicitly."""
|
||||
try:
|
||||
from remove_ai_watermarks._internal.c2pa import (
|
||||
c2pa_info_has_invalid_credential,
|
||||
@@ -337,25 +330,4 @@ 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)
|
||||
@@ -61,8 +61,9 @@ def visible_provenance(source: str | Path) -> frozenset[str]:
|
||||
|
||||
Mapping: a Google/Gemini C2PA issuer -> ``"gemini"``; a ``samsung_genai`` marker ->
|
||||
``"samsung"``; a China-AIGC (TC260) label -> the vendor its ``ContentProducer``
|
||||
names (``KnownMark.tc260_producer_codes``), falling back to ByteDance's two products
|
||||
when the producer is absent or unmapped.
|
||||
names (``KnownMark.tc260_producer_codes``). An absent or unmapped producer confirms
|
||||
no particular product: the TC260 standard identifies neither a manufacturer nor a
|
||||
product.
|
||||
|
||||
The TC260 label itself says only "this is AI", so it used to relax Doubao and
|
||||
Jimeng on EVERY China-AIGC image -- including one carrying a Qwen or Kling mark,
|
||||
@@ -87,24 +88,21 @@ def visible_provenance(source: str | Path) -> frozenset[str]:
|
||||
def _tc260_vendors(path: Path) -> frozenset[str]:
|
||||
"""Vendor keys a TC260 label confirms, from its ``ContentProducer`` identity.
|
||||
|
||||
An absent, unreadable or unmapped producer falls back to the historical pair rather
|
||||
than to nothing: the caller has already established that the AIGC signal fired, so
|
||||
the image IS China-AIGC labeled, and dropping to no relaxation would lose the
|
||||
detections the fallback recovers today. The re-read is deliberately isolated -- a
|
||||
failure here must narrow the answer, never discard the rest of the provenance.
|
||||
An absent, unreadable or unmapped producer confirms no particular product. The
|
||||
caller has established only that the AIGC signal fired; a shared standard cannot
|
||||
safely relax one manufacturer's detector. The re-read is deliberately isolated --
|
||||
a failure here must narrow the answer, never discard the rest of the provenance.
|
||||
"""
|
||||
import contextlib
|
||||
|
||||
from remove_ai_watermarks._internal.constants import TC260_FALLBACK_VENDORS
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
from remove_ai_watermarks.metadata import aigc_label, uscc_of
|
||||
from remove_ai_watermarks.watermark_registry import tc260_producer_vendors
|
||||
from remove_ai_watermarks.watermark_registry import tc260_producer_mark
|
||||
|
||||
producer = (aigc_label(path) or {}).get("ContentProducer", "")
|
||||
if producer and (vendor := tc260_producer_vendors().get(uscc_of(producer))):
|
||||
return frozenset({vendor})
|
||||
return TC260_FALLBACK_VENDORS
|
||||
if producer and (mark := tc260_producer_mark(uscc_of(producer))):
|
||||
return frozenset({mark.key})
|
||||
return frozenset()
|
||||
|
||||
|
||||
def _load_visible_input(source: str | Path | NDArray[Any]) -> _VisibleInput:
|
||||
|
||||
@@ -136,11 +136,6 @@ _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."
|
||||
@@ -822,6 +817,16 @@ _AI_VENDOR_TOKENS: tuple[tuple[str, str], ...] = (
|
||||
("dreamina", "ByteDance"),
|
||||
("volcengine", "ByteDance"),
|
||||
("volcano engine", "ByteDance"),
|
||||
("qwen", "Alibaba"),
|
||||
("alibaba", "Alibaba"),
|
||||
("kling", "Kuaishou"),
|
||||
("kuaishou", "Kuaishou"),
|
||||
("yuanbao", "Tencent"),
|
||||
("tencent", "Tencent"),
|
||||
("runninghub", "RunningHub"),
|
||||
("baidu", "Baidu"),
|
||||
("ernie", "Baidu"),
|
||||
("liblib", "LiblibAI"),
|
||||
("canva", "Canva"),
|
||||
("elevenlabs", "ElevenLabs"),
|
||||
("eleven labs", "ElevenLabs"),
|
||||
@@ -860,16 +865,16 @@ def _vendor_of(text: str | None) -> str | None:
|
||||
_C2PA_MANIFEST_SOURCE = "c2pa_manifest"
|
||||
_CLASH_SOURCE: dict[str, str] = {"c2pa": _C2PA_MANIFEST_SOURCE, "synthid": _C2PA_MANIFEST_SOURCE}
|
||||
|
||||
# The generic China TC260 AIGC vendor label -- a COUNTRY-LEVEL regulatory "this is AI"
|
||||
# stamp any Chinese generator applies to its own output, naming no specific vendor.
|
||||
_GENERIC_AIGC_VENDOR = "China AIGC (TC260)"
|
||||
# Vendors that apply the TC260 label to their OWN output. When one is co-attributed with
|
||||
# the generic AIGC label, the label is that vendor's own stamp (not an independent
|
||||
# competing origin), so the clash check attributes the AIGC label to it -- else a legit
|
||||
# ByteDance/Doubao image (C2PA "ByteDance" + its own TC260 label) would false-clash once
|
||||
# ByteDance normalizes via _vendor_of. Chinese generators only (Canva/BFL/ElevenLabs,
|
||||
# also added to _vendor_of, are NOT TC260 appliers).
|
||||
_TC260_VENDORS: frozenset[str] = frozenset({"ByteDance"})
|
||||
|
||||
def _tc260_manufacturer_of(producer: str) -> str | None:
|
||||
"""Resolve a TC260 producer to a normalized manufacturer, if registered."""
|
||||
if not producer:
|
||||
return None
|
||||
from remove_ai_watermarks.metadata import uscc_of
|
||||
from remove_ai_watermarks.watermark_registry import tc260_producer_mark
|
||||
|
||||
mark = tc260_producer_mark(uscc_of(producer))
|
||||
return _vendor_of(mark.manufacturer if mark else None)
|
||||
|
||||
|
||||
def _integrity_clashes(
|
||||
@@ -895,18 +900,6 @@ def _integrity_clashes(
|
||||
# families clash only when they belong to different provenance sources (see
|
||||
# _CLASH_SOURCE) AND name different vendors -- so multiple vendors named within
|
||||
# one C2PA manifest (C2PA issuer + SynthID provenance) do not flag.
|
||||
# The generic TC260 AIGC label is a Chinese regulatory "this is AI" stamp. When a
|
||||
# Chinese TC260-applying vendor (ByteDance) is ALSO attributed, the label is that
|
||||
# vendor's own stamp on its own output, so attribute it to that vendor -- a legit
|
||||
# Doubao image carries BOTH a ByteDance C2PA manifest and its own TC260 label and
|
||||
# must not clash. Against a NON-TC260 vendor (OpenAI, Google, ...) the label stays
|
||||
# generic and still clashes as a laundering tell (a foreign-vendor image carrying a
|
||||
# Chinese TC260 label names two different origins).
|
||||
if ai_vendors.get("aigc") == _GENERIC_AIGC_VENDOR:
|
||||
own = next((v for f, v in ai_vendors.items() if f != "aigc" and v in _TC260_VENDORS), None)
|
||||
if own:
|
||||
ai_vendors = {**ai_vendors, "aigc": own} # copy co-located with the relabel
|
||||
|
||||
source = {fam: _CLASH_SOURCE.get(fam, fam) for fam in ai_vendors}
|
||||
independent_conflict = any(
|
||||
source[a] != source[b] and ai_vendors[a] != ai_vendors[b] for a, b in itertools.combinations(ai_vendors, 2)
|
||||
@@ -1376,38 +1369,12 @@ 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"
|
||||
# next to compositeWithTrainedAlgorithmicMedia. It was detected but
|
||||
# previously never attributed.
|
||||
if b"Apple Photos Clean Up" in head:
|
||||
platform = "Apple Photos (Clean Up AI edit)"
|
||||
else:
|
||||
# The platform line follows the same measured bet the seal signal
|
||||
# and the strength router make: Muse Image is the tag writer whose
|
||||
# outputs this profile targets, so a hedged Muse attribution is
|
||||
# more useful than "platform not specified" while the panel below
|
||||
# already prices the Content Seal removal. The hedge stays in the
|
||||
# wording - it names the attribution basis, not a detection.
|
||||
platform = "Meta Muse Image (attributed by the standalone AI digital-source tag)"
|
||||
# Apple Photos Clean Up carries an explicit product credit beside the shared
|
||||
# IPTC value. Without such product-specific evidence the standard names no
|
||||
# platform and must not imply a vendor watermark.
|
||||
if platform is None and b"Apple Photos Clean Up" in head:
|
||||
platform = "Apple Photos (Clean Up AI edit)"
|
||||
|
||||
# ── IPTC 2025.1 AI-disclosure fields (Iptc4xmpExt:AISystemUsed etc.) ─
|
||||
iptc_ai = any(m in head for m in IPTC_AI_FIELD_MARKERS)
|
||||
@@ -1435,8 +1402,9 @@ def _identify_from_evidence(
|
||||
signals.append(Signal("aigc", f"TC260 AIGC label{f' (producer {producer})' if producer else ''}", "high"))
|
||||
watermarks.append("China AIGC label (TC260 standard)")
|
||||
if platform is None:
|
||||
platform = "China AIGC-labeled generator (TC260; e.g. Doubao)"
|
||||
ai_vendor_claims["aigc"] = _GENERIC_AIGC_VENDOR
|
||||
platform = "China AIGC-labeled content (TC260 standard)"
|
||||
if manufacturer := _tc260_manufacturer_of(producer):
|
||||
ai_vendor_claims["aigc"] = manufacturer
|
||||
|
||||
# ── Local diffusion parameters (Stable Diffusion / ComfyUI) ──────
|
||||
local_keys = sorted(k for k in meta if k.lower() in _LOCAL_GEN_KEYS)
|
||||
|
||||
@@ -290,8 +290,7 @@ class KnownMark:
|
||||
# Social Credit Codes as normalized by ``metadata.uscc_of``, plus the bare product
|
||||
# names a few generators write instead. Here rather than in a separate table
|
||||
# because a newly registered TC260 mark whose codes were forgotten fails SILENTLY:
|
||||
# it falls back to relaxing ByteDance's pair on an image carrying the new mark,
|
||||
# the exact false positive the producer code exists to prevent.
|
||||
# its own detector remains strict even on an image carrying that producer's label.
|
||||
tc260_producer_codes: tuple[str, ...] = ()
|
||||
# Optional single-pass dual verdict for the arbiter's perception stage (see
|
||||
# `detect_both`). None = fall back to two `_detect` calls.
|
||||
@@ -886,16 +885,9 @@ def resolve_trust(
|
||||
return "confirmed" if confirmed else "strict"
|
||||
|
||||
|
||||
def tc260_producer_vendors() -> dict[str, str]:
|
||||
"""TC260 ``ContentProducer`` identity -> the mark key whose vendor signs with it.
|
||||
|
||||
Derived from the registry rows, so registering a TC260 mark and its producer codes
|
||||
is one edit. A mark registered without codes falls through to
|
||||
:data:`TC260_FALLBACK_VENDORS`, which relaxes ByteDance's pair -- a silent wrong
|
||||
answer on an image carrying the new mark, which is why the codes belong on the row
|
||||
next to ``label_regime`` rather than in a table someone must remember to update.
|
||||
"""
|
||||
return {code: mark.key for mark in _REGISTRY for code in mark.tc260_producer_codes}
|
||||
def tc260_producer_mark(code: str) -> KnownMark | None:
|
||||
"""Return the registry row for one TC260 ``ContentProducer`` identity."""
|
||||
return next((mark for mark in _REGISTRY if code in mark.tc260_producer_codes), None)
|
||||
|
||||
|
||||
def _pill_suppressors() -> set[str]:
|
||||
|
||||
+10
-16
@@ -136,9 +136,9 @@ class TestVisibleProvenance:
|
||||
assert "doubao" in prov
|
||||
assert "jimeng" not in prov
|
||||
|
||||
def test_unmapped_tc260_producer_falls_back_to_the_bytedance_pair(self, monkeypatch, tmp_path):
|
||||
"""An unrecognized producer must not lose the relaxation entirely: the label is
|
||||
still evidence that some China-AIGC vendor made the image."""
|
||||
@pytest.mark.parametrize("producer", ["0011999999999999999999999", ""])
|
||||
def test_tc260_without_a_mapped_producer_does_not_confirm_a_product(self, monkeypatch, tmp_path, producer):
|
||||
"""A shared labeling standard cannot identify a product by itself."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from remove_ai_watermarks import identify, metadata
|
||||
@@ -148,8 +148,8 @@ class TestVisibleProvenance:
|
||||
"identify",
|
||||
lambda *a, **k: SimpleNamespace(platform=None, signals=[SimpleNamespace(name="aigc")]),
|
||||
)
|
||||
monkeypatch.setattr(metadata, "aigc_label", lambda _p: {"ContentProducer": "0011999999999999999999999"})
|
||||
assert raiw.visible_provenance(tmp_path / "x.png") == frozenset({"doubao", "jimeng"})
|
||||
monkeypatch.setattr(metadata, "aigc_label", lambda _p: {"ContentProducer": producer})
|
||||
assert raiw.visible_provenance(tmp_path / "x.png") == frozenset()
|
||||
|
||||
def test_known_tc260_producer_names_a_single_vendor(self, monkeypatch, tmp_path):
|
||||
from types import SimpleNamespace
|
||||
@@ -165,16 +165,10 @@ class TestVisibleProvenance:
|
||||
monkeypatch.setattr(metadata, "aigc_label", lambda _p: {"ContentProducer": "001191440101MA9Y9T4H7A00001"})
|
||||
assert raiw.visible_provenance(tmp_path / "x.png") == frozenset({"qwen"})
|
||||
|
||||
def test_every_mapped_producer_names_a_registered_mark(self):
|
||||
"""The table drives the arbiter's provenance set, so a typo'd key would relax
|
||||
nothing and fail silently. Checked here, not at import: importing the registry
|
||||
from _internal.constants would drag it into every metadata-only path."""
|
||||
from remove_ai_watermarks._internal.constants import TC260_FALLBACK_VENDORS
|
||||
from remove_ai_watermarks.watermark_registry import known_marks, mark_keys, tc260_producer_vendors
|
||||
def test_every_primary_tc260_mark_declares_a_producer_code(self):
|
||||
"""Only companion pills may inherit a primary mark's producer identity."""
|
||||
from remove_ai_watermarks.watermark_registry import known_marks
|
||||
|
||||
keys = set(mark_keys())
|
||||
assert set(tc260_producer_vendors().values()) <= keys
|
||||
assert keys >= TC260_FALLBACK_VENDORS
|
||||
# Primary TC260 marks name their producer. Companion pills inherit the primary
|
||||
# mark's product-level provenance rather than duplicating an external identity.
|
||||
unmapped = {m.key for m in known_marks() if m.label_regime == "tc260" and not m.tc260_producer_codes}
|
||||
@@ -189,7 +183,7 @@ class TestVisibleProvenance:
|
||||
assert raiw.visible_provenance(tmp_path / "missing.png") == frozenset()
|
||||
|
||||
def test_uses_report_signals_for_falsy_metadata_values(self, monkeypatch, tmp_path):
|
||||
"""An empty TC260 object and Samsung genAIType=0 are still present signals.
|
||||
"""An empty TC260 object names no product; Samsung genAIType=0 is present.
|
||||
|
||||
The report has already normalized those values, so the public API must not
|
||||
re-read the file and accidentally discard them by truthiness.
|
||||
@@ -204,7 +198,7 @@ class TestVisibleProvenance:
|
||||
)
|
||||
monkeypatch.setattr(identify, "identify", lambda *args, **kwargs: report)
|
||||
|
||||
assert raiw.visible_provenance(tmp_path / "synthetic.png") == frozenset({"doubao", "jimeng", "samsung"})
|
||||
assert raiw.visible_provenance(tmp_path / "synthetic.png") == frozenset({"samsung"})
|
||||
|
||||
|
||||
class TestRemoveVisibleOutputPath:
|
||||
|
||||
+26
-58
@@ -30,6 +30,7 @@ from remove_ai_watermarks.identify import (
|
||||
_attribute_platform,
|
||||
_integrity_clashes,
|
||||
_issuers_in,
|
||||
_tc260_manufacturer_of,
|
||||
_vendor_of,
|
||||
evidence_from_metadata_record,
|
||||
extract_provenance_evidence,
|
||||
@@ -731,6 +732,8 @@ class TestIdentifyRealSamples:
|
||||
assert r.is_ai_generated is True
|
||||
assert r.platform == "Apple Photos (Clean Up AI edit)"
|
||||
assert r.ai_source_kind == "enhanced"
|
||||
assert "content_seal" not in [signal.name for signal in r.signals]
|
||||
assert not any("Meta" in watermark for watermark in r.watermarks)
|
||||
|
||||
def test_standalone_iptc_composite_synthetic_is_enhanced(self, tmp_path: Path):
|
||||
p = tmp_path / "composite.jpg"
|
||||
@@ -743,15 +746,8 @@ 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.
|
||||
"""
|
||||
def test_standalone_ai_tag_does_not_claim_a_vendor_watermark(self, tmp_path: Path):
|
||||
"""The shared IPTC standard proves neither Meta nor Content Seal."""
|
||||
p = tmp_path / "muse-tag.jpg"
|
||||
p.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
|
||||
@@ -761,38 +757,9 @@ class TestIdentifyRealSamples:
|
||||
|
||||
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_seal_platform_attribution_follows_the_signal(self, tmp_path: Path):
|
||||
"""The Likely-source line follows the same bet the seal signal makes.
|
||||
|
||||
Apple keeps its own attribution; every other standalone-tag file gets the
|
||||
hedged Muse attribution instead of "platform not specified", so the panel
|
||||
that prices the Content Seal and the source line agree.
|
||||
"""
|
||||
muse = tmp_path / "muse.jpg"
|
||||
muse.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
|
||||
)
|
||||
apple = tmp_path / "apple.jpg"
|
||||
apple.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="compositeWithTrainedAlgorithmicMedia" '
|
||||
b'photoshop:Credit="Apple Photos Clean Up"></x:xmpmeta>\xff\xd9'
|
||||
)
|
||||
|
||||
assert identify(muse, check_visible=False, check_invisible=False).platform == (
|
||||
"Meta Muse Image (attributed by the standalone AI digital-source tag)"
|
||||
)
|
||||
assert identify(apple, check_visible=False, check_invisible=False).platform == "Apple Photos (Clean Up AI edit)"
|
||||
|
||||
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]
|
||||
assert "content_seal" not in names
|
||||
assert r.platform is None
|
||||
assert not any("Content Seal" in watermark for watermark in r.watermarks)
|
||||
|
||||
def test_flux_bfl_c2pa_png(self):
|
||||
# flux-1.png: real Black Forest Labs FLUX.2 Playground output (signed C2PA).
|
||||
@@ -1541,7 +1508,7 @@ class TestIdentifyInvisibleWatermark:
|
||||
|
||||
|
||||
class TestIdentifyAIGC:
|
||||
"""China TC260 AIGC label is detected and attributed (e.g. Doubao)."""
|
||||
"""China TC260 AIGC labels are detected without guessing the product."""
|
||||
|
||||
def _aigc_png(self, tmp_path: Path) -> Path:
|
||||
from PIL import Image
|
||||
@@ -1644,6 +1611,11 @@ class TestVendorOf:
|
||||
|
||||
|
||||
class TestIntegrityClashesHelper:
|
||||
def test_tc260_manufacturer_comes_from_the_registered_producer(self):
|
||||
assert _tc260_manufacturer_of("001191440101MA9Y9T4H7A00001") == "Alibaba"
|
||||
assert _tc260_manufacturer_of("0011999999999999999999999") is None
|
||||
assert _tc260_manufacturer_of("") is None
|
||||
|
||||
def test_two_ai_vendors_clash(self):
|
||||
clashes = _integrity_clashes({"c2pa": "OpenAI", "exif_generator": "Ideogram"}, None, camera_has_ai_marker=True)
|
||||
assert len(clashes) == 1
|
||||
@@ -1665,19 +1637,11 @@ class TestIntegrityClashesHelper:
|
||||
== []
|
||||
)
|
||||
|
||||
def test_bytedance_c2pa_plus_own_aigc_no_clash(self):
|
||||
# A legit ByteDance/Doubao image carries BOTH a ByteDance C2PA manifest and its
|
||||
# own China TC260 AIGC label. The label is ByteDance's own regulatory stamp, so
|
||||
# it must be attributed to ByteDance and NOT read as a competing origin.
|
||||
assert (
|
||||
_integrity_clashes({"c2pa": "ByteDance", "aigc": "China AIGC (TC260)"}, None, camera_has_ai_marker=True)
|
||||
== []
|
||||
)
|
||||
def test_matching_tc260_manufacturer_no_clash(self):
|
||||
assert _integrity_clashes({"c2pa": "ByteDance", "aigc": "ByteDance"}, None, camera_has_ai_marker=True) == []
|
||||
|
||||
def test_foreign_vendor_plus_aigc_still_clashes(self):
|
||||
# But a NON-Chinese vendor's C2PA next to a China TC260 label names two different
|
||||
# origins -- a laundering tell that must still fire (the generic label stays generic).
|
||||
clashes = _integrity_clashes({"c2pa": "OpenAI", "aigc": "China AIGC (TC260)"}, None, camera_has_ai_marker=True)
|
||||
def test_different_tc260_manufacturer_clashes(self):
|
||||
clashes = _integrity_clashes({"c2pa": "OpenAI", "aigc": "ByteDance"}, None, camera_has_ai_marker=True)
|
||||
assert len(clashes) == 1
|
||||
assert "Conflicting AI-origin" in clashes[0]
|
||||
|
||||
@@ -1754,9 +1718,13 @@ class TestIntegrityClashEndToEnd:
|
||||
|
||||
def test_two_generator_stamps_clash(self, tmp_path: Path):
|
||||
# An OpenAI C2PA manifest (AI source) on an image that ALSO carries a
|
||||
# China TC260 AIGC label = two independent generator stamps naming
|
||||
# different origins -> a laundering tell.
|
||||
path = self._c2pa_jpeg(tmp_path, b"OpenAI ... trainedAlgorithmicMedia ... TC260:AIGC label")
|
||||
# Qwen TC260 AIGC label = two independent generator stamps naming
|
||||
# different manufacturers -> a laundering tell.
|
||||
path = self._c2pa_jpeg(
|
||||
tmp_path,
|
||||
b"OpenAI ... trainedAlgorithmicMedia ... "
|
||||
b'<TC260:AIGC>{"Label":"1","ContentProducer":"91440101MA9Y9T4H7A"}</TC260:AIGC>',
|
||||
)
|
||||
r = identify(path, check_visible=False, check_invisible=False)
|
||||
assert r.integrity_clashes
|
||||
assert any("Conflicting AI-origin" in c for c in r.integrity_clashes)
|
||||
@@ -1773,7 +1741,7 @@ class TestIntegrityClashEndToEnd:
|
||||
# the provenance is inconsistent (a laundering / spoofing tell).
|
||||
path = self._c2pa_jpeg(
|
||||
tmp_path,
|
||||
b'Pixel Camera ... <TC260:AIGC>{"Label":"1","ContentProducer":"BYTEDANCE001"}</TC260:AIGC>',
|
||||
b'Pixel Camera ... <TC260:AIGC>{"Label":"1","ContentProducer":"91110102MACQD9K640"}</TC260:AIGC>',
|
||||
)
|
||||
r = identify(path, check_visible=False, check_invisible=False)
|
||||
assert r.platform == "Google Pixel (camera, C2PA capture)"
|
||||
|
||||
@@ -263,16 +263,13 @@ class TestResolveStrength:
|
||||
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."""
|
||||
def test_vendor_for_strength_does_not_treat_shared_iptc_as_meta(self, tmp_path):
|
||||
"""A standard AI tag does not identify the Meta strength cohort."""
|
||||
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"
|
||||
assert vendor_for_strength(tagged) is None
|
||||
|
||||
stripped = tmp_path / "stripped.webp"
|
||||
stripped.write_bytes(b"RIFF\x24\x00\x00\x00WEBPVP8 \x18\x00\x00\x00" + b"\x00" * 16)
|
||||
|
||||
@@ -694,6 +694,10 @@ class TestMarkKnowledgeIsOnTheRow:
|
||||
assert manufacturers["kling"] == "kuaishou"
|
||||
assert manufacturers["yuanbao"] == "tencent"
|
||||
|
||||
def test_tc260_producer_lookup_returns_the_registry_row(self):
|
||||
assert reg.tc260_producer_mark("91440101MA9Y9T4H7A") is reg.get_mark("qwen")
|
||||
assert reg.tc260_producer_mark("unknown") is None
|
||||
|
||||
def test_platform_token_marks_are_the_c2pa_attributed_ones(self):
|
||||
# Gemini (Google C2PA) and Microsoft (issuer "Microsoft") are the marks whose
|
||||
# vendor a C2PA platform string can confirm; every other mark reaches its
|
||||
|
||||
Reference in new issue
Block a user