diff --git a/docs/watermarking-landscape.md b/docs/watermarking-landscape.md index 90798fb..572a8a1 100644 --- a/docs/watermarking-landscape.md +++ b/docs/watermarking-landscape.md @@ -10,7 +10,7 @@ Who embeds what, and whether it is locally detectable (so we know which gaps are **Root cause and external confirmation (deep-research 2026-06-19, adversarially verified).** This is the SCHEME's ceiling, not our usage — there is no better decoder to adopt. The imwatermark maintainers state verbatim (both the ShieldMnt and Stability-AI READMEs) that the algorithm "cannot guarantee to decode the original watermarks 100% accurately even though we don't apply any attack." Independent measurement (WMAdapter, arXiv:2406.08337 Table 2) puts dwtDct at only **~0.79 bit accuracy on CLEAN images (~38/48 bits — already below our 44 gate)**, collapsing to ~0.50 (chance) under crop/JPEG. Two code-verified + locally-reproduced mechanisms drive the content-dependent failures: (1) the decoder reads each bit as the **highest-magnitude DCT coefficient per block**, so any content coefficient exceeding the encoded target flips the bit; (2) the default embed is in the **YUV chroma channel, which 8-bit-clamps on white/bright pixels** (a +36 chroma delta survives a white-fill round-trip as only +4, ~89% loss) — this is the mechanism behind the bright-flat / minimalist failures and the all-ones degenerate decode. No maintained fork or detector decodes this scheme reliably: the WAVES benchmark (arXiv:2401.08573) relegates DWT-DCT to supplementary appendix G.5 and targets Stable Signature / Tree-Ring / StegaStamp instead; learned encoder/decoder schemes reach ~0.98-0.99 clean but are a DIFFERENT watermark class (not what SDXL/FLUX stamp). `dwtDctSvd` does not help (SDXL embeds `dwtDct`; dwtDctSvd cannot decode it, and its clean accuracy ~0.72 is lower). **Authoritative conclusion: the open DWT-DCT mark cannot be turned from positive-only into a reliable real-world detector; keep it positive-only and rely on C2PA.** (Refuted along the way: that the library is unmaintained, and that it is robust to JPEG but only fails on geometric attacks — both did not survive verification.) Consequence for the FLUX hosted-output question (BFL Playground, FLUX.2 [pro] + FLUX.1 [dev], 2026-06-19): all samples carry the signed C2PA manifest (issuer "Black Forest Labs"); the open DWT-DCT decode returned `None`, but every available FLUX carrier (textured fox AND a minimalist-flat generation) failed the positive control (28/48), so the detector is blind on them and **whether BFL hosted output embeds the open pixel watermark is UNRESOLVED** (an earlier note here wrongly asserted it absent — overstated; a later note blamed "high texture" — also wrong, flat carriers fail too). What IS established: C2PA is the reliable FLUX identifier; the `_BITS_48` pattern is correct (round-trips on chatgpt/firefly/random). Resolving the hosted question needs a hosted FLUX carrier that first passes a >=44/48 positive control, which neither a textured nor a flat prompt produced — low priority (the open mark is only a stripped-metadata fallback). -- **C2PA / IPTC (covered by the issuer/marker scan):** OpenAI, Google, Adobe Firefly, Microsoft (Designer + **Bing Image Creator** — collected 2026-05-24; Bing now runs Microsoft's own **MAI-Image** model, signs C2PA as "Microsoft", NOT OpenAI/DALL-E), **Stability AI** (collected from Brand Studio / DreamStudio successor; signs C2PA as "Stability AI Ltd", no SynthID, no imwatermark on its current Stable Image model — issuer added to `C2PA_ISSUERS`), and **Canva** (Magic Media signs C2PA as "Canva" + `trainedAlgorithmicMedia` with a generic `c2pa-rs` claim generator, no SynthID — issuer `b"Canva"` → "Canva (Magic Media)"; verified samples disproved the earlier assumption that Canva downloads always strip C2PA). Still unsampled: Getty, Shutterstock. Midjourney embeds NO C2PA and no invisible watermark (our `mj-*` sample carried only the IPTC tag). +- **C2PA / IPTC (covered by the issuer/marker scan):** OpenAI, Google, Adobe Firefly, Microsoft (Copilot + Designer; Bing Image Creator collected 2026-05-24 still signs as "Microsoft" and now runs **MAI-Image**, NOT OpenAI/DALL-E), **Stability AI** (collected from Brand Studio / DreamStudio successor; signs C2PA as "Stability AI Ltd", no SynthID, no imwatermark on its current Stable Image model — issuer added to `C2PA_ISSUERS`), and **Canva** (Magic Media signs C2PA as "Canva" + `trainedAlgorithmicMedia` with a generic `c2pa-rs` claim generator, no SynthID — issuer `b"Canva"` → "Canva (Magic Media)"; verified samples disproved the earlier assumption that Canva downloads always strip C2PA). Still unsampled: Getty, Shutterstock. Midjourney embeds NO C2PA and no invisible watermark (our `mj-*` sample carried only the IPTC tag). **Samsung Galaxy AI** signs supported edits with C2PA and may carry the proprietary `genAIType` marker. The registered visible detector covers the @@ -28,7 +28,7 @@ take precedence over issuer attribution. Supported mappings include Higgsfield AI, Topaz Labs Image API, and TikTok Ad Creative Toolbox; an unknown claim generator still falls back to the certificate issuer. -**ByteDance Volcano Engine (Volcengine)** — the cloud behind Doubao / Jimeng — signs its AI image output with a cert from `certificate_center@volcengine.com` + `trainedAlgorithmicMedia` (issuer `b"volcengine"` → "ByteDance (Volcano Engine)", platform "ByteDance (Doubao / Jimeng / Volcano Engine)"); note this is the C2PA-signed surface, distinct from the XMP/PNG TC260 `AIGC` label Doubao also uses. ByteDance's **international brand (BytePlus / Seedream / Seededit)** signs the same content as **"Byteplus Pte. Ltd."**. The bare `volcengine` needle missed it, so BytePlus output was mis-attributed to "Adobe Firefly" through an incidental "Adobe XMP" toolkit string. Issuer `b"Byteplus"` now maps directly to "BytePlus (ByteDance)". ByteDance's consumer app **Dreamina** (the international Jimeng brand) signs as **"Bytedance Pte. Ltd."** with a `Dreamina/x.y` claim generator but, unlike the Volcano Engine surface, ships **no `trainedAlgorithmicMedia`**. Issuer `b"Dreamina"` maps to "ByteDance (Dreamina)" with **`asserts_ai=True`**. Registering the broader **issuer** `b"Bytedance Pte"` was deliberately avoided because that same entity also signs non-AI CapCut edits; keying on the `Dreamina` generator token is precise. +**ByteDance Volcano Engine (Volcengine)** — the cloud behind Doubao / Jimeng — signs its AI image output with a cert from `certificate_center@volcengine.com` + `trainedAlgorithmicMedia` (issuer `b"volcengine"` → "ByteDance (Volcano Engine)", platform "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)"); note this is the C2PA-signed surface, distinct from the XMP/PNG TC260 `AIGC` label Doubao also uses. ByteDance's **international brand (BytePlus / Seedream / Seededit)** signs the same content as **"Byteplus Pte. Ltd."**. The bare `volcengine` needle missed it, so BytePlus output was mis-attributed to "Adobe Firefly" through an incidental "Adobe XMP" toolkit string. Issuer `b"Byteplus"` now maps directly to "BytePlus (ByteDance)". ByteDance's consumer app **Dreamina** (the international Jimeng brand) signs as **"Bytedance Pte. Ltd."** with a `Dreamina/x.y` claim generator but, unlike the Volcano Engine surface, ships **no `trainedAlgorithmicMedia`**. Issuer `b"Dreamina"` maps to "ByteDance (Dreamina)" with **`asserts_ai=True`**. Registering the broader **issuer** `b"Bytedance Pte"` was deliberately avoided because that same entity also signs non-AI CapCut edits; keying on the `Dreamina` generator token is precise. - **EXIF/XMP/PNG-text generator tag (caught by `exif_generator`):** **Ideogram** writes EXIF `Make="Ideogram AI"` (collected 2026-05-24 — no C2PA, no SynthID, no imwatermark; the Make tag is the only signal). Additional verified generator stamps include **NovelAI** (`Software`, `Source`, and `Title` PNG text chunks), **Reve** (`Software` or XMP `CreatorTool` = `reve.com`), and **Aphrodite AI** (`Make` or `Software` = `Aphrodite AI`). - **App-export provenance and AIGC JSON:** supported ByteDance-family exports can place a JSON object in EXIF `ImageDescription` or `UserComment`, independently of C2PA or TC260. Exact `product` values for Doubao, Xinghui, and Dreamina are removable product provenance, but do not alone prove that the pixels were generated. Dreamina additionally requires `exportType=generation` for that verdict. A nested Aweme `aigc_type=1` or private ByteDance `aigc_label_type=1` / `2` is an AIGC disclosure; `0` is inconclusive and can occur on a Dreamina generation export. Plain Aweme, retouch, and `lv` exports are preserved. The lower-case private field is deliberately not interpreted as the normative TC260 `Label`, whose values `1` / `2` / `3` mean generated / possibly generated / suspected generated under [GB 45438-2025](https://www.tc260.org.cn/upload/2025-03-15/1742009439794081593.pdf). - **xAI / Grok — its own EXIF signature scheme, NOT C2PA (DETECTED by `metadata.xai_signature`, built 2026-05-26).** diff --git a/src/remove_ai_watermarks/_internal/constants.py b/src/remove_ai_watermarks/_internal/constants.py index 0dcbea7..7f7f416 100644 --- a/src/remove_ai_watermarks/_internal/constants.py +++ b/src/remove_ai_watermarks/_internal/constants.py @@ -65,7 +65,7 @@ def _vendor( # Order is product priority when a manifest mentions more than one organization. C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = ( - _vendor(b"Microsoft", "Microsoft", "Microsoft (Bing Image Creator / Designer)", "Microsoft"), + _vendor(b"Microsoft", "Microsoft", "Microsoft (Copilot / Designer)", "Microsoft"), _vendor(b"Adobe", "Adobe", "Adobe Firefly", "Adobe"), _vendor( b"OpenAI", @@ -78,18 +78,25 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = ( _vendor(b"Google", "Google LLC", "Google (Gemini / Imagen)", "Google", synthid=True), _vendor(b"Stability AI", "Stability AI", "Stability AI (Stable Image / DreamStudio)", "Stability AI"), _vendor(b"Black Forest Labs", "Black Forest Labs", "Black Forest Labs (FLUX)", "Black Forest Labs"), - _vendor(b"volcengine", "ByteDance (Volcano Engine)", "ByteDance (Doubao / Jimeng / Volcano Engine)", "ByteDance"), + _vendor( + b"volcengine", + "ByteDance (Volcano Engine)", + "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)", + "ByteDance", + ), _vendor( "北京火山引擎科技有限公司", "ByteDance (Volcano Engine)", - "ByteDance (Doubao / Jimeng / Volcano Engine)", + "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)", "ByteDance", ), - _vendor(b"Byteplus", "BytePlus (ByteDance)", "ByteDance (Doubao / Jimeng / Volcano Engine)", "ByteDance"), + _vendor( + b"Byteplus", "BytePlus (ByteDance)", "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)", "ByteDance" + ), _vendor( b"Dreamina", "ByteDance (Dreamina)", - "ByteDance (Doubao / Jimeng / Volcano Engine)", + "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)", "ByteDance", asserts_ai=True, ), @@ -109,7 +116,7 @@ C2PA_IDENTITY_AI_ORGS = frozenset(vendor.org for vendor in C2PA_AI_VENDORS if ve C2PA_CLAIM_GENERATOR_PLATFORMS: tuple[tuple[str, str], ...] = ( ("adobe_firefly", "Adobe Firefly"), ("firefly", "Adobe Firefly"), - ("dreamina", "ByteDance (Doubao / Jimeng / Volcano Engine)"), + ("dreamina", "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)"), ("higgsfield ai", "Higgsfield AI"), ("topaz labs image api", "Topaz Labs"), ("tiktok ad creative toolbox", "TikTok Ad Creative Toolbox"), diff --git a/src/remove_ai_watermarks/identify.py b/src/remove_ai_watermarks/identify.py index c30e9e8..ecdeafc 100644 --- a/src/remove_ai_watermarks/identify.py +++ b/src/remove_ai_watermarks/identify.py @@ -770,6 +770,7 @@ _AI_VENDOR_TOKENS: tuple[tuple[str, str], ...] = ( ("google", "Google"), ("firefly", "Adobe"), ("adobe", "Adobe"), + ("copilot", "Microsoft"), ("bing", "Microsoft"), ("designer", "Microsoft"), ("microsoft", "Microsoft"), diff --git a/src/remove_ai_watermarks/qwen_engine.py b/src/remove_ai_watermarks/qwen_engine.py index a1c1045..641f460 100644 --- a/src/remove_ai_watermarks/qwen_engine.py +++ b/src/remove_ai_watermarks/qwen_engine.py @@ -1,4 +1,4 @@ -"""Qwen (Tongyi Qianwen, Alibaba) visible watermark detector/localizer. +"""Qwen (Alibaba) visible watermark detector/localizer. Qwen stamps its generations with a visible "千问AI生成" text strip in the bottom-right corner -- the explicit AIGC label mandated by China's GB 45438-2025 diff --git a/src/remove_ai_watermarks/watermark_registry.py b/src/remove_ai_watermarks/watermark_registry.py index ed60233..d116d45 100644 --- a/src/remove_ai_watermarks/watermark_registry.py +++ b/src/remove_ai_watermarks/watermark_registry.py @@ -20,7 +20,7 @@ Entries: - ``gemini`` -- Google Gemini / Nano Banana sparkle, bottom-right. - ``doubao`` -- ByteDance Doubao "豆包AI生成" text strip, bottom-right. - ``jimeng`` -- ByteDance Jimeng / Dreamina "★ 即梦AI" wordmark, bottom-right. - - ``qwen`` -- Alibaba Tongyi Qianwen "千问AI生成" text strip, bottom-right. + - ``qwen`` -- Alibaba Qwen "千问AI生成" text strip, bottom-right. - ``kling`` -- Kuaishou Kling "可灵AI 3.0" text strip, bottom-right. - ``yuanbao`` -- Tencent Yuanbao "元宝 / AI生成" two-line mark, bottom-right. - ``samsung`` -- Samsung Galaxy AI "Contenuti generati dall'AI" strip, bottom-left. @@ -651,7 +651,7 @@ _REGISTRY: tuple[KnownMark, ...] = ( "qwen", "Qwen 千问AI生成 text", "bottom-right", - platform="Alibaba Tongyi Qianwen (visible 千问AI生成 mark detected)", + platform="Alibaba Qwen (visible 千问AI生成 mark detected)", tc260_producer_codes=("91440101MA9Y9T4H7A",), ), _text_mark( diff --git a/tests/test_identify.py b/tests/test_identify.py index 25f89cf..706037e 100644 --- a/tests/test_identify.py +++ b/tests/test_identify.py @@ -16,6 +16,7 @@ from unittest.mock import patch import pytest +from remove_ai_watermarks._internal.constants import C2PA_AI_VENDORS, C2PA_CLAIM_GENERATOR_PLATFORMS from remove_ai_watermarks.identify import ( ProvenanceEvidence, ProvenanceReport, @@ -1300,12 +1301,20 @@ class TestVendorOf: def test_registered_vendors_normalize(self): # Regression: these registered C2PA vendors returned None, so their claims never # entered clash detection (a coverage hole). They now normalize to one origin. - assert _vendor_of("ByteDance (Doubao / Jimeng / Volcano Engine)") == "ByteDance" + assert _vendor_of("Microsoft (Copilot / Designer)") == "Microsoft" + assert _vendor_of("Copilot") == "Microsoft" + assert _vendor_of("ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)") == "ByteDance" assert _vendor_of("Dreamina/1.2") == "ByteDance" assert _vendor_of("Canva (Magic Media)") == "Canva" assert _vendor_of("Black Forest Labs (FLUX)") == "Black Forest Labs" assert _vendor_of("Eleven Labs Inc.") == "ElevenLabs" + def test_bytedance_issuers_share_one_platform(self): + expected = "ByteDance (Doubao / Jimeng / Dreamina / Volcano Engine)" + platforms = {vendor.platform for vendor in C2PA_AI_VENDORS if vendor.needle == "ByteDance"} + assert platforms == {expected} + assert ("dreamina", expected) in C2PA_CLAIM_GENERATOR_PLATFORMS + class TestIntegrityClashesHelper: def test_two_ai_vendors_clash(self):