mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-18 07:20:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1811b6221 | ||
|
|
e09104e39f | ||
|
|
29f17105de |
No files matched your search
@@ -0,0 +1,90 @@
|
||||
# Content Seal oracle corpus
|
||||
|
||||
Muse Image (`muse-image-1.0`) generations with externally verified Content Seal
|
||||
verdicts, produced through the Meta Model API on 2026-08-26 and checked against
|
||||
the public detector at `https://meta.ai/identification` (anonymous session, no
|
||||
login). Follows the `data/synthid/originals` pattern: binaries live in
|
||||
`originals/`, every derived variant is recorded in `manifest.csv` as a recipe
|
||||
plus hash and is not stored.
|
||||
|
||||
## What this corpus establishes
|
||||
|
||||
- The Meta Model API image endpoint (`POST /v1/images/generations`) stamps the
|
||||
same Content Seal pixel watermark as the consumer Meta AI app: all five
|
||||
generations verified positive with attribution "Muse Image 1 - Meta".
|
||||
- The detector response carries a per-generation ID and creation timestamp
|
||||
embedded in the watermark payload. Both survived a 512 px LANCZOS resize and
|
||||
a full-size JPEG q85 re-encode (same ID returned), so the payload is more
|
||||
robust than the detection threshold.
|
||||
- Center crops lose the seal: 50% and 33% linear center crops of two different
|
||||
images all returned "No AI signatures from Meta were found", consistent with
|
||||
the Reuters 2026-07-11 analysis (55% missed after cropping).
|
||||
- API outputs carry XMP `iptcExt:DigitalSourceType =
|
||||
trainedAlgorithmicMedia`, so local `identify` flags them via the existing
|
||||
Made-with-AI path. Metadata-stripping transforms fall back to unknown, and
|
||||
Content Seal has no local decoder in this project: the oracle is the only
|
||||
reader.
|
||||
- Drift finding: the 512 px resize of `gen_fox_forest` triggers a
|
||||
medium-confidence false positive "Tencent Yuanbao (visible 元宝 / AI生成
|
||||
mark)" in this project's `identify`. Recorded here as a reproducible case.
|
||||
|
||||
## Oracle limits and wire format
|
||||
|
||||
There is no public or documented checking API. Verified against the official
|
||||
developer documentation on 2026-08-26 (`https://dev.meta.ai/docs/`): the full
|
||||
Meta Model API reference lists only Responses, Chat Completions, Messages,
|
||||
Files, Images (`/v1/images/generations`, `/v1/images/edits`), and Models, with
|
||||
no identification, detection, or watermark endpoint, and the image-generation,
|
||||
Muse Image cookbook, and pricing pages never mention watermark, Content Seal,
|
||||
or provenance at all. The API applies the seal (every generation in this corpus
|
||||
carries it) while documenting nothing about it. The web tool drives an internal
|
||||
REST pair, captured from the browser network log on 2026-08-26:
|
||||
|
||||
1. `POST https://rupload.meta.ai/gen_ai_document_gen_ai_tenant/<uuid>` with the
|
||||
raw file bytes, `x-entity-type`, `x-entity-length`, `ai_detector_upload: true`,
|
||||
and an anonymous `authorization: OAuth ecto1:<token>` session token minted by
|
||||
the page.
|
||||
2. `POST https://meta.ai/api/ai-detector` with `Bearer ecto1:<token>` and body
|
||||
`{"media_id": "...", "fileName": "...", "mimeType": "..."}`.
|
||||
|
||||
The rate limit is enforced at that endpoint, server-side, and keyed beyond the
|
||||
browser session: the API itself returns `429 {"errorType": "rate_limited"}`,
|
||||
and clearing cookies and storage changed nothing, so driving the internal pair
|
||||
directly does not bypass it. The Meta Model API (`api.meta.ai/v1`, where the
|
||||
generation key works) has no identification endpoint; plausible paths all
|
||||
return 404. Rows with an empty `oracle_verdict` were transformed but not yet
|
||||
checkable. Read a verdict only from the settled page text after the
|
||||
result-complete state ("Upload another file"): a wait for a verdict string can
|
||||
match the previous upload's text, and the fresh-navigation protocol used for
|
||||
the calibration rows below is the race-free variant.
|
||||
|
||||
## Strength floor calibration (qwen-zimage, seed 0)
|
||||
|
||||
The library resolves strength per vendor with measured floors (OpenAI
|
||||
0.07675 / Google 0.27 / Microsoft InvisMark 0.15 in
|
||||
`_internal/watermark_profiles.py`). Meta Content Seal has no floor yet; the
|
||||
goal of these rows is to measure one by that same methodology: independent
|
||||
generations, each one's first-clean boundary, floor = worst boundary plus the
|
||||
observed cross-source spread.
|
||||
|
||||
Measured (2026-08-26/27, oracle `meta.ai/identification`):
|
||||
|
||||
- Default pipeline clears Content Seal: tested samples came back clean at the
|
||||
default resolution-adaptive strength (~0.1305 at 2.56 MP), including the
|
||||
worst source.
|
||||
- Five independent generations bracketed. First-clean boundaries:
|
||||
lighthouse (0.0525, 0.06], fox (0.03, 0.0375], night_city (0.03, 0.0375],
|
||||
mug <= 0.03, text <= 0.015. Cross-source spread is wide (a factor of four
|
||||
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.
|
||||
|
||||
## Regeneration
|
||||
|
||||
API key is not stored in this repository. Regenerate with the script pattern
|
||||
from the session (env `MUSE_API_KEY`, endpoint
|
||||
`https://api.meta.ai/v1/images/generations`, model `muse-image-1.0`); prompts
|
||||
are recorded per file in `manifest.csv`.
|
||||
@@ -0,0 +1,44 @@
|
||||
name,file,origin,source_url,prompt,requested_size,delivered_size,sha256,xmp_iptc_trainedAlgorithmicMedia,oracle_verdict,oracle_attribution,oracle_generation_id,oracle_created_utc,checked_at_utc,notes
|
||||
gen_fox_forest,originals/gen_fox_forest.webp,meta-model-api,,A red fox resting on a mossy log in a misty forest at dawn photorealistic shallow depth of field,1024x1024,1600x1600,6d7cecf322a5ac4b2547e2f32c767b3cb3e8a0e49f274cfd00aa31be6697eed4,yes,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbLlNjYhvPAxD,2026-08-26T19:07Z,2026-08-26T19:11Z,checked twice; ID stable across uploads
|
||||
gen_text_poster,originals/gen_text_poster.webp,meta-model-api,,A cheerful flat-design poster that says 'WATERMARK FIELD DAY 2026' in bold rounded letters over a coral and cream striped background,1024x1024,1600x1600,873927662d8198e9c38b4ae71a2919ba5d417dfcd714649db1b0ae231e8cbf9b,yes,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbJuIKXPAYxsd,2026-08-26T19:08Z,2026-08-26T19:08Z,
|
||||
gen_lighthouse_watercolor,originals/gen_lighthouse_watercolor.webp,meta-model-api,,A watercolor illustration of a lighthouse on a cliff at golden hour seagulls in the distance soft loose washes white paper texture,1024x1024,1600x1600,69bee7c376608568361085602c5c7406900885849bd94c5279c3924cf793b27c,yes,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbB5uWbAmWmwp,2026-08-26T19:08Z,2026-08-26T19:08Z,
|
||||
gen_night_city,originals/gen_night_city.webp,meta-model-api,,A night city skyline in the rain neon reflections on wet asphalt cinematic wide shot moody,1536x1024,1920x1280,345adf6b9f777b15ef7ce7bbdfb3b355a1d9e882740809e966e9f6caafd94d48,yes,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbH5uWbAmWmwp,2026-08-26T19:08Z,2026-08-26T19:08Z,
|
||||
gen_studio_mug,originals/gen_studio_mug.webp,meta-model-api,,A minimalist studio photo of a single matte ceramic mug on a pastel blue background soft shadow clean commercial look,1024x1024,1600x1600,2b7bc7fd56fa2250c66ef74f8b3e49d9666fe147d894d5baf9461b5e36077a07,yes,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbAsSozV2XCzw,2026-08-26T19:08Z,2026-08-26T19:08Z,
|
||||
fox_crop50,,derived:gen_fox_forest,,center crop 0.5 linear each side then WEBP q95,,800x800,80b91995560a7c4b08596a26b3401a29afb82691fad193502697a49785eab7be,no_stripped_by_reencode,not_detected,,,,2026-08-26T19:13Z,seal lost; PIL re-encode strips XMP
|
||||
fox_crop33,,derived:gen_fox_forest,,center crop 0.33 linear each side then WEBP q95,,528x528,3b3aded8f4fcb98d0599431abccb73b9581854ae34c6bc869724cd5c86a1e35e,no_stripped_by_reencode,not_detected,,,,2026-08-26T19:13Z,seal lost
|
||||
fox_res512,,derived:gen_fox_forest,,resize long side 512 LANCZOS then WEBP q95,,512x512,19b85f615d56f5e9f55cd97bae0ee343f4f432e6bfa0a6fc7acb07dec2cc1d81,no_stripped_by_reencode,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbLlNjYhvPAxD,2026-08-26T19:07Z,2026-08-26T19:13Z,generation ID and creation time identical to the original generation: watermark payload survives the resize
|
||||
fox_jpeg85,,derived:gen_fox_forest,,full-size JPEG q85 re-encode,,1600x1600,3bc7dc11bf3c16c83814df1e6485cdb6e49e21141511dda56d210cc920a8c5e3,no_stripped_by_reencode,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbLlNjYhvPAxD,2026-08-26T19:07Z,2026-08-26T19:13Z,payload survives JPEG recompression; local identify returns unknown (metadata stripped)
|
||||
text_crop50,,derived:gen_text_poster,,center crop 0.5 linear each side then WEBP q95,,800x800,3f0d4075012ee4fec820209c5c1c32bb739998813325f49b1124f379162f9837,no_stripped_by_reencode,not_detected,,,,2026-08-26T19:13Z,seal lost
|
||||
text_crop33,,derived:gen_text_poster,,center crop 0.33 linear each side then WEBP q95,,528x528,95cef87002961831b21f6f47cd9a0824a67e2ee6f8503356fd5db6a3fcfcc998,no_stripped_by_reencode,,,,,,daily oracle limit reached before this row could be checked
|
||||
text_res512,,derived:gen_text_poster,,resize long side 512 LANCZOS then WEBP q95,,512x512,b319727d02e908f22e59f148050e6b4686cbd1a622bd69db51fe4072a6f66b34,no_stripped_by_reencode,,,,,,daily oracle limit reached; also the fox twin of this transform triggered the Yuanbao false positive below
|
||||
text_jpeg85,,derived:gen_text_poster,,full-size JPEG q85 re-encode,,1600x1600,76f00fcde1e974549afa5d90e1879c705e3c3631ef1f35cab901fc912cd15fa4,no_stripped_by_reencode,,,,,,daily oracle limit reached before this row could be checked
|
||||
blog_qr_manhwa,,meta-blog-cdn,https://lookaside.fbsbx.com/elementpath/media/?media_id=1807972953501959&version=1783318590&transcode_extension=webp,,,1920x1280,8ee7700dff59f21d7de393a619664a0b9360f148d18df289364ecea939b3dea9,yes,detected,Muse Image 1 - Meta,,,2026-08-26T18:59Z,not stored; re-downloadable from source_url
|
||||
blog_fractal_poster,,meta-blog-cdn,https://lookaside.fbsbx.com/elementpath/media/?media_id=1499727434782014&version=1783727678&transcode_extension=webp,,,2096x992,2a96168db026444b47ac37807fd99a88c8b7fa6866e1c93a54ab427fbe581f76,no,detected,Muse Image 1 - Meta,,,2026-08-26T18:59Z,seal survived CDN transcode without any metadata
|
||||
blog_flipbook_punch,,meta-blog-cdn,https://lookaside.fbsbx.com/elementpath/media/?media_id=2521328538303639&version=1783396590&transcode_extension=webp,,,1920x1280,35f28dc2cbdf44aed190bc8efaa0fe7bfa288a2196638712dd83bc941cc294d5,yes,detected,Muse Image 1 - Meta,,,2026-08-26T19:00Z,
|
||||
blog_teen_cat,,meta-blog-cdn,https://lookaside.fbsbx.com/elementpath/media/?media_id=1325391919706734&version=1783319609&transcode_extension=webp,,,1600x1600,c2e7df000ffa6520589d8efc5f0bcbeb759175cdaa0c827189c28a64f75d3dd7,yes,detected,Muse Image 1 - Meta,,,2026-08-26T19:00Z,
|
||||
blog_hero_1,,meta-blog-cdn,https://lookaside.fbsbx.com/elementpath/media/?media_id=2140990336800598&version=1783727678,,,1344x1792,7655639acb9f826cdb864d869530153efc1e1467ff414b5a63e80aa7b39b2fb7,no,detected,Muse Image 1 - Meta,,,2026-08-26T19:00Z,seal survived CDN transcode without any metadata
|
||||
control_synth_gradient,,synthetic-local,,PIL gradient with rectangle JPEG q90; never touched any AI pipeline,,1024x768,25f29c0b993ed1a8e41060ed50de778d875a6d94847ced180aafc5a6ad429f82,no,not_detected,,,,2026-08-26T19:01Z,negative control: No AI signatures from Meta were found
|
||||
fox_nometa,,derived:gen_fox_forest,,metadata --remove only (no pixel change),,1600x1600,685d3a5c5d74e653ec7615fc7a347a0584c277fc7fb59594d388ac619ffe3985,no_stripped,detected,Muse Image 1 - Meta,AXZpZGVvc2VhbHY0X2NhX3NjbLlNjYhvPAxD,2026-08-26T19:07Z,2026-08-26T22:19Z,metadata stripping does NOT remove Content Seal; seal lives in pixels; same generation ID as original
|
||||
fox_modal_invisible,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=default-adaptive(approx 0.1305 at 1600x1600),,1600x1600,6daa3e9b5dde5cb443d8071c5294262742f001c02b694243764a50345769d8b8,no_stripped,not_detected,,,,2026-08-26T22:21Z,current default pipeline clears Content Seal
|
||||
text_modal_invisible,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=default-adaptive(approx 0.1305 at 1600x1600),,1600x1600,8530a82fb856b0a00651076ac1ad8a7544b5d8873cac9bd5780704fbe10f53f8,no_stripped,not_detected,,,,2026-08-26T22:22Z,current default pipeline clears Content Seal
|
||||
fox_str0.06000,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.06,,1600x1600,7b5e5ae75ca20f2a3cdfcfa7666104752d9557b08ac236db6f1548434940a1f3,no_stripped,not_detected,,,,2026-08-26T22:28Z,strength calibration ladder
|
||||
fox_str0.04500,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.045,,1600x1600,26da15694b3096e2f1717154db213e814cdd27f2fc40e5f924db2324213cf960,no_stripped,not_detected,,,,2026-08-27T02:28Z,strength calibration ladder; re-confirmed clean at 0.045
|
||||
fox_str0.03750,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0375,,1600x1600,b7eb0616d10042933edf22cfc30aff4df915384828289b409f6cfa4cdf2807d4,no_stripped,not_detected,,,,2026-08-26T22:30Z,fresh-page protocol; fox first-clean boundary is in (0.03 0.0375]
|
||||
fox_str0.03000,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.03,,1600x1600,cff80d18d9d00000f47e0f97c419a37c13a13831795768aee8727aa9516d1401,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-26T22:31Z,fresh-page protocol; seal survives 0.03
|
||||
fox_str0.02000,,derived:gen_fox_forest,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.02,,1600x1600,b19183ec60b63a5b73d3e1c723e7943584912857d51a4b5f85259c5c77da0e53,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-26T22:28Z,settled-read protocol; monotonic below the 0.03 boundary
|
||||
text_str0.03750,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0375,,1600x1600,bacfd4eca7b13616502f74497cbe4f02ef6893918870f13f9e76fdb93b43107f,no_stripped,not_detected,,,,2026-08-26T22:33Z,fresh-page protocol
|
||||
text_str0.03000,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.03,,1600x1600,d5c6cffbf471ef8bb74ee8c0d22cc17d9da2c446b83e46c4be67e6ea4cdd09de,no_stripped,not_detected,,,,2026-08-26T22:33Z,fresh-page protocol; text first-clean boundary is at or below 0.03 - cross-source spread vs fox confirmed
|
||||
lh_str0.03750,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0375,,1600x1600,f5169debc5fd2792399c0ea3fa88cb1d05f523d95004bec240c73db0e9aaee51,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-27T02:28Z,seal survives 0.0375 where fox is already clean
|
||||
lh_str0.03000,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.03,,1600x1600,110d2112d91dd53df5ccd1839e0e2b41c44aa1832adb0090032660e071fb1fd4,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-27T02:28Z,fresh-page protocol; lighthouse holds its seal below the fox boundary: cross-source spread is wide
|
||||
text_str0.02500,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.025,,1600x1600,f46a6c53b4f3d39fccb1d62cca82ced2e163e4424d13c98e6468a8e083c0fa54,no_stripped,not_detected,,,,2026-08-27T02:28Z,mid rung inside the clean zone
|
||||
text_str0.02000,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.02,,1600x1600,a19cbe22313e0f19768c98567ea89f2d9dc30827691fc7ec2971c52f3e7c6cc6,no_stripped,not_detected,,,,2026-08-27T02:28Z,text clean at 0.02: boundary <= 0.02 - the easiest source measured
|
||||
text_str0.01500,,derived:gen_text_poster,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.015,,1600x1600,aa3816c75e38f3ab5afc378d349caafad56f2bb6da859a5ee68fdea57ad00d73,no_stripped,not_detected,,,,2026-08-27T02:41Z,text clean at 0.015: boundary <= 0.015 - the easiest source measured
|
||||
lh_str0.02500,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.025,,1600x1600,989c4a7a804bc8420ec4d86b9445bc3a37382c1834782a7f8842f781d69b05e2,no_stripped,,,,,,below the measured detected boundary (0.03) for this source; not individually checked
|
||||
lh_str0.02000,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.02,,1600x1600,6a4a4ec94292a145e7d73e6900b8ec6d0eede17137f2b91fba7757ec0bdba5ec,no_stripped,,,,,,below the measured detected boundary (0.03) for this source; not individually checked
|
||||
nc_str0.03750,,derived:gen_night_city,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0375,,1920x1280,46f322335f801ee54427eb39275216465f9a7cfa3d918c698e4d90a2439a0815,no_stripped,not_detected,,,,2026-08-27T02:28Z,boundary (0.03; 0.0375]
|
||||
nc_str0.03000,,derived:gen_night_city,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.03,,1920x1280,3f42632319248ad1a91ef2457c7e0b13ed17e1d2569decbbaab67c2072e9a305,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-27T02:28Z,night_city holds seal at 0.03
|
||||
mug_str0.03750,,derived:gen_studio_mug,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0375,,1600x1600,3b6109ae7e86b8ea74ce33315e3f2a7eda1cadb724d429f0951f13c3ead34b0f,no_stripped,not_detected,,,,2026-08-27T02:28Z,upper rung for the mug bracket
|
||||
mug_str0.03000,,derived:gen_studio_mug,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.03,,1600x1600,1c24c984677e1528da83003ac40594f6e5ef1fa9351f1368b6d31a0101308583,no_stripped,not_detected,,,,2026-08-27T02:28Z,mug clean at 0.03: boundary <= 0.03
|
||||
lh_str0.05250,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.0525,,1600x1600,8912ba1ff4a26790f69224c49dd649e46835e277c375276ac9604f5339775b2a,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-27T02:41Z,bisection: lighthouse first-clean boundary bracketed to (0.0525; 0.06]
|
||||
lh_str0.04500,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.045,,1600x1600,cfdcf7bbb27c740846a1de4edffee3b25a624c2a8e1aba2362feaa25b682e3ae,no_stripped,detected,Muse Image 1 - Meta,,,2026-08-27T02:28Z,seal survives 0.045: lighthouse is the worst source measured
|
||||
lh_str0.06000,,derived:gen_lighthouse_watercolor,,modal raiw-worker qwen-zimage mode=invisible seed=0 strength=0.06,,1600x1600,4e214be00a89dc9f1f32ec39483c5e9668a1ec7f38b15e5794942479c839fcca,no_stripped,not_detected,,,,2026-08-27T02:28Z,lighthouse first-clean boundary bracketed to (0.045 0.06]
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 585 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 652 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
@@ -521,7 +521,7 @@ held by its masked ArcFace vector -- no original pixel copied.
|
||||
drift, NOT a perfect face swap. Set expectations; PuLID/InstantID are the higher-fidelity (heavier)
|
||||
paths if needed.
|
||||
- **Value scales with strength:** at low strength (OpenAI 0.10) faces barely drift, so FaceID is
|
||||
marginal; at the higher strength a hard vendor (Google 0.30) needs, FaceID earns its keep.
|
||||
marginal; at the higher strength then used for a hard vendor (Google 0.30), FaceID earned its keep.
|
||||
|
||||
### Build plan (staged)
|
||||
|
||||
|
||||
@@ -111,6 +111,14 @@ For important outputs:
|
||||
Provider systems can change, so a result verified on one file, seed, or version
|
||||
is not a permanent certification.
|
||||
|
||||
Meta Content Seal (Muse Image) is in the same family: no local decoder, presence
|
||||
recognizable only through the XMP `trainedAlgorithmicMedia` companion tag while
|
||||
that metadata survives, and removal verifiable only through the anonymous
|
||||
`meta.ai/identification` oracle, which rate-limits by IP per day. The oracle's
|
||||
negative is weaker than its positive: Reuters measured it missing 55% of cropped
|
||||
Muse images, so treat a negative on a cropped or heavily edited file as
|
||||
inconclusive rather than clean.
|
||||
|
||||
### Video SynthID removal is lossy and content-dependent
|
||||
|
||||
The `video invisible` command and `remove_video_invisible` API regenerate video
|
||||
@@ -177,13 +185,14 @@ path has not yet answered.
|
||||
The two profiles resolve an unset strength differently, because different things
|
||||
were measured for each.
|
||||
|
||||
`qwen-zimage` reads it from image area, through the resolution-adaptive denoise
|
||||
curve. The vendor is deliberately ignored for openai/unknown content: the curve,
|
||||
not the issuer, is what was calibrated. One measured exception (0.27.2):
|
||||
Google-provenance content takes the flat `QWEN_ZIMAGE_GOOGLE_STRENGTH` 0.30 floor
|
||||
- the curve's 0.154 top left a 4.33 MP fixture oracle-detected on the full
|
||||
production path (2026-08-18), while 0.30 anchors measured clean on two fixtures
|
||||
in two accounts.
|
||||
`qwen-zimage` reads unknown content from the resolution-adaptive denoise curve.
|
||||
Measured provider cohorts instead take flat operating points: OpenAI `0.07675`,
|
||||
Google `0.27`, and Microsoft InvisMark `0.15`. OpenAI and Microsoft add one full
|
||||
observed cross-source boundary spread to the worst clean source; Microsoft's three
|
||||
first-clean boundaries were `0.04125`, `0.055`, and `0.095`, giving `0.14875` before
|
||||
rounding up. Google's candidate was separately repeated across three valid sources
|
||||
and three accounts. The small corpora make these operating points, not universal
|
||||
thresholds.
|
||||
|
||||
`sdxl-zimage` reads it from the C2PA issuer, on a flat ladder:
|
||||
|
||||
|
||||
@@ -876,6 +876,11 @@ are CUDA-only. `controlnet`, `sdxl`, `qwen` and `default` were removed rather th
|
||||
kept as a CPU path, and are rejected rather than aliased onward. There is no
|
||||
content-dependent automatic router.
|
||||
|
||||
`qwen-zimage` normally resolves global denoise from image area for unknown content.
|
||||
Measured provider cohorts bypass that curve with flat operating points. The values,
|
||||
measurement derivations, and corpus limits are canonical in
|
||||
[`known-limitations.md`](known-limitations.md#strength-is-content-and-seed-dependent).
|
||||
|
||||
For serverless cold starts, `InvisibleEngine.preload(global_only=True)` loads the
|
||||
mandatory global stage and YuNet while leaving the optional Z-Image and SAM face
|
||||
stack lazy until a face is detected. The default `preload()` still loads every
|
||||
@@ -1144,6 +1149,40 @@ sizes the curve's own values verify clean, which is what a resolution-scaled
|
||||
requirement would predict. There is no measured size at which the shipped curve fails,
|
||||
so it is left alone.
|
||||
|
||||
### Meta Content Seal boundaries for qwen-zimage
|
||||
|
||||
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`). Muse Image carries no C2PA, so
|
||||
`vendor_for_strength` returns None for it and these images ride the default
|
||||
resolution-adaptive curve; there is deliberately no `meta` entry in the flat vendor
|
||||
map, because no provenance signal can establish that cohort automatically and an
|
||||
unreachable constant would be dead code.
|
||||
|
||||
| source (2.56 MP generation) | detected at | clean from |
|
||||
|---|---|---|
|
||||
| gen_lighthouse_watercolor | 0.0525 | 0.06 |
|
||||
| gen_fox_forest | 0.03 | 0.0375 |
|
||||
| gen_night_city | 0.03 | 0.0375 |
|
||||
| gen_studio_mug | -- | 0.03 |
|
||||
| gen_text_poster | -- | 0.015 |
|
||||
|
||||
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.
|
||||
|
||||
### Static prompt embeddings
|
||||
|
||||
Both stages prompt with module constants, and at CFG 1.0 DiffSynth's
|
||||
|
||||
@@ -154,6 +154,31 @@ 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.
|
||||
|
||||
Meta Muse Image stamps every output with Content Seal, a proprietary invisible
|
||||
pixel watermark, and ships no visible mark (the legacy `Imagined with AI`
|
||||
corner mark belongs to the pre-2026 Imagine pipeline and is not registered).
|
||||
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
|
||||
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
|
||||
per-generation ID and creation timestamp read from the watermark payload. No
|
||||
identification endpoint exists in the Meta Model API itself (the reference at
|
||||
dev.meta.ai documents only generation and edits for images), and the official
|
||||
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).
|
||||
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.
|
||||
|
||||
For MP4, MOV, and M4V, `video invisible` or the explicit
|
||||
`video all --invisible` option can regenerate the video through a VAE and strip
|
||||
source metadata. The shipped profile is oracle-certified, but it is not a local
|
||||
@@ -174,6 +199,7 @@ not a universal clean verdict.
|
||||
| Google Gemini | Sparkle | Diffusion regeneration for SynthID | C2PA and related source signals |
|
||||
| Google Veo video | Veo diamond and legacy text | Oracle-certified VAE removal for SynthID | C2PA and related source signals |
|
||||
| OpenAI image generators | None registered | Diffusion regeneration for supported invisible signals | C2PA and generator provenance |
|
||||
| Meta Muse Image | None on Muse output (legacy `Imagined with AI` unregistered) | Diffusion regeneration for Content Seal, oracle-verified on the default profile | XMP IPTC `trainedAlgorithmicMedia` companion tag; no local Content Seal decoder |
|
||||
| Microsoft Paint and Photos | None registered | External Microsoft oracle for InvisMark; no validated local decoder | Paint C2PA soft-binding algorithm and identifier |
|
||||
| Stable Diffusion and SDXL | None registered | Diffusion regeneration; optional open decoder | Embedded parameters and text metadata |
|
||||
| FLUX | None registered | Diffusion regeneration; optional open decoder | C2PA for supported sources |
|
||||
|
||||
+11
-11
@@ -537,8 +537,8 @@ study (section 2.2) gives empirical floors:
|
||||
> were removed, and `OPENAI_STRENGTH` / `GEMINI_STRENGTH` / `UNKNOWN_STRENGTH` went
|
||||
> with them. Everything from here to the end of this section is a record of what was
|
||||
> measured on those profiles, kept because the oracle verdicts are still the evidence
|
||||
> base. For the strength policy in force now see `module-internals.md`: `qwen-zimage`
|
||||
> uses `resolution_adaptive_denoise`, `sdxl-zimage` a flat vendor ladder.
|
||||
> base. For the strength policy in force now see
|
||||
> [`known-limitations.md`](known-limitations.md#strength-is-content-and-seed-dependent).
|
||||
|
||||
The default was **vendor-adaptive** (`watermark_profiles.resolve_strength` +
|
||||
`vendor_for_strength`): the tool read the C2PA issuer on the original input and picked
|
||||
@@ -691,9 +691,9 @@ solid at 0.10 but at 0.15 it is near-threshold noise; (2) for reliable removal p
|
||||
a strength with MARGIN above the borderline (controlnet >= 0.20), not exactly on
|
||||
it; (3) **historical engineering conclusion:** this dated run argued for a
|
||||
higher ControlNet strength than the then-current default. That proposal was
|
||||
later superseded. The current resolver intentionally shares the 0.10/0.15
|
||||
ladder between SDXL and ControlNet and uses a separate Qwen ladder; see
|
||||
`_internal/watermark_profiles.py`.
|
||||
later superseded. The resolver of that period shared the 0.10/0.15 ladder between
|
||||
SDXL and ControlNet; the current policies are recorded in
|
||||
[`known-limitations.md`](known-limitations.md#strength-is-content-and-seed-dependent).
|
||||
Source images are private (faces / product shots), not committed; reproduce on any
|
||||
photoreal + flat-graphic gpt-image pair, varying the seed, and re-checking the
|
||||
oracle.
|
||||
@@ -770,14 +770,14 @@ reproducible verification requires a fixed seed.
|
||||
Watermarking.** arXiv:2505.21620.
|
||||
https://arxiv.org/abs/2505.21620
|
||||
|
||||
**Google floor on qwen-zimage (0.27.2, 2026-08-19).** The bracket above did not
|
||||
**Google floor on qwen-zimage (0.27.2, remeasured 2026-08-25).** The bracket above did not
|
||||
survive the full production path: on the 4.33 MP CJK-sign fixture (visible-stage
|
||||
sparkle removal -> qwen-zimage seed 0 at the curve's 0.154 top -> resize-back ->
|
||||
metadata strip), the Gemini verifier detected SynthID x3 with a valid
|
||||
pixel-identical stripped control in the same session. Google-provenance content
|
||||
now takes a flat `QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.30` floor instead of the area
|
||||
curve - 0.30 anchors measured clean in Gemini on two fixtures (CJK sign +
|
||||
18-face) at 3/3 checks across two work accounts, and stayed clean with the
|
||||
vae-glyphs donor layer on top; openai/unknown content keeps the resolution
|
||||
curve. An explicit strength still wins. Certification artifacts: raiw-app
|
||||
initially took a flat `0.30` floor instead of the area curve. The later provider
|
||||
remeasurement established a common pass at `0.25` across three valid sources and
|
||||
separately repeated `QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.27` across those sources and
|
||||
three accounts. OpenAI now has its own measured flat operating point; unknown
|
||||
content keeps the resolution curve. An explicit strength still wins. Certification artifacts: raiw-app
|
||||
`data/certification/text-restoration-2026-08-18/` (the failing re-baseline).
|
||||
@@ -20,8 +20,8 @@ filter applied to every candidate below.
|
||||
## Problem recap
|
||||
|
||||
The `invisible` pipeline is SDXL base 1.0 img2img to defeat SynthID. The default
|
||||
strength has risen over time as Google hardens SynthID (0.05 -> 0.10 -> **~0.30**, the
|
||||
current threshold for fresh Gemini output); higher strength deforms text more, which is
|
||||
strength rose over time as Google hardened SynthID (0.05 -> 0.10 -> **~0.30** in
|
||||
this experiment; the later measured operating point is 0.27); higher strength deforms text more, which is
|
||||
exactly why text protection matters. Text is protected via Differential Diffusion with a
|
||||
per-pixel change map (`preserve` ~0.9) driven by the PP-OCRv3 DB detector
|
||||
(`text_protector.py`). Large text survives; **small text (sub ~8 px strokes) softens or
|
||||
|
||||
@@ -133,7 +133,7 @@ auditable detection boundary than the provider-specific synthetic path here.
|
||||
|
||||
**The faint-visible-mark precision/recall wall is fundamental, not a heuristic artifact.** The visible-watermark-detection literature has moved to LEARNED segmentation / object-detection (WDNet WACV'21 arXiv:2012.07616; SLBR ACM MM'21, open code+weights; the PRCV'18 large-scale detector; Su et al. survey 2025), but three verified findings bound what a learned detector actually buys: (1) a claim that a confidence threshold "cleanly separates" true from false matches even with a learned CNN front-end was **REFUTED** in verification (arXiv:1705.08593) -- the precision/recall wall persists even with learned features. (2) Learned detectors need a LARGE, pattern-diverse labeled dataset trained on synthetic composites (PRCV'18: 60k images / 80 watermark classes; CLWD: 60k / 160 marks), and off-distribution degradation is a documented real axis (models trained on limited-pattern LVW transfer worse; diversity of training patterns drives generalization). (3) Inference is cheap (WDNet ~8 ms at 256x256) -- the cost is the data pipeline, not runtime. Net: a learned detector shifts the frontier but does NOT remove the wall; for a SINGLE mark the cheapest next step is a small patch classifier (real-sparkle vs false-positive) on top of the existing NCC localizer, not a full segmentation model. SLBR is a ready baseline. The current NCC + false-positive gate (core-ring brightness margin + gradient-NCC crispness + white-core saturation) is a sound operating point, and the residual miss is the information-theoretic wall the literature confirms.
|
||||
|
||||
**Visible-mark landscape beyond the registry.** Meta stamps a visible "Imagined with AI" mark (bottom-LEFT, a small symbol) on its OWN Meta AI / "Imagine" output; for third-party images it relies on C2PA / IPTC, not a visible mark. Samsung Galaxy AI additionally uses a **four-star icon** variant in a corner alongside the localized text wordmark `samsung_engine` calibrates (only the Italian text variant is covered) -- the icon is a distinct, uncovered variant. Every source agrees visible + metadata marks are trivially removable (crop / screenshot, ~2 s), which is the tool's premise.
|
||||
**Visible-mark landscape beyond the registry.** Since Muse Image (2026-07) Meta's own generation output carries NO visible mark, only the invisible proprietary Content Seal; the legacy visible "Imagined with AI" mark (bottom-LEFT, a small symbol) belongs to the pre-2026 Meta AI / "Imagine" pipeline and remains unregistered (string verified, position not). For third-party images Meta relies on C2PA / IPTC, not a visible mark. Samsung Galaxy AI additionally uses a **four-star icon** variant in a corner alongside the localized text wordmark `samsung_engine` calibrates (only the Italian text variant is covered) -- the icon is a distinct, uncovered variant. Every source agrees visible + metadata marks are trivially removable (crop / screenshot, ~2 s), which is the tool's premise.
|
||||
|
||||
**Regulatory driver -- China GB 45438-2025 is the strongest VISIBLE-mark mandate.** The CAC / TC260 "Measures for Labeling AI-Generated Synthesized Content" (issued March 2025, **effective 2025-09-01**, technical standard **GB 45438-2025**, building on the TC260 Aug-2023 practice guide) MANDATE a **visible** label for AI images -- a visible textual mark whose height must be **>= 5% of the image's shortest side** -- plus the metadata (implicit) label. Several such CJK text marks are now registered; see [supported signals](supported-signals.md) for the current list. By contrast EU AI Act Article 50 mandates only the MACHINE-READABLE mark (enforceable 2026-08-02, grace to 2026-12-02); a visible label is proposed and modality-specific (visible for images) but is NOT a hard "fixed icon" mandate -- a claim that Art 50 requires a clearly-visible fixed icon for images was refuted in verification. Primary-source dates verified against the article/standard text, not search summaries.
|
||||
|
||||
@@ -161,6 +161,8 @@ 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 `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/`.
|
||||
|
||||
**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
@@ -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.0"
|
||||
version = "0.31.2"
|
||||
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11,<3.15"
|
||||
|
||||
@@ -32,7 +32,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.31.0"
|
||||
__version__ = "0.31.2"
|
||||
|
||||
__all__ = [
|
||||
"BatchSummary",
|
||||
|
||||
@@ -69,17 +69,33 @@ SDXL_ZIMAGE_OPENAI_STRENGTH = 0.15
|
||||
SDXL_ZIMAGE_GEMINI_STRENGTH = 0.25
|
||||
SDXL_ZIMAGE_UNKNOWN_STRENGTH = SDXL_ZIMAGE_GEMINI_STRENGTH
|
||||
|
||||
# qwen-zimage keeps its resolution curve for openai/unknown content, but Google
|
||||
# content takes this flat oracle-measured floor instead of the curve (whose top,
|
||||
# 0.154, left a 4.33 MP CJK-sign fixture detected x3 on the full production path
|
||||
# with a valid pixel control, 2026-08-18). 0.30 anchors measured clean in Gemini
|
||||
# across two fixtures and two work accounts (CJK sign + 18-face, 3/3 checks each,
|
||||
# 2026-08 research) and stayed clean under the vae-glyphs donor layer on top.
|
||||
QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.30
|
||||
# qwen-zimage keeps its resolution curve for unknown content, but measured vendor
|
||||
# cohorts bypass it. Google remained detectable through 0.24375; 0.25 cleared all
|
||||
# three valid sources and 0.27 was separately repeated clean across them and three
|
||||
# accounts, so the independently checked 0.27 candidate is the operating floor.
|
||||
QWEN_ZIMAGE_GOOGLE_STRENGTH = 0.27
|
||||
|
||||
# The two OpenAI sources first cleared at 0.06225 and 0.0695. Add one full observed
|
||||
# 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,
|
||||
# 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
|
||||
|
||||
_QWEN_ZIMAGE_FLAT_STRENGTH_BY_VENDOR: dict[str, float] = {
|
||||
"google": QWEN_ZIMAGE_GOOGLE_STRENGTH,
|
||||
"openai": QWEN_ZIMAGE_OPENAI_STRENGTH,
|
||||
"microsoft": QWEN_ZIMAGE_MICROSOFT_STRENGTH,
|
||||
}
|
||||
|
||||
|
||||
# sdxl-zimage picks its strength from the VENDOR (unlike qwen-zimage, which derives it
|
||||
# from image area). An unlisted or unknown vendor falls back to the Gemini value.
|
||||
# sdxl-zimage always picks its strength from the vendor. qwen-zimage instead uses
|
||||
# the vendor only for measured cohorts and image area for unknown content. An
|
||||
# unlisted or unknown SDXL vendor falls back to the Gemini value.
|
||||
_SDXL_ZIMAGE_STRENGTH_BY_VENDOR: dict[str, float] = {
|
||||
"openai": SDXL_ZIMAGE_OPENAI_STRENGTH,
|
||||
"google": SDXL_ZIMAGE_GEMINI_STRENGTH,
|
||||
@@ -112,7 +128,8 @@ def strength_default_help() -> str:
|
||||
"""Describe the live default policy without duplicating its values."""
|
||||
return (
|
||||
"profile-adaptive (qwen-zimage uses resolution-adaptive denoise, with a "
|
||||
f"flat oracle-measured Google floor of {QWEN_ZIMAGE_GOOGLE_STRENGTH}; sdxl-zimage "
|
||||
f"flat OpenAI {QWEN_ZIMAGE_OPENAI_STRENGTH} / Google {QWEN_ZIMAGE_GOOGLE_STRENGTH} / "
|
||||
f"Microsoft InvisMark {QWEN_ZIMAGE_MICROSOFT_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)"
|
||||
)
|
||||
@@ -131,10 +148,10 @@ def resolve_strength(
|
||||
from the vendor, so it needs ``size``; returning ``None`` for it instead would push that
|
||||
branch onto every caller and move one of the two strength policies outside this
|
||||
module. ``size`` is required for qwen-zimage without an explicit strength.
|
||||
One measured exception since 0.27.2: Google-provenance content on qwen-zimage
|
||||
takes the flat ``QWEN_ZIMAGE_GOOGLE_STRENGTH`` floor instead of the area curve
|
||||
(the curve's 0.154 top left a 4.33 MP fixture oracle-detected on the full
|
||||
production path; see the constant's comment).
|
||||
Measured vendor exceptions bypass the area curve: OpenAI and Microsoft use one
|
||||
additional observed cross-source spread over their worst clean boundaries, while
|
||||
Google uses a separately repeated cross-source candidate. See the constants'
|
||||
comments for the exact derivations.
|
||||
"""
|
||||
if strength is not None:
|
||||
return strength
|
||||
@@ -142,23 +159,32 @@ def resolve_strength(
|
||||
return _SDXL_ZIMAGE_STRENGTH_BY_VENDOR.get((vendor or "").casefold(), SDXL_ZIMAGE_UNKNOWN_STRENGTH)
|
||||
if size is None:
|
||||
raise ValueError("qwen-zimage resolves strength from image area, so size is required")
|
||||
if (vendor or "").casefold() == "google":
|
||||
return QWEN_ZIMAGE_GOOGLE_STRENGTH
|
||||
vendor_strength = _QWEN_ZIMAGE_FLAT_STRENGTH_BY_VENDOR.get((vendor or "").casefold())
|
||||
if vendor_strength is not None:
|
||||
return vendor_strength
|
||||
from remove_ai_watermarks._internal.qwen_zimage_pipeline import resolution_adaptive_denoise
|
||||
|
||||
return resolution_adaptive_denoise(*size)
|
||||
|
||||
|
||||
def vendor_for_strength(image_path: Path) -> Literal["openai", "google"] | None:
|
||||
"""Select the strength cohort using the input's SynthID provenance evidence."""
|
||||
def vendor_for_strength(image_path: Path) -> Literal["openai", "google", "microsoft"] | None:
|
||||
"""Select the strength cohort from non-invalid pixel-watermark provenance."""
|
||||
try:
|
||||
from remove_ai_watermarks._internal.c2pa import (
|
||||
c2pa_info_has_invalid_credential,
|
||||
c2pa_info_has_invismark,
|
||||
extract_c2pa_info,
|
||||
)
|
||||
from remove_ai_watermarks.metadata import synthid_source
|
||||
|
||||
evidence = (synthid_source(image_path) or "").casefold()
|
||||
info = extract_c2pa_info(image_path)
|
||||
evidence = (synthid_source(image_path, c2pa_info=info) or "").casefold()
|
||||
except Exception:
|
||||
return None
|
||||
if "google" in evidence:
|
||||
return "google"
|
||||
if "openai" in evidence:
|
||||
return "openai"
|
||||
if not c2pa_info_has_invalid_credential(info) and c2pa_info_has_invismark(info):
|
||||
return "microsoft"
|
||||
return None
|
||||
@@ -792,7 +792,7 @@ def _iptc_ai_system_impl(image_path: Path) -> str | None:
|
||||
return iptc_ai_system_in(scan_head(image_path))
|
||||
|
||||
|
||||
def synthid_source(image_path: Path) -> str | None:
|
||||
def synthid_source(image_path: Path, *, c2pa_info: dict[str, Any] | None = None) -> str | None:
|
||||
"""Return the vendor name(s) when provenance establishes SynthID.
|
||||
|
||||
This is provenance-based, not a local pixel decode. Google states that all
|
||||
@@ -808,6 +808,8 @@ def synthid_source(image_path: Path) -> str | None:
|
||||
|
||||
Args:
|
||||
image_path: Path to the image (PNG, JPEG, WebP, or ISOBMFF container).
|
||||
c2pa_info: Already extracted normalized C2PA info, for callers that also need
|
||||
another claim from the same manifest.
|
||||
|
||||
Returns:
|
||||
Comma-joined vendor name(s) (e.g. ``"OpenAI"``) or None.
|
||||
@@ -821,7 +823,7 @@ def synthid_source(image_path: Path) -> str | None:
|
||||
# Prefer the official reader's structured result. A failed asset binding or
|
||||
# signature cannot establish the claim, and the raw fallback below must not
|
||||
# resurrect it from the same damaged manifest bytes.
|
||||
c2pa = extract_c2pa_info(image_path)
|
||||
c2pa = extract_c2pa_info(image_path) if c2pa_info is None else c2pa_info
|
||||
if c2pa_info_has_invalid_credential(c2pa):
|
||||
return None
|
||||
vendors = c2pa.get("synthid_vendors")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Tests for the Content Seal oracle corpus layout.
|
||||
|
||||
Mirrors the synthid corpus guard: the manifest is the source of truth for
|
||||
which binaries exist, and every recorded hash must match the file it names.
|
||||
Derived rows are recipes, not stored files, so only originals are checked
|
||||
against disk.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
CORPUS_DIR = Path(__file__).resolve().parent.parent / "data" / "contentseal"
|
||||
MANIFEST = CORPUS_DIR / "manifest.csv"
|
||||
ORIGINALS = CORPUS_DIR / "originals"
|
||||
|
||||
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
_VALID_VERDICTS = {"detected", "not_detected", ""}
|
||||
_VALID_ORIGINS = {"meta-model-api", "derived", "meta-blog-cdn", "synthetic-local"}
|
||||
|
||||
|
||||
def _manifest_rows() -> list[dict[str, str]]:
|
||||
with open(MANIFEST, newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def test_manifest_original_rows_match_binaries_and_hashes() -> None:
|
||||
rows = [r for r in _manifest_rows() if r["file"]]
|
||||
stored = {path.name for path in ORIGINALS.iterdir() if path.is_file()}
|
||||
assert {r["file"].removeprefix("originals/") for r in rows} == stored
|
||||
|
||||
for row in rows:
|
||||
digest = hashlib.sha256((CORPUS_DIR / row["file"]).read_bytes()).hexdigest()
|
||||
assert digest == row["sha256"], row["file"]
|
||||
|
||||
|
||||
def test_manifest_rows_are_well_formed() -> None:
|
||||
rows = _manifest_rows()
|
||||
assert len({row["sha256"] for row in rows}) == len(rows), "duplicate sha256"
|
||||
|
||||
for row in rows:
|
||||
assert _SHA256.match(row["sha256"]), row["name"]
|
||||
assert row["origin"].split(":")[0] in _VALID_ORIGINS, row["name"]
|
||||
assert row["oracle_verdict"] in _VALID_VERDICTS, row["name"]
|
||||
# Every oracle verdict must carry its check timestamp.
|
||||
if row["oracle_verdict"]:
|
||||
assert row["checked_at_utc"], row["name"]
|
||||
# Detection rows must name the oracle attribution.
|
||||
if row["oracle_verdict"] == "detected":
|
||||
assert "Muse Image 1" in row["oracle_attribution"], row["name"]
|
||||
|
||||
|
||||
def test_default_pipeline_clearance_is_recorded() -> None:
|
||||
"""The verified claim that the default profile clears Content Seal must stay."""
|
||||
rows = {row["name"]: row for row in _manifest_rows()}
|
||||
for name in ("fox_modal_invisible", "text_modal_invisible"):
|
||||
assert rows[name]["oracle_verdict"] == "not_detected", name
|
||||
@@ -596,6 +596,19 @@ class TestSynthIDSourceNonPng:
|
||||
)
|
||||
assert synthid_source(path) == "OpenAI"
|
||||
|
||||
def test_preextracted_c2pa_is_reused(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
"""A caller classifying another claim must not parse the manifest twice."""
|
||||
from remove_ai_watermarks._internal import c2pa
|
||||
|
||||
path = self._c2pa_jpeg(tmp_path, "chatgpt-reused.jpg", b"OpenAI")
|
||||
info = {"synthid_vendors": ["OpenAI"]}
|
||||
|
||||
def unexpected_extract(_path: Path):
|
||||
pytest.fail("synthid_source reparsed pre-extracted C2PA")
|
||||
|
||||
monkeypatch.setattr(c2pa, "extract_c2pa_info", unexpected_extract)
|
||||
assert synthid_source(path, c2pa_info=info) == "OpenAI"
|
||||
|
||||
def test_legacy_openai_c2pa_without_watermark_action_is_none(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, "legacy-chatgpt.jpg", b"OpenAI")
|
||||
assert synthid_source(path) is None
|
||||
|
||||
+32
-9
@@ -15,6 +15,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_OPENAI_STRENGTH,
|
||||
REMOVAL_MODULES,
|
||||
SDXL_ZIMAGE_GEMINI_STRENGTH,
|
||||
SDXL_ZIMAGE_OPENAI_STRENGTH,
|
||||
@@ -198,19 +199,19 @@ class TestNoReembeddedWatermark:
|
||||
|
||||
|
||||
class TestResolveStrength:
|
||||
"""resolve_strength answers for sdxl-zimage and defers for qwen-zimage."""
|
||||
"""resolve_strength owns the qwen-zimage and sdxl-zimage policies."""
|
||||
|
||||
def test_qwen_zimage_answers_from_the_resolution_curve(self):
|
||||
"""The function is total: it owns both policies rather than returning None.
|
||||
|
||||
qwen-zimage picks strength from image area, so it takes the size. Returning
|
||||
None for it would push that branch onto every caller and leave one of the two
|
||||
strength policies living outside this module. Google content is the one
|
||||
measured exception since 0.27.2: it takes the flat oracle-measured floor
|
||||
instead of the curve (the curve's 0.154 top left a 4.33 MP fixture
|
||||
oracle-detected on the full production path).
|
||||
qwen-zimage picks unknown content's strength from image area, so it takes the
|
||||
size. Returning None would push that branch onto every caller and leave one of
|
||||
the two strength policies living outside this module. Measured providers take
|
||||
flat corpus-derived operating points instead.
|
||||
"""
|
||||
assert resolve_strength(None, "openai", "qwen-zimage", size=(2000, 1850)) == pytest.approx(0.154)
|
||||
assert resolve_strength(None, "openai", "qwen-zimage", size=(2000, 1850)) == pytest.approx(
|
||||
QWEN_ZIMAGE_OPENAI_STRENGTH
|
||||
)
|
||||
assert resolve_strength(None, None, "qwen-zimage", size=(600, 500)) == pytest.approx(0.084)
|
||||
assert resolve_strength(None, "google", "qwen-zimage", size=(600, 500)) == QWEN_ZIMAGE_GOOGLE_STRENGTH
|
||||
# The floor holds at every size, not only above the curve's top rung.
|
||||
@@ -221,6 +222,12 @@ 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)])
|
||||
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_sdxl_zimage_uses_its_flat_vendor_ladder(self):
|
||||
|
||||
assert SDXL_ZIMAGE_OPENAI_STRENGTH == 0.15
|
||||
@@ -253,7 +260,7 @@ class TestResolveStrength:
|
||||
|
||||
|
||||
class TestVendorForStrength:
|
||||
"""vendor_for_strength normalizes SynthID provenance to openai/google/None."""
|
||||
"""Normalize supported invisible-watermark provenance to a removal profile."""
|
||||
|
||||
@staticmethod
|
||||
def _patch(value):
|
||||
@@ -271,6 +278,22 @@ class TestVendorForStrength:
|
||||
with self._patch("Google"):
|
||||
assert vendor_for_strength(Path("x.png")) == "google"
|
||||
|
||||
@pytest.mark.parametrize(("integrity", "expected"), [("valid", "microsoft"), ("invalid", None)])
|
||||
def test_only_valid_microsoft_invismark_selects_the_removal_floor(self, integrity, expected):
|
||||
from remove_ai_watermarks._internal.watermark_profiles import vendor_for_strength
|
||||
|
||||
info = {
|
||||
"soft_binding_vendors": ["Microsoft InvisMark"],
|
||||
"c2pa_integrity": integrity,
|
||||
"c2pa_signature": "valid",
|
||||
"c2pa_signer_validity": "valid",
|
||||
}
|
||||
with (
|
||||
self._patch(None),
|
||||
patch("remove_ai_watermarks._internal.c2pa.extract_c2pa_info", return_value=info),
|
||||
):
|
||||
assert vendor_for_strength(Path("x.png")) == expected
|
||||
|
||||
def test_both_issuers_google_wins(self):
|
||||
# The more-robust watermark wins -> safer (higher) strength.
|
||||
from remove_ai_watermarks._internal.watermark_profiles import vendor_for_strength
|
||||
|
||||
@@ -908,6 +908,7 @@ def test_sdxl_zimage_strength_is_vendor_adaptive_and_leaves_other_profiles_alone
|
||||
"""An SDXL global pass needs more strength than Qwen, so it gets its own policy."""
|
||||
from remove_ai_watermarks._internal.watermark_profiles import (
|
||||
QWEN_ZIMAGE_GOOGLE_STRENGTH,
|
||||
QWEN_ZIMAGE_OPENAI_STRENGTH,
|
||||
SDXL_ZIMAGE_GEMINI_STRENGTH,
|
||||
SDXL_ZIMAGE_OPENAI_STRENGTH,
|
||||
resolve_strength,
|
||||
@@ -917,11 +918,12 @@ def test_sdxl_zimage_strength_is_vendor_adaptive_and_leaves_other_profiles_alone
|
||||
assert resolve_strength(None, "google", "sdxl-zimage") == pytest.approx(SDXL_ZIMAGE_GEMINI_STRENGTH)
|
||||
# Unknown provenance takes the stricter of the two.
|
||||
assert resolve_strength(None, None, "sdxl-zimage") == pytest.approx(SDXL_ZIMAGE_GEMINI_STRENGTH)
|
||||
# An explicit value still wins, and qwen-zimage's curve is untouched by this
|
||||
# ladder - with the one measured exception: Google content takes the flat
|
||||
# oracle floor (0.27.2), not the area curve.
|
||||
# An explicit value still wins. qwen-zimage's unknown cohort keeps the curve,
|
||||
# while measured providers take their flat operating points.
|
||||
assert resolve_strength(0.4, "google", "sdxl-zimage") == pytest.approx(0.4)
|
||||
assert resolve_strength(None, "openai", "qwen-zimage", size=(2000, 1850)) == pytest.approx(0.154)
|
||||
assert resolve_strength(None, "openai", "qwen-zimage", size=(2000, 1850)) == pytest.approx(
|
||||
QWEN_ZIMAGE_OPENAI_STRENGTH
|
||||
)
|
||||
assert resolve_strength(None, "google", "qwen-zimage", size=(2000, 1850)) == pytest.approx(
|
||||
QWEN_ZIMAGE_GOOGLE_STRENGTH
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user