mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-28 08:20:25 +02:00
Ship the measured Meta Content Seal cohort with auto routing and --vendor override
Full Meta Muse Image support in the invisible-removal path: - QWEN_ZIMAGE_META_STRENGTH = 0.1: derived by the standard worst-boundary-plus-cross-source-spread method over five oracle-bracketed generations (data/contentseal/manifest.csv) - Auto mode: vendor_for_strength routes a file whose only provenance is the standalone AI IPTC trainedAlgorithmicMedia tag onto the meta cohort; C2PA issuers win first, so Google/OpenAI/Microsoft routing is unchanged. Muse WebP outputs place the XMP in a tail chunk, so the scan uses the shared chunk-aware metadata.scan_head rather than a plain head read - Explicit override: --vendor on invisible/all/batch and InvisibleOptions.vendor name the cohort on stripped files; naming a cohort asserts the watermark is present, so the no-signal gate treats it like --force at both the CLI and API seams - sdxl-zimage has no measured Meta rung: an explicit meta vendor falls to the conservative unknown 0.25 rather than inventing one - identify emits a Content Seal caveat pointing at the removal path - The legacy visible 'Imagined with AI' mark stays unregistered: a dedicated sample hunt (newsroom mockups, community posts, press screenshots, dead imagine.meta.com, broken Wayback captures) found no pixel-verifiable capture, and the registry rule forbids encoding a corner without one. erase --region remains its removal path; outcome recorded in the landscape Co-Authored-By: Claude Fable 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 4.5
parent
a1811b6221
commit
ab528ec0e8
@@ -426,6 +426,39 @@ class TestInvisibleCommand:
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_engine.remove_watermark.assert_called_once()
|
||||
|
||||
def test_invisible_explicit_vendor_implies_force_and_sets_cohort(self, runner, sample_png, tmp_path):
|
||||
"""--vendor meta names a cohort the file cannot prove (Content Seal carries
|
||||
no C2PA), so it must both bypass the no-signal skip and arrive at the engine
|
||||
as the vendor, where it resolves to the measured Meta floor (not the
|
||||
area-curve value the same size would otherwise get)."""
|
||||
mock_cls, mock_engine = _mock_invisible_engine()
|
||||
output = tmp_path / "clean.png"
|
||||
with (
|
||||
patch("remove_ai_watermarks.invisible_engine.is_available", return_value=True),
|
||||
patch("remove_ai_watermarks.cli.InvisibleEngine", mock_cls, create=True),
|
||||
patch("remove_ai_watermarks.invisible_engine.InvisibleEngine", mock_cls),
|
||||
):
|
||||
result = runner.invoke(main, ["invisible", str(sample_png), "-o", str(output), "--vendor", "meta"])
|
||||
assert result.exit_code == 0, result.output
|
||||
kwargs = mock_engine.remove_watermark.call_args.kwargs
|
||||
assert kwargs["vendor"] == "meta"
|
||||
assert "0.1" in result.output # the resolved Meta floor, printed
|
||||
assert "0.094" not in result.output # not the sample's area-curve value (200x200 -> ~0.0944)
|
||||
|
||||
def test_invisible_vendor_auto_keeps_detection_semantics(self, runner, sample_png, tmp_path):
|
||||
"""--vendor auto is the default spelled out: detection still runs and a
|
||||
no-signal file still skips."""
|
||||
mock_cls, mock_engine = _mock_invisible_engine()
|
||||
output = tmp_path / "clean.png"
|
||||
with (
|
||||
patch("remove_ai_watermarks.invisible_engine.is_available", return_value=True),
|
||||
patch("remove_ai_watermarks.cli.InvisibleEngine", mock_cls, create=True),
|
||||
patch("remove_ai_watermarks.invisible_engine.InvisibleEngine", mock_cls),
|
||||
):
|
||||
result = runner.invoke(main, ["invisible", str(sample_png), "-o", str(output), "--vendor", "auto"])
|
||||
assert result.exit_code == 2, result.output
|
||||
mock_engine.remove_watermark.assert_not_called()
|
||||
|
||||
def test_invisible_runs_without_force_when_signal_present(self, runner, tmp_path):
|
||||
"""An image carrying an AI metadata signal IS a scrub target, so the run
|
||||
proceeds with no --force needed."""
|
||||
|
||||
+51
-1
@@ -6,6 +6,7 @@ answer and a clean refusal rather than a fallback ladder.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -15,6 +16,7 @@ from remove_ai_watermarks._internal.utils import get_image_format, is_supported_
|
||||
from remove_ai_watermarks._internal.watermark_profiles import (
|
||||
PROFILE_CHOICES,
|
||||
QWEN_ZIMAGE_GOOGLE_STRENGTH,
|
||||
QWEN_ZIMAGE_META_STRENGTH,
|
||||
QWEN_ZIMAGE_OPENAI_STRENGTH,
|
||||
REMOVAL_MODULES,
|
||||
SDXL_ZIMAGE_GEMINI_STRENGTH,
|
||||
@@ -198,6 +200,30 @@ class TestNoReembeddedWatermark:
|
||||
assert "add_watermarker" not in calls["controlnet"]
|
||||
|
||||
|
||||
# Minimal WebP stub whose XMP chunk carries the IPTC trainedAlgorithmicMedia
|
||||
# tag exactly as Muse outputs place it (built inline so the test has no binary
|
||||
# fixture dependency).
|
||||
_XMP_PAYLOAD = (
|
||||
b'<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax#">'
|
||||
b'<rdf:Description rdf:about="" xmlns:iptcExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/" '
|
||||
b'iptcExt:DigitalSourceType="http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"/>'
|
||||
b"</rdf:RDF></x:xmpmeta>"
|
||||
)
|
||||
|
||||
|
||||
def _webp_stub(xmp: bytes | None) -> bytes:
|
||||
def chunk(cid: bytes, data: bytes) -> bytes:
|
||||
return cid + struct.pack("<I", len(data)) + data + (b"\x00" if len(data) % 2 else b"")
|
||||
|
||||
body = b"WEBP" + chunk(b"VP8L", b"\x00" * 20)
|
||||
if xmp is not None:
|
||||
body += chunk(b"XMP ", xmp)
|
||||
return b"RIFF" + struct.pack("<I", len(body)) + body
|
||||
|
||||
|
||||
_MUSE_WEBP_WITH_IPTC_TAG = _webp_stub(_XMP_PAYLOAD)
|
||||
|
||||
|
||||
class TestResolveStrength:
|
||||
"""resolve_strength owns the qwen-zimage and sdxl-zimage policies."""
|
||||
|
||||
@@ -222,12 +248,36 @@ class TestResolveStrength:
|
||||
with pytest.raises(ValueError, match="size is required"):
|
||||
resolve_strength(None, "openai", "qwen-zimage")
|
||||
|
||||
@pytest.mark.parametrize(("vendor", "expected"), [("microsoft", 0.15), ("openai", 0.07675)])
|
||||
@pytest.mark.parametrize(("vendor", "expected"), [("microsoft", 0.15), ("openai", 0.07675), ("meta", 0.1)])
|
||||
def test_qwen_zimage_measured_vendors_use_flat_cross_source_margins(self, vendor, expected):
|
||||
"""Measured providers must not fall below their operating points on small files."""
|
||||
assert resolve_strength(None, vendor, "qwen-zimage", size=(600, 500)) == pytest.approx(expected)
|
||||
assert resolve_strength(None, vendor, "qwen-zimage", size=(4000, 3000)) == pytest.approx(expected)
|
||||
|
||||
def test_meta_floor_is_size_independent_and_sdxl_falls_to_unknown(self):
|
||||
"""The Meta floor bypasses the area curve at every size (Content Seal removal
|
||||
was bracketed on 2.56 MP generations and derived as a flat cross-source
|
||||
margin, like the other measured cohorts), while sdxl-zimage has no measured
|
||||
Meta rung and must fall to the conservative unknown value, not invent one."""
|
||||
for size in ((600, 500), (1600, 1600), (1920, 1280), (4000, 3000)):
|
||||
assert resolve_strength(None, "meta", "qwen-zimage", size=size) == pytest.approx(QWEN_ZIMAGE_META_STRENGTH)
|
||||
assert resolve_strength(None, "meta", "sdxl-zimage") == SDXL_ZIMAGE_UNKNOWN_STRENGTH
|
||||
|
||||
def test_vendor_for_strength_routes_standalone_iptc_to_meta(self, tmp_path):
|
||||
"""Auto mode: a file whose only provenance is the AI IPTC tag routes to the
|
||||
meta cohort (Muse carries no C2PA; the tag is its fallback companion), while
|
||||
a file without the tag stays on the resolution curve and a C2PA issuer still
|
||||
wins over the tag."""
|
||||
from remove_ai_watermarks._internal.watermark_profiles import vendor_for_strength
|
||||
|
||||
tagged = tmp_path / "tagged.webp"
|
||||
tagged.write_bytes(_MUSE_WEBP_WITH_IPTC_TAG)
|
||||
assert vendor_for_strength(tagged) == "meta"
|
||||
|
||||
stripped = tmp_path / "stripped.webp"
|
||||
stripped.write_bytes(b"RIFF\x24\x00\x00\x00WEBPVP8 \x18\x00\x00\x00" + b"\x00" * 16)
|
||||
assert vendor_for_strength(stripped) is None
|
||||
|
||||
def test_sdxl_zimage_uses_its_flat_vendor_ladder(self):
|
||||
|
||||
assert SDXL_ZIMAGE_OPENAI_STRENGTH == 0.15
|
||||
|
||||
Reference in New Issue
Block a user