mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-21 07:00:56 +02:00
fix(identify): device-token-first C2PA attribution; add verified Pixel (v0.6.2)
Replaces the claim-generator-string match with a distinctive device-token scan of the manifest bytes (_device_platform / _DEVICE_C2PA_PLATFORM), which is more robust: it catches devices where the generator name lives under a non-standard CBOR key (Pixel uses `claim_generator_info`, so it has no `claim_generator`). - Adds Google Pixel, verified against a real Pixel 10 Pro C2PA file (attached to c2pa-rs issue #1609/#1554): cert CN "Pixel Camera", digitalSourceType `computationalCapture` -> capture authenticity, not AI (is_ai stays None). - Token distinctiveness is load-bearing: bare "Truepic" matched the OpenAI chatgpt-1.png fixture (Truepic is a trust-chain signing authority), so the token is the specific "Truepic_Lens"; "Pixel Camera" (cert CN) not "Pixel". - Verified Leica/Nikon/Truepic/Pixel attribute correctly and OpenAI/Adobe/MJ do not regress. Sony/Canon/Samsung/Bria stay unmapped: no public direct- download C2PA sample exists to verify their in-manifest string. - Regression tests: device token beats incidental issuer mentions (Leica, Pixel-vs-Google). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r
|
||||
## How to run
|
||||
|
||||
- `uv run remove-ai-watermarks all <image.png> -o <output.png>`
|
||||
- `uv run remove-ai-watermarks visible <image.png> -o <out.png>` — visible-mark removal, CPU, no GPU. `--mark auto` (default) routes between the Gemini sparkle and the Doubao "豆包AI生成" text strip by detector confidence; `--mark gemini` / `--mark doubao` force one.
|
||||
- `uv run remove-ai-watermarks erase <image.png> --region x,y,w,h -o <out.png>` — universal region eraser (any logo/object, any position). `--backend cv2` (default, no deps) or `--backend lama` (big-LaMa via onnxruntime, extra `lama`); `--region` is repeatable.
|
||||
- `uv run remove-ai-watermarks identify <image>` — provenance verdict (platform + watermark inventory + confidence); `--json` for machine output, `--no-visible` to skip the cv2 sparkle detector
|
||||
- `uv run remove-ai-watermarks metadata <image.png> --check` — inspect AI metadata (C2PA, EXIF, PNG chunks)
|
||||
- `uv run remove-ai-watermarks metadata <image.png> --remove -o <out.png>` — strip all AI metadata
|
||||
@@ -28,12 +30,18 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r
|
||||
- `noai/c2pa.py` — PNG chunk parser; use `extract_c2pa_chunk(path)` to get raw caBX payload, `has_c2pa_metadata(path)` to detect. Do not reimplement chunk parsing. `extract_c2pa_info(path)` sets `synthid_watermark`/`synthid_vendors` when the manifest is signed by a SynthID-using vendor, and `soft_binding`/`soft_binding_vendors` when a `c2pa.soft-binding` `alg` names a forensic-watermark vendor (`soft_binding_vendors_in(buffer)` is the shared byte-scan, used by both the PNG parser and the non-PNG binary path).
|
||||
- `noai/constants.py` — PNG_SIGNATURE, C2PA_CHUNK_TYPE, C2PA_SIGNATURES, C2PA_ISSUERS, `SYNTHID_C2PA_ISSUERS` (issuers that pair SynthID with C2PA: Google, OpenAI), and `C2PA_SOFT_BINDINGS` (soft-binding `alg` prefix → forensic-watermark vendor: Adobe TrustMark, Digimarc, Imatag, Steg.AI, Microsoft, ...). Add a new issuer/binding here, not inline.
|
||||
- `metadata.py` — `synthid_source(path)` returns the vendor name(s) if the C2PA manifest implies a SynthID pixel watermark, else None. Format-agnostic: PNG via the caBX parser, JPEG/WebP/AVIF/HEIF/JXL via a binary scan (C2PA marker + SynthID issuer + AI-source marker). `get_ai_metadata` surfaces the verdict, and `metadata --check` prints it as a callout. Both `get_ai_metadata` and `has_ai_metadata` guard the PIL open with `except Exception` (HEIC/unknown formats raise non-OSError) and fall through to the binary scan. `xai_signature(path)` detects xAI/Grok's EXIF-only scheme (`ImageDescription` = `Signature: <base64>` + UUID `Artist`); it feeds `has_ai_metadata`, `get_ai_metadata` (key `xai_signature`), and `identify`. `iptc_ai_system(path)` detects the IPTC Photo Metadata 2025.1 AI-disclosure XMP properties (`IPTC_AI_FIELD_MARKERS` = `AISystemUsed`/`AISystemVersionUsed`/`AIPromptInformation`/`AIPromptWriterName`) and returns the `AISystemUsed` generator name (or `"fields present"`). `remove_ai_metadata` routes **ISOBMFF video** (`.mp4`/`.mov`/`.m4v`) through the same `isobmff.strip_c2pa_boxes` as AVIF/HEIF (MP4 is ISOBMFF), and `_scrub_ai_exif` removes the xAI signature + AI-generator EXIF tags on JPEG output.
|
||||
- `identify.py` — `identify(path)` aggregates every locally-readable signal (C2PA issuer→platform, C2PA soft-binding forensic-watermark vendor, IPTC "Made with AI" + IPTC 2025.1 `AISystemUsed`, embedded SD/ComfyUI params, SynthID proxy, xAI/Grok EXIF signature via `metadata.xai_signature`, visible Gemini sparkle, open invisible watermark, Adobe TrustMark via `trustmark_detector`) into one `ProvenanceReport`. `is_ai_generated` is True or None (never asserted False — stripped metadata is not proof of clean origin). Visible-sparkle is promoted only at confidence ≥ `_SPARKLE_THRESHOLD` (0.5; corpus-tuned to separate Gemini sparkles ≥0.56 from non-sparkle ≤0.49). The cv2 dependency lives in `gemini_engine.detect_sparkle_confidence`, not here. **C2PA platform attribution is claim-generator-first, issuer-scan fallback** (`_CLAIM_GENERATOR_PLATFORM` then `_ISSUER_PLATFORM`): the claim generator (via the now-public `c2pa.cbor_text_after(head, b"claim_generator")` for non-PNG) names what *produced* the asset, while the issuer byte-scan matches incidental chain mentions. **Verified on real signed files 2026-05-26:** the old issuer-only scan mis-attributed Leica→"Truepic" (timestamp authority), Nikon→"Adobe Firefly" (XMP-toolkit "Adobe" + the sample's "Adobe_MAX" name), and Truepic→"Google"; claim-generator-first fixes all three. `_CLAIM_GENERATOR_PLATFORM` only lists device tokens **verified against a real C2PA file** (Leica `lc_c2pa`, Nikon, Truepic Lens) -- Pixel/Samsung/Sony/Canon/Bria are documented but NOT added until a real signed sample confirms the in-manifest string (same fixture discipline as Grok/Doubao/Ideogram). Camera C2PA marks capture authenticity, not AI, so these never set `is_ai` (that stays driven by digital-source-type). Add platform mappings to `_CLAIM_GENERATOR_PLATFORM` / `_ISSUER_PLATFORM`, not inline. For non-PNG containers (JPEG/WebP/AVIF/HEIF/JXL) the caBX parser returns nothing, so issuer (`_issuers_in`) and generator (`_ai_tools_in`, reusing `C2PA_AI_TOOLS`) are recovered by binary-scanning the first MB. EXIF `Software` / `Make` / `Artist` / `ImageDescription` and XMP `CreatorTool` generator tags are read by `metadata.exif_generator` (PIL+piexif for any format PIL opens incl. AVIF, plus a container-agnostic XMP raw-byte scan that also covers HEIF/JXL), matched against `AI_GENERATOR_TOKENS` so ordinary editors (plain "Adobe Photoshop") and real-camera `Make` ("Apple"/"Canon") are not flagged. **Ideogram tags its output with EXIF `Make="Ideogram AI"`** (verified on a real download 2026-05-24) — that's why `Make` is read.
|
||||
- `identify.py` — `identify(path)` aggregates every locally-readable signal (C2PA issuer→platform, C2PA soft-binding forensic-watermark vendor, IPTC "Made with AI" + IPTC 2025.1 `AISystemUsed`, embedded SD/ComfyUI params, SynthID proxy, xAI/Grok EXIF signature via `metadata.xai_signature`, visible Gemini sparkle, open invisible watermark, Adobe TrustMark via `trustmark_detector`) into one `ProvenanceReport`. `is_ai_generated` is True or None (never asserted False — stripped metadata is not proof of clean origin). Visible-sparkle is promoted only at confidence ≥ `_SPARKLE_THRESHOLD` (0.5; corpus-tuned to separate Gemini sparkles ≥0.56 from non-sparkle ≤0.49). The cv2 dependency lives in `gemini_engine.detect_sparkle_confidence`, not here. **C2PA platform attribution is device-token-first, issuer-scan fallback** (`_device_platform` scans manifest bytes for `_DEVICE_C2PA_PLATFORM` tokens, then `_attribute_platform`/`_ISSUER_PLATFORM`). **Why, verified on real signed files 2026-05-26:** the old issuer-only byte-scan matched ANY issuer substring anywhere, so multi-entity manifests mis-attributed -- Leica→"Truepic" (a signing authority in the trust chain), Nikon→"Adobe Firefly" (XMP-toolkit "Adobe" + the sample's "Adobe_MAX" name), Pixel→"Google (Gemini)" ("Google LLC" cert org), Truepic→"Google". A distinctive device token wins instead. **Token distinctiveness is load-bearing:** bare `b"Truepic"` mis-fires (it appears in unrelated trust chains -- it mis-attributed the OpenAI `chatgpt-1.png` fixture), so the token is the specific `b"Truepic_Lens"` from the Lens SDK claim generator; likewise `b"Pixel Camera"` (cert CN) not bare `b"Pixel"`. `_DEVICE_C2PA_PLATFORM` lists ONLY tokens **verified against a real C2PA file**: Leica (`lc_c2pa`/`Leica Camera`), Nikon (`NIKON`), Pixel (`Pixel Camera` -- from a real Pixel 10 Pro file attached to c2pa-rs issue #1609/#1554), Truepic (`Truepic_Lens`). Sony/Canon/Samsung/Bria have **no public direct-download C2PA sample** (checked: upload-to-verify or token-gated only), so they stay unmapped until a real file is captured (same fixture discipline as Grok/Doubao). Camera C2PA marks capture authenticity, not AI (Pixel carries `computationalCapture`, not `trainedAlgorithmicMedia`), so these never set `is_ai` -- that stays driven by digital-source-type. `c2pa.cbor_text_after` (now public) is best-effort for the `generator` detail string only and can be None when the manifest keys it `claim_generator_info` (Pixel). Add device tokens to `_DEVICE_C2PA_PLATFORM`, generator/issuer platforms to `_ISSUER_PLATFORM`, not inline. For non-PNG containers (JPEG/WebP/AVIF/HEIF/JXL) the caBX parser returns nothing, so issuer (`_issuers_in`) and generator (`_ai_tools_in`, reusing `C2PA_AI_TOOLS`) are recovered by binary-scanning the first MB. EXIF `Software` / `Make` / `Artist` / `ImageDescription` and XMP `CreatorTool` generator tags are read by `metadata.exif_generator` (PIL+piexif for any format PIL opens incl. AVIF, plus a container-agnostic XMP raw-byte scan that also covers HEIF/JXL), matched against `AI_GENERATOR_TOKENS` so ordinary editors (plain "Adobe Photoshop") and real-camera `Make` ("Apple"/"Canon") are not flagged. **Ideogram tags its output with EXIF `Make="Ideogram AI"`** (verified on a real download 2026-05-24) — that's why `Make` is read.
|
||||
- `gemini_engine.py` — visible Gemini-sparkle remover/detector (cv2/numpy, no GPU). `detect_sparkle_confidence(path)` is the file-level entry point used by `identify.py`.
|
||||
- `doubao_engine.py` — visible Doubao "豆包AI生成" remover/detector (cv2/numpy, no GPU). `DoubaoEngine.locate` anchors a bottom-right box by **geometry** (mark scales with image WIDTH, fractions in module constants; no bundled template), `extract_mask` pulls the light low-saturation glyphs with a **polarity-aware white top-hat** (brighter-than-blurred-local-bg, so white-paper documents are left untouched instead of smeared), `detect` thresholds glyph coverage (`DETECT_MIN_COVERAGE` 0.16 separates real marks ≥0.20 from corner noise, which stays ≤0.06 on large images but can spike to ~0.15 on tiny ones), `remove_watermark` inpaints (cv2 Telea/NS) and **bails when coverage > `MAX_INPAINT_COVERAGE` 0.50** (dense-text background → would smear). Wired into `visible --mark` via `cli._run_doubao_if_selected`. **Logo is near-white (~253), not the gray some third-party tools assume.** Best on photo/illustration backgrounds; high-contrast edges leave faint residue (cv2-inpaint limit). Clean per-pixel reverse-alpha (Gemini-style) is the future upgrade but needs a captured/distilled alpha map — see below.
|
||||
- `region_eraser.py` — universal region eraser (`erase` CLI). `erase(image, boxes=|mask=, backend=)`: `boxes_to_mask` → `cv2.inpaint` (`cv2` backend, default, no deps) or big-LaMa via onnxruntime (`lama` backend, extra `lama`, `Carve/LaMa-ONNX` Apache-2.0 model downloaded on first use, never bundled). `erase_lama` crops a padded region around the mask, runs LaMa at its fixed 512² input, pastes only masked pixels back (untouched areas stay pixel-exact). Lazy `_get_lama_session` singleton; `lama_available()` guards the optional import. **LaMa-ONNX costs ~3.5-4 GB peak RAM and ~5-6 s/call on CPU** (FFC working set, not arena — `enable_cpu_mem_arena=False` does not help), so it does NOT fit a minimal droplet; the cv2 backend (tens of MB, ~30 ms) does. LaMa quality at low RAM = serverless/GPU, mirroring how raiw.cc offloads SDXL to fal.
|
||||
- `invisible_watermark.py` — `detect_invisible_watermark(path)` decodes the OPEN DWT-DCT watermarks (public decoder, no key) embedded by Stable Diffusion / SDXL / FLUX via the `imwatermark` library. Known fixed patterns (verified against upstream source) live in `_BITS_48` (SDXL 48-bit, FLUX.2 48-bit) and `_SD1_STRING` ("StableDiffusionV1", SD 1.x/2.x). Optional dep (extra `detect`); returns None when absent. The `detect` extra pulls **torch** transitively (invisible-watermark declares torch a hard dep, and `WatermarkDecoder` eagerly imports `rivaGan` -> `torch` at import time), so detection needs torch present even though dwtDct runs CPU-only on cv2/numpy/pywavelets — no GPU and no separate `gpu` extra required. **Unlike SynthID this is locally detectable**, but the watermark is fragile (does not survive JPEG re-encode/resize — verified gone after JPEG q90), so it confirms origin only on pristine files. Add new known patterns here. The file carries a top-of-module pyright pragma because imwatermark/cv2 ship no type stubs.
|
||||
- `trustmark_detector.py` — `detect_trustmark(path)` decodes the OPEN, keyless **Adobe TrustMark** watermark (the soft binding behind Adobe Durable Content Credentials, `alg` `com.adobe.trustmark.P`) via the optional `trustmark` package (extra `trustmark`; pulls torch, downloads model weights on first use). Mirrors `invisible_watermark.py` (lazy singleton, top-of-module pyright pragma, returns None when absent). It detects *provenance*, not AI origin as such (TrustMark also marks human-authored content), so `identify` lists it as a watermark without setting `is_ai_generated`. Other soft-binding vendors (Digimarc/Imatag/Steg.AI/...) have no public decoder — they are only *named* via the `C2PA_SOFT_BINDINGS` scan, not decoded.
|
||||
- `face_protector.py` — YOLO detect + soft-blend pattern; mirror this for any "protect region during diffusion" features
|
||||
|
||||
### Doubao clean-reverse-alpha distillation (researched 2026-05-26, NOT yet shipped)
|
||||
|
||||
Plan to get Gemini-style pixel-accurate reverse-alpha for Doubao at cv2 cost (so it runs on a minimal CPU droplet): use LaMa **offline** to reconstruct the clean background `O`, then per-pixel solve `I=(1-α)O+αL` for the alpha map + logo colour (this is Google's CVPR-2017 multi-image idea, with LaMa standing in for the multi-image optimisation). Validated in principle (glyph error drops as alignment improves) but **blocked on data**, confirmed empirically over 6 attempts: a clean canonical alpha map needs **several PRISTINE images at ONE resolution** with varied backgrounds. The findable public Doubao images (ayaya, Tools_CleanMark, ours) are all different resolutions AND lossy (jpg/webp), and Doubao's mark position drifts ~±50 px even at the same resolution, so cross-image registration (ECC / phase-correlation) fails (1/11 templates aligned) — error floor ~64/255 on glyph pixels, not shippable. To finish: ~5-8 pristine same-resolution Doubao originals → per-image α via LaMa → align the clean α templates → median → ship the tiny map + an NCC localiser (Doubao's mark drifts, so runtime needs the same locate-then-reverse-alpha the Gemini engine already does). Don't retry on lossy/mixed-resolution scraped images — proven insufficient.
|
||||
|
||||
## Watermarking landscape (research 2026-05-24)
|
||||
|
||||
Who embeds what, and whether it is locally detectable (so we know which gaps are fillable). See `identify.py` for what we read.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "remove-ai-watermarks"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
description = "Remove visible and invisible AI watermarks from images (Gemini / Nano Banana, ChatGPT, Stable Diffusion)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Remove-AI-Watermarks: Unified tool for removing visible and invisible AI watermarks."""
|
||||
|
||||
__version__ = "0.6.1"
|
||||
__version__ = "0.6.2"
|
||||
|
||||
@@ -127,31 +127,35 @@ def _ai_tools_in(data: bytes) -> list[str]:
|
||||
return sorted({name for sig, name in C2PA_AI_TOOLS.items() if sig in data})
|
||||
|
||||
|
||||
# C2PA claim-generator substring -> platform. The claim generator names what
|
||||
# PRODUCED the asset, so it is far more reliable than byte-scanning the manifest
|
||||
# for an issuer name (which also matches incidental mentions: a timestamp
|
||||
# authority like "Truepic" in a Leica chain, an XMP-toolkit "Adobe" string in a
|
||||
# Nikon file, or "Google" in a URL -- all verified on real samples). Ordered:
|
||||
# camera tokens first so a device wins over an incidental tool name (Nikon's
|
||||
# real sample claim generator also contains "Adobe_MAX"). Camera C2PA marks
|
||||
# CAPTURE authenticity, not AI, so these never assert is_ai on their own (the
|
||||
# verdict still comes from the digital-source-type). Only tokens verified
|
||||
# against a real signed file are listed; add more as samples are captured.
|
||||
_CLAIM_GENERATOR_PLATFORM: tuple[tuple[str, str], ...] = (
|
||||
("lc_c2pa", "Leica (camera, C2PA capture)"),
|
||||
("leica", "Leica (camera, C2PA capture)"),
|
||||
("nikon", "Nikon (camera, C2PA capture)"),
|
||||
("truepic", "Truepic Lens (verified capture)"),
|
||||
# Distinctive C2PA device/camera tokens (cert CN, cert org, or claim-generator
|
||||
# substrings) scanned in the manifest bytes -> platform. This is more reliable
|
||||
# than mapping an issuer name (which also matches incidental mentions: a
|
||||
# timestamp authority like "Truepic" in a Leica chain, an XMP-toolkit "Adobe"
|
||||
# string in a Nikon file, or "Google" in a Pixel camera's cert -- all verified
|
||||
# on real samples), and more robust than parsing the claim generator (which
|
||||
# lives under varying CBOR keys, e.g. `claim_generator` vs `claim_generator_info`,
|
||||
# and is absent on the Pixel sample where only the cert CN "Pixel Camera"
|
||||
# identifies it). Camera C2PA marks CAPTURE authenticity, not AI, so these never
|
||||
# assert is_ai on their own (the verdict still comes from the digital-source-type:
|
||||
# the Pixel sample carries `computationalCapture`, not `trainedAlgorithmicMedia`).
|
||||
# Only tokens verified against a real signed file are listed (Leica, Nikon,
|
||||
# Truepic, Google Pixel); add Sony/Canon/Samsung/Bria as real samples are captured.
|
||||
_DEVICE_C2PA_PLATFORM: tuple[tuple[bytes, str], ...] = (
|
||||
(b"lc_c2pa", "Leica (camera, C2PA capture)"),
|
||||
(b"Leica Camera", "Leica (camera, C2PA capture)"),
|
||||
(b"NIKON", "Nikon (camera, C2PA capture)"),
|
||||
(b"Pixel Camera", "Google Pixel (camera, C2PA capture)"),
|
||||
# "Truepic_Lens" (from the Lens SDK claim generator), NOT bare "Truepic" --
|
||||
# Truepic is a C2PA signing authority whose name appears in the trust chain
|
||||
# of unrelated manifests (e.g. OpenAI), so the bare token mis-attributes.
|
||||
(b"Truepic_Lens", "Truepic Lens (verified capture)"),
|
||||
)
|
||||
|
||||
|
||||
def _platform_from_generator(generator: str | None) -> str | None:
|
||||
"""Map a C2PA claim-generator string to a device/platform, or None."""
|
||||
if not generator:
|
||||
return None
|
||||
low = generator.lower()
|
||||
for token, platform in _CLAIM_GENERATOR_PLATFORM:
|
||||
if token in low:
|
||||
def _device_platform(head: bytes) -> str | None:
|
||||
"""Map a distinctive C2PA device/camera token in the manifest bytes to a platform."""
|
||||
for token, platform in _DEVICE_C2PA_PLATFORM:
|
||||
if token in head:
|
||||
return platform
|
||||
return None
|
||||
|
||||
@@ -237,17 +241,20 @@ def identify(image_path: Path, *, check_visible: bool = True, check_invisible: b
|
||||
c2pa_is_ai = "trainedAlgorithmicMedia" in info.get("source_type", "") or any(
|
||||
m in head for m in (b"trainedAlgorithmicMedia", b"compositeWithTrainedAlgorithmicMedia")
|
||||
)
|
||||
# Generator: structured for PNG, CBOR-scanned for other containers. The claim
|
||||
# generator is the authoritative "what produced this", so it drives platform
|
||||
# attribution; the issuer byte-scan is only the fallback (it matches
|
||||
# incidental chain/namespace mentions -- on real samples Leica mis-read as
|
||||
# Truepic, Nikon as Adobe, Truepic as Google until claim-generator took over).
|
||||
# Generator string (for the signal detail): structured for PNG, CBOR-scanned
|
||||
# for other containers. Best-effort -- some manifests key it as
|
||||
# `claim_generator_info` (Pixel), so this can be None even when a device is
|
||||
# identified by `_device_platform`.
|
||||
generator = (
|
||||
info.get("claim_generator")
|
||||
or cbor_text_after(head, b"claim_generator")
|
||||
or (", ".join(tools) if (tools := _ai_tools_in(head)) else None)
|
||||
)
|
||||
platform = (_platform_from_generator(generator) or _attribute_platform(issuers)) if has_c2pa else None
|
||||
# Platform: a distinctive device/camera token in the manifest wins (it is the
|
||||
# signer/producer), with the issuer byte-scan only as fallback. The issuer
|
||||
# scan alone mis-attributed real samples (Leica->Truepic timestamp authority,
|
||||
# Nikon->Adobe namespace, Pixel->Google Gemini) -- the device scan fixes that.
|
||||
platform = (_device_platform(head) or _attribute_platform(issuers)) if has_c2pa else None
|
||||
if has_c2pa:
|
||||
detail = ", ".join(filter(None, [", ".join(issuers), generator, info.get("source_type")]))
|
||||
signals.append(Signal("c2pa", detail or "C2PA manifest present", "high"))
|
||||
|
||||
+17
-12
@@ -337,24 +337,29 @@ class TestIdentifyIptcAi:
|
||||
assert "Gemini" in r.platform
|
||||
|
||||
|
||||
class TestIdentifyC2paClaimGenerator:
|
||||
"""C2PA attribution prefers claim_generator over incidental issuer tokens."""
|
||||
class TestIdentifyC2paDevice:
|
||||
"""A distinctive C2PA device token wins platform attribution over incidental
|
||||
issuer-name mentions (regression guard for real-sample mis-attribution:
|
||||
Leica->Truepic, Nikon->Adobe, Pixel->Google Gemini)."""
|
||||
|
||||
def test_claim_generator_beats_incidental_tokens(self, tmp_path: Path):
|
||||
# Real manifests mention timestamp authorities / XMP toolkits, so
|
||||
# "Adobe"/"Google"/"Truepic" appear incidentally; the claim generator
|
||||
# (a Leica camera) must win the platform attribution. Regression guard
|
||||
# for the real-sample mis-attribution (Leica->Truepic, Nikon->Adobe).
|
||||
gen = b"M11-P/2.0.1 lc_c2pa"
|
||||
blob = (
|
||||
b"\xff\xd8\xff\xe1 c2pa.claim jumbf Adobe Google Truepic "
|
||||
b"claim_generator" + bytes([0x60 + len(gen)]) + gen + b" \xff\xd9"
|
||||
)
|
||||
def test_leica_token_beats_incidental_tokens(self, tmp_path: Path):
|
||||
# "Adobe"/"Google"/"Truepic" appear incidentally; Leica's lc_c2pa wins.
|
||||
blob = b"\xff\xd8\xff\xe1 c2pa.claim jumbf Adobe Google Truepic lc_c2pa \xff\xd9"
|
||||
p = tmp_path / "leica_like.jpg"
|
||||
p.write_bytes(blob)
|
||||
r = identify(p, check_visible=False, check_invisible=False)
|
||||
assert r.platform == "Leica (camera, C2PA capture)"
|
||||
|
||||
def test_pixel_camera_cert_beats_incidental_google(self, tmp_path: Path):
|
||||
# Pixel's cert CN is "Pixel Camera"; "Google LLC" appears as the cert org
|
||||
# but must NOT yield "Google (Gemini / Imagen)" -- it is a camera capture.
|
||||
blob = b"\xff\xd8\xff\xe1 c2pa.claim jumbf Google LLC Adobe Pixel Camera \xff\xd9"
|
||||
p = tmp_path / "pixel_like.jpg"
|
||||
p.write_bytes(blob)
|
||||
r = identify(p, check_visible=False, check_invisible=False)
|
||||
assert r.platform == "Google Pixel (camera, C2PA capture)"
|
||||
assert r.is_ai_generated is None # camera capture, not AI
|
||||
|
||||
|
||||
# ── Open invisible watermark (SD/SDXL/FLUX) integration ─────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user