mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-31 01:30:35 +02:00
Merge remote-tracking branch 'origin/main' into docs/arxiv-paper-review
# Conflicts: # docs/installation.md # docs/supported-signals.md # docs/verification-plan.md
This commit is contained in:
+35
-2
@@ -228,8 +228,8 @@ class TestVisibleCommand:
|
||||
# The transparent corners must remain transparent.
|
||||
assert out[0, 0, 3] == 0
|
||||
assert out[199, 199, 3] == 0
|
||||
# The opaque centre remains opaque (the watermark region default is bottom-right,
|
||||
# which doesn't overlap the centre square at 200x200).
|
||||
# The opaque center remains opaque (the watermark region default is bottom-right,
|
||||
# which doesn't overlap the center square at 200x200).
|
||||
assert out[100, 100, 3] == 255
|
||||
|
||||
def test_visible_keeps_alpha_opaque_in_watermark_region(self, runner, tmp_path):
|
||||
@@ -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."""
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""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
|
||||
|
||||
|
||||
def test_deterministic_transforms_reproduce_recorded_hashes(tmp_path: Path) -> None:
|
||||
from scripts.contentseal_transforms import reproduce_transforms
|
||||
|
||||
outputs = reproduce_transforms(tmp_path)
|
||||
|
||||
assert len(outputs) == 8
|
||||
assert all(path.is_file() for path in outputs)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Regression: a manifest that names its own forensic soft binding must not
|
||||
also report a SynthID watermark from the generic vendor-token inference.
|
||||
|
||||
Microsoft Designer manifests sign as Microsoft, carry the InvisMark
|
||||
``c2pa.watermarked`` action, and name their generation agent
|
||||
"Azure OpenAI ImageGen". The OpenAI issuer token inside that agent name plus
|
||||
the watermarked action used to satisfy the OpenAI SynthID-evidence rule,
|
||||
double-counting one forensic mark as two pixel watermarks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from remove_ai_watermarks._internal.c2pa import c2pa_info_from_manifest_store
|
||||
|
||||
DESIGNER_STORE = {
|
||||
"active_manifest": "designer",
|
||||
"manifests": {
|
||||
"designer": {
|
||||
"signature_info": {"issuer": "Microsoft Corporation", "common_name": "Microsoft Corporation"},
|
||||
"claim_generator_info": [{"name": "Microsoft Responsible AI Provenance", "version": "1.0"}],
|
||||
"assertions": [
|
||||
{
|
||||
"label": "c2pa.actions",
|
||||
"data": {
|
||||
"actions": [
|
||||
{
|
||||
"action": "c2pa.created",
|
||||
"softwareAgent": {"name": "Azure OpenAI ImageGen"},
|
||||
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
|
||||
},
|
||||
{"action": "c2pa.watermarked"},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "c2pa.soft-binding",
|
||||
"data": {
|
||||
"alg": "com.microsoft.invismark.1",
|
||||
"blocks": [{"value": "bf7a2993-cc1f-47e1-b1f0-cd8839aabb22"}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_named_soft_binding_suppresses_generic_synthid_evidence() -> None:
|
||||
info = c2pa_info_from_manifest_store(DESIGNER_STORE)
|
||||
assert info["ai_source_kind"] == "generated"
|
||||
assert info["soft_binding_algorithm"] == "com.microsoft.invismark.1"
|
||||
assert info.get("synthid_watermark") is None
|
||||
assert info.get("synthid_vendors") is None
|
||||
|
||||
|
||||
def test_vendor_agent_name_alone_is_not_the_vendors_provenance() -> None:
|
||||
"""The identity-scoped inference must not fire on a service name either.
|
||||
|
||||
Same manifest without the soft binding: the "Azure OpenAI ImageGen" agent
|
||||
is not an OpenAI signature or claim generator, so no OpenAI SynthID
|
||||
evidence may be derived from it.
|
||||
"""
|
||||
store = {
|
||||
"active_manifest": "designer",
|
||||
"manifests": {
|
||||
"designer": {
|
||||
"signature_info": {"issuer": "Microsoft Corporation", "common_name": "Microsoft Corporation"},
|
||||
"claim_generator_info": [{"name": "Microsoft Responsible AI Provenance", "version": "1.0"}],
|
||||
"assertions": [
|
||||
{
|
||||
"label": "c2pa.actions",
|
||||
"data": {
|
||||
"actions": [
|
||||
{
|
||||
"action": "c2pa.created",
|
||||
"softwareAgent": {"name": "Azure OpenAI ImageGen"},
|
||||
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
|
||||
},
|
||||
{"action": "c2pa.watermarked"},
|
||||
]
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
info = c2pa_info_from_manifest_store(store)
|
||||
assert info.get("synthid_watermark") is None
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Cross-corpus integrity checks for tracked evaluation tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
EVALUATIONS = ROOT / "data" / "evaluations"
|
||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def test_every_recorded_evaluation_sha256_is_well_formed() -> None:
|
||||
checked = 0
|
||||
for path in sorted(EVALUATIONS.rglob("*.csv")):
|
||||
with path.open(newline="", encoding="utf-8") as stream:
|
||||
for line_number, row in enumerate(csv.DictReader(stream), start=2):
|
||||
for field, value in row.items():
|
||||
if field is not None and field.endswith("sha256") and value:
|
||||
assert SHA256.fullmatch(value), f"{path.relative_to(ROOT)}:{line_number} {field}={value!r}"
|
||||
checked += 1
|
||||
assert checked > 0
|
||||
+89
-20
@@ -17,7 +17,11 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks._internal.c2pa import c2pa_info_from_manifest_store
|
||||
from remove_ai_watermarks._internal.constants import C2PA_AI_VENDORS, C2PA_CLAIM_GENERATOR_PLATFORMS
|
||||
from remove_ai_watermarks._internal.constants import (
|
||||
C2PA_AI_VENDORS,
|
||||
C2PA_CLAIM_GENERATOR_PLATFORMS,
|
||||
C2PA_IDENTITY_AI_ORGS,
|
||||
)
|
||||
from remove_ai_watermarks.identify import (
|
||||
ProvenanceEvidence,
|
||||
ProvenanceReport,
|
||||
@@ -179,7 +183,7 @@ class TestProvenanceEvidence:
|
||||
|
||||
assert report.is_ai_generated is True
|
||||
assert report.confidence == "high"
|
||||
assert report.platform == "OpenAI (ChatGPT / gpt-image / DALL-E / Sora)"
|
||||
assert report.platform == "OpenAI (ChatGPT / GPT Image / DALL·E / Sora)"
|
||||
assert not any("not anchored" in caveat for caveat in report.caveats)
|
||||
|
||||
def test_external_metadata_record_builds_equivalent_evidence(self, tmp_path: Path):
|
||||
@@ -244,7 +248,7 @@ class TestProvenanceEvidence:
|
||||
report = identify_from_evidence(evidence_from_metadata_record(record, path=path))
|
||||
|
||||
assert report.is_ai_generated is True
|
||||
assert report.platform == "OpenAI (ChatGPT / gpt-image / DALL-E / Sora)"
|
||||
assert report.platform == "OpenAI (ChatGPT / GPT Image / DALL·E / Sora)"
|
||||
assert [signal.name for signal in report.signals] == ["c2pa"]
|
||||
|
||||
def test_external_generator_bytes_are_normalized(self, tmp_path: Path):
|
||||
@@ -374,13 +378,12 @@ class TestAttributePlatform:
|
||||
assert platform
|
||||
assert "Canva" in platform
|
||||
|
||||
def test_byteplus_attributes_to_bytedance(self):
|
||||
def test_byteplus_keeps_its_product_name(self):
|
||||
# ByteDance's intl brand signs as "Byteplus Pte. Ltd."; the registry maps
|
||||
# it to the ByteDance platform (was mis-read as Adobe via an incidental
|
||||
# it to the ByteDance family (was mis-read as Adobe via an incidental
|
||||
# "Adobe XMP" file string before the entry existed).
|
||||
platform = _attribute_platform(["BytePlus (ByteDance)"])
|
||||
assert platform
|
||||
assert "ByteDance" in platform
|
||||
assert platform == "BytePlus (ByteDance)"
|
||||
|
||||
def test_empty_is_none(self):
|
||||
assert _attribute_platform([]) is None
|
||||
@@ -444,7 +447,7 @@ class TestIdentifyNonPng:
|
||||
path = self._c2pa_jpeg(tmp_path, b"certificate_center@volcengine.com ... trainedAlgorithmicMedia")
|
||||
r = identify(path, check_visible=False, check_invisible=False)
|
||||
assert r.is_ai_generated is True
|
||||
assert "ByteDance" in (r.platform or "")
|
||||
assert r.platform == "ByteDance Volcano Engine"
|
||||
|
||||
def test_bytedance_chinese_legal_name_attributed(self, tmp_path: Path):
|
||||
# Some Volcano Engine certs name the signer with the Chinese legal entity
|
||||
@@ -454,7 +457,7 @@ class TestIdentifyNonPng:
|
||||
path = self._c2pa_jpeg(tmp_path, blob)
|
||||
r = identify(path, check_visible=False, check_invisible=False)
|
||||
assert r.is_ai_generated is True
|
||||
assert "ByteDance" in (r.platform or "")
|
||||
assert r.platform == "ByteDance Volcano Engine"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("claim_generator", "platform"),
|
||||
@@ -498,7 +501,7 @@ class TestIdentifyNonPng:
|
||||
path = self._c2pa_jpeg(tmp_path, b"Bytedance Pte. Ltd. Dreamina/7.5.0 c2pa.created")
|
||||
r = identify(path, check_visible=False, check_invisible=False)
|
||||
assert r.is_ai_generated is True
|
||||
assert "ByteDance" in (r.platform or "")
|
||||
assert r.platform == "ByteDance Dreamina"
|
||||
|
||||
def test_elevenlabs_attributed(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, b"Eleven Labs Inc. ... trainedAlgorithmicMedia")
|
||||
@@ -666,6 +669,57 @@ class TestIdentifyRealSamples:
|
||||
assert r.is_ai_generated is True
|
||||
assert r.ai_source_kind == "enhanced"
|
||||
|
||||
def test_standalone_ai_tag_attributes_the_content_seal(self, tmp_path: Path):
|
||||
"""A standalone AI digital-source tag emits the seal as its own signal.
|
||||
|
||||
Muse Image outputs carry no C2PA; this tag is their only provenance, and
|
||||
Muse stamps every output with the invisible Content Seal. The signal is
|
||||
the strength router's Meta bet as evidence - an attribution, not a decode
|
||||
(no public decoder exists), so its confidence is medium and the caveat
|
||||
still points at the oracle.
|
||||
"""
|
||||
p = tmp_path / "muse-tag.jpg"
|
||||
p.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
|
||||
)
|
||||
|
||||
r = identify(p, check_visible=False, check_invisible=False)
|
||||
|
||||
names = [s.name for s in r.signals]
|
||||
assert "iptc" in names
|
||||
assert "content_seal" in names
|
||||
seal = next(s for s in r.signals if s.name == "content_seal")
|
||||
assert seal.confidence == "medium"
|
||||
assert "Invisible Content Seal watermark (Meta Muse attribution)" in r.watermarks
|
||||
assert any("meta.ai/identification" in c for c in r.caveats)
|
||||
|
||||
def test_seal_platform_attribution_follows_the_signal(self, tmp_path: Path):
|
||||
"""The Likely-source line follows the same bet the seal signal makes.
|
||||
|
||||
Apple keeps its own attribution; every other standalone-tag file gets the
|
||||
hedged Muse attribution instead of "platform not specified", so the panel
|
||||
that prices the Content Seal and the source line agree.
|
||||
"""
|
||||
muse = tmp_path / "muse.jpg"
|
||||
muse.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
|
||||
)
|
||||
apple = tmp_path / "apple.jpg"
|
||||
apple.write_bytes(
|
||||
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="compositeWithTrainedAlgorithmicMedia" '
|
||||
b'photoshop:Credit="Apple Photos Clean Up"></x:xmpmeta>\xff\xd9'
|
||||
)
|
||||
|
||||
assert identify(muse, check_visible=False, check_invisible=False).platform == (
|
||||
"Meta Muse Image (attributed by the standalone AI digital-source tag)"
|
||||
)
|
||||
assert identify(apple, check_visible=False, check_invisible=False).platform == "Apple Photos (Clean Up AI edit)"
|
||||
|
||||
def test_c2pa_backed_file_gets_no_content_seal_attribution(self):
|
||||
"""C2PA issuers win first: a manifest-backed file is not Meta-routed."""
|
||||
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False, check_invisible=False)
|
||||
assert "content_seal" not in [s.name for s in r.signals]
|
||||
|
||||
def test_flux_bfl_c2pa_png(self):
|
||||
# flux-1.png: real Black Forest Labs FLUX.2 Playground output (signed C2PA).
|
||||
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False)
|
||||
@@ -844,7 +898,7 @@ class TestIdentifyAigcPngChunk:
|
||||
assert "doubao" in signal.detail
|
||||
|
||||
|
||||
# ── HuggingFace-hosted job marker (medium confidence) ───────────────
|
||||
# ── Hugging Face-hosted job marker (medium confidence) ─────────────
|
||||
|
||||
|
||||
class TestIdentifyHuggingFaceJob:
|
||||
@@ -866,7 +920,7 @@ class TestIdentifyHuggingFaceJob:
|
||||
assert r.is_ai_generated is True
|
||||
assert r.confidence == "medium"
|
||||
assert r.platform is not None
|
||||
assert "HuggingFace" in r.platform
|
||||
assert "Hugging Face" in r.platform
|
||||
signal = next(s for s in r.signals if s.name == "hf_job")
|
||||
assert signal.confidence == "medium"
|
||||
|
||||
@@ -1170,7 +1224,7 @@ class TestSynthIDProvenanceEvidence:
|
||||
png = self._png(tmp_path, "dreamina.png", self._png_chunk(b"caBX", b"jumbc2pa Dreamina/7.5.0 c2pa.created"))
|
||||
r = identify(png, check_visible=False, check_invisible=False)
|
||||
assert r.is_ai_generated is True
|
||||
assert "ByteDance" in (r.platform or "")
|
||||
assert r.platform == "ByteDance Dreamina"
|
||||
|
||||
|
||||
class TestReportSerializable:
|
||||
@@ -1401,7 +1455,7 @@ class TestIdentifyAIGC:
|
||||
|
||||
class TestVendorOf:
|
||||
def test_openai_variants(self):
|
||||
assert _vendor_of("OpenAI (ChatGPT / gpt-image / DALL-E / Sora)") == "OpenAI"
|
||||
assert _vendor_of("OpenAI (ChatGPT / GPT Image / DALL·E / Sora)") == "OpenAI"
|
||||
assert _vendor_of("DALL-E 3") == "OpenAI"
|
||||
|
||||
def test_google_variants(self):
|
||||
@@ -1427,17 +1481,32 @@ class TestVendorOf:
|
||||
# entered clash detection (a coverage hole). They now normalize to one origin.
|
||||
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("ByteDance Volcano Engine") == "ByteDance"
|
||||
assert _vendor_of("BytePlus (ByteDance)") == "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"
|
||||
assert _vendor_of("Ideogram") == "Ideogram"
|
||||
|
||||
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
|
||||
def test_ideogram_issuer_attributed(self):
|
||||
# Corpus evidence 2026-08-08: four uploads signed "Ideogram, Inc" read as
|
||||
# unknown-signer C2PA with no platform. The issuer token is the org prefix.
|
||||
platforms = {v.issuer: v.platform for v in C2PA_AI_VENDORS}
|
||||
assert platforms[b"Ideogram"] == "Ideogram"
|
||||
assert "Ideogram" in C2PA_IDENTITY_AI_ORGS
|
||||
assert _issuers_in(b"...CN=Ideogram, Inc...trainedAlgorithmicMedia") == ["Ideogram"]
|
||||
|
||||
def test_bytedance_issuers_keep_the_source_product(self):
|
||||
platforms = {
|
||||
vendor.issuer: vendor.platform
|
||||
for vendor in C2PA_AI_VENDORS
|
||||
if vendor.org.startswith("ByteDance") or vendor.org.startswith("BytePlus")
|
||||
}
|
||||
assert platforms[b"volcengine"] == "ByteDance Volcano Engine"
|
||||
assert platforms[b"Byteplus"] == "BytePlus (ByteDance)"
|
||||
assert platforms[b"Dreamina"] == "ByteDance Dreamina"
|
||||
assert ("dreamina", "ByteDance Dreamina") in C2PA_CLAIM_GENERATOR_PLATFORMS
|
||||
|
||||
|
||||
class TestIntegrityClashesHelper:
|
||||
|
||||
@@ -60,6 +60,7 @@ class TestVerifiedTextMode:
|
||||
manifest.write_text("{}", encoding="utf-8")
|
||||
cases = (
|
||||
("sdxl-zimage", {}, "qwen-zimage"),
|
||||
("qwen-zimage", {"tile": True}, "not calibrated with --tile"),
|
||||
("qwen-zimage", {"max_resolution": 1024}, "max-resolution 0"),
|
||||
("qwen-zimage", {"humanize": 1.0}, "humanize=0"),
|
||||
("qwen-zimage", {"adaptive_polish": True}, "polish disabled"),
|
||||
@@ -128,10 +129,6 @@ class TestVerifiedTextMode:
|
||||
|
||||
assert seen["fidelity_anchor"] is True
|
||||
|
||||
engine.remove_watermark(source, output, text_manifest=manifest, tile=True)
|
||||
|
||||
assert seen["tile"] is True
|
||||
|
||||
|
||||
class TestNativeOutputSize:
|
||||
"""Model-side latent-grid rounding must not change the public output size."""
|
||||
@@ -248,7 +245,7 @@ class TestEngineResolvesThePolishPerProfile:
|
||||
|
||||
This is the change that stopped a library caller and a CLI caller on one profile
|
||||
from producing different pixels, and it had no test: rebinding
|
||||
``resolve_adaptive_polish`` to ``bool(value)`` -- exactly the pre-commit behaviour --
|
||||
``resolve_adaptive_polish`` to ``bool(value)`` -- exactly the pre-commit behavior --
|
||||
left the whole suite green.
|
||||
"""
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestConfig:
|
||||
assert KlingEngine().config.provenance_ncc_factor == 1.0
|
||||
|
||||
def test_gate_above_clean_arm_max(self):
|
||||
# Clean arm scored p99 0.304 / max 0.320 on 286 hand-labelled frames; the
|
||||
# Clean arm scored p99 0.304 / max 0.320 on 286 hand-labeled frames; the
|
||||
# gate must sit above that with margin.
|
||||
assert KlingEngine().config.detect_ncc_threshold > 0.32
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the LibLibAI ("LibLibAI" wordmark) visible-watermark engine.
|
||||
"""Tests for the LiblibAI ("LiblibAI" wordmark) visible-watermark engine.
|
||||
|
||||
Every tuned constant in ``liblib_engine`` was measured on the 15-frame vendor
|
||||
cohort (2026-07-22); these tests pin the load-bearing ones: the bottom-CENTER
|
||||
@@ -24,7 +24,7 @@ _MARK_FRAC = 0.10 # measured wordmark width, fraction of the frame WIDTH
|
||||
|
||||
|
||||
def _compose(w: int, h: int, bg: float = 100.0):
|
||||
"""Composite a triangle logo + the LibLibAI wordmark, bottom-center."""
|
||||
"""Composite a triangle logo + the LiblibAI wordmark, bottom-center."""
|
||||
img = np.full((h, w, 3), bg, np.float32)
|
||||
at = _alpha_template()
|
||||
gw = int(_MARK_FRAC * w)
|
||||
@@ -112,8 +112,8 @@ class TestDetectAndMask:
|
||||
assert eng.footprint_mask(img) is None
|
||||
|
||||
def test_confident_liblib_detection_suppresses_the_jimeng_pill(self):
|
||||
# A LibLibAI image is TC260 too but is not Jimeng-basic: like Doubao/Qwen/
|
||||
# Kling/RunningHub/Baidu, a confident LibLibAI detection must veto the pill.
|
||||
# A LiblibAI image is TC260 too but is not Jimeng-basic: like Doubao/Qwen/
|
||||
# Kling/RunningHub/Baidu, a confident LiblibAI detection must veto the pill.
|
||||
# It was the one mark the hand-written veto list in ``_keep_pill`` missed.
|
||||
from remove_ai_watermarks.watermark_registry import _keep_pill
|
||||
|
||||
|
||||
@@ -1562,7 +1562,7 @@ class TestAIGCLabel:
|
||||
|
||||
|
||||
class TestHuggingFaceJob:
|
||||
"""HuggingFace-hosted job marker (``hf-job-id`` PNG text chunk)."""
|
||||
"""Hugging Face-hosted job marker (``hf-job-id`` PNG text chunk)."""
|
||||
|
||||
def _hf_png(self, tmp_path: Path, job_id: str = "ec8380a6-2091-423a-b835-209420f99ee1") -> Path:
|
||||
p = tmp_path / "hfjob.png"
|
||||
@@ -1622,10 +1622,13 @@ class TestSoftBinding:
|
||||
from remove_ai_watermarks._internal.c2pa import soft_binding_vendors_in
|
||||
|
||||
assert soft_binding_vendors_in(b"...alg...com.adobe.trustmark.P...") == ["Adobe TrustMark"]
|
||||
assert soft_binding_vendors_in(b"com.digimarc.validate.1") == ["Digimarc"]
|
||||
assert soft_binding_vendors_in(b"com.digimarc.validate.1") == ["Digimarc Validate"]
|
||||
assert soft_binding_vendors_in(b"ai.steg.api blah") == ["Steg.AI"]
|
||||
# Registry-verified vendors added in v0.6.x.
|
||||
assert soft_binding_vendors_in(b"ai.trufo.gen1.image") == ["Trufo"]
|
||||
assert soft_binding_vendors_in(b"ai.trufo.pawprint.watermark") == ["Trufo PawPrint"]
|
||||
assert soft_binding_vendors_in(b"com.aiwatermark.pixelseal.1") == ["AIWatermark PixelSeal"]
|
||||
assert soft_binding_vendors_in(b"com.aiwatermark.videoseal.1") == ["AIWatermark VideoSeal"]
|
||||
assert soft_binding_vendors_in(b"com.aiwatermark.audioseal.1") == ["AIWatermark AudioSeal"]
|
||||
assert soft_binding_vendors_in(b"io.iscc.v0") == ["ISCC (content code)"]
|
||||
|
||||
def test_vendors_in_empty_when_absent(self):
|
||||
|
||||
@@ -266,8 +266,8 @@ class TestC2PA:
|
||||
|
||||
def test_content_fingerprint_does_not_trigger_invisible_removal(self):
|
||||
info = {
|
||||
"soft_binding": "Adobe (content fingerprint)",
|
||||
"soft_binding_vendors": ["Adobe (content fingerprint)"],
|
||||
"soft_binding": "Adobe Image Comparator Network",
|
||||
"soft_binding_vendors": ["Adobe Image Comparator Network"],
|
||||
}
|
||||
|
||||
assert c2pa_info_has_removal_hint(info) is False
|
||||
@@ -1007,7 +1007,7 @@ class TestTc260ContainerRouting:
|
||||
def _riff_chunk(chunk_id: bytes, payload: bytes) -> bytes:
|
||||
return chunk_id + len(payload).to_bytes(4, "little") + payload + (b"\x00" if len(payload) & 1 else b"")
|
||||
|
||||
def _labelled_avi(self) -> bytes:
|
||||
def _labeled_avi(self) -> bytes:
|
||||
info = self._riff_chunk(b"AIGC", _TC260_AIGC_VALUE)
|
||||
body = b"AVI " + self._riff_chunk(b"LIST", b"INFO" + info)
|
||||
return b"RIFF" + len(body).to_bytes(4, "little") + body
|
||||
@@ -1016,7 +1016,7 @@ class TestTc260ContainerRouting:
|
||||
from remove_ai_watermarks.metadata import aigc_label
|
||||
|
||||
target = tmp_path / "clip.bin" # correct AVI bytes, wrong suffix
|
||||
target.write_bytes(self._labelled_avi())
|
||||
target.write_bytes(self._labeled_avi())
|
||||
label = aigc_label(target)
|
||||
assert label is not None
|
||||
assert label["Label"] == "1"
|
||||
@@ -1025,7 +1025,7 @@ class TestTc260ContainerRouting:
|
||||
from remove_ai_watermarks.metadata import aigc_label
|
||||
|
||||
target = tmp_path / "clip.avi"
|
||||
target.write_bytes(self._labelled_avi())
|
||||
target.write_bytes(self._labeled_avi())
|
||||
assert aigc_label(target) is not None
|
||||
|
||||
def test_webp_yields_nothing_from_the_riff_reader(self, tmp_path: Path):
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Tests for the measured Microsoft top-right AI-badge engine.
|
||||
|
||||
The covered variant is a white top-right pill with dark internal shapes. The
|
||||
2026-08-27 calibration kept visually confirmed carriers, provenance-only files,
|
||||
and no-signal controls separate. These tests pin the load-bearing constants --
|
||||
especially the long-side scale basis and the internal holes as the discriminator.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks import watermark_registry as registry
|
||||
from remove_ai_watermarks.microsoft_engine import (
|
||||
_ALPHA_HEIGHT_FRAC,
|
||||
_ALPHA_WIDTH_FRAC,
|
||||
MicrosoftEngine,
|
||||
_alpha_template,
|
||||
)
|
||||
|
||||
_INSET = 0.010 # measured pill inset from the top/right edges (long-side fraction)
|
||||
|
||||
|
||||
def _pill_geometry(w: int, h: int) -> tuple[int, int, int, int]:
|
||||
long_side = max(w, h)
|
||||
pw = int(_ALPHA_WIDTH_FRAC * long_side)
|
||||
ph = max(4, int(_ALPHA_HEIGHT_FRAC * long_side))
|
||||
pad = int(_INSET * long_side)
|
||||
return w - pad - pw, pad, pw, ph
|
||||
|
||||
|
||||
def _compose(w: int, h: int, bg: float = 110.0):
|
||||
"""Composite the synthetic pill at its measured size onto a flat background."""
|
||||
img = np.full((h, w, 3), bg, np.uint8)
|
||||
at = _alpha_template()
|
||||
x0, y0, pw, ph = _pill_geometry(w, h)
|
||||
pill = cv2.resize(at, (pw, ph))
|
||||
region = img[y0 : y0 + ph, x0 : x0 + pw]
|
||||
bright = pill > 0.6
|
||||
region[bright] = 245
|
||||
# Internal holes are dark ink inside the pill, not background.
|
||||
region[~bright] = 45
|
||||
return img, (x0, y0, pw, ph)
|
||||
|
||||
|
||||
def _plain_pill(w: int, h: int, text: str | None = None) -> np.ndarray:
|
||||
"""Return a white rounded pill without the expected holes, or with foreign text."""
|
||||
img = np.full((h, w, 3), 110.0, np.uint8)
|
||||
x0, y0, pw, ph = _pill_geometry(w, h)
|
||||
cv2.rectangle(img, (x0, y0), (x0 + pw, y0 + ph), (245, 245, 245), -1)
|
||||
cv2.circle(img, (x0 + ph // 2, y0 + ph // 2), ph // 3, (110, 110, 110), -1)
|
||||
if text:
|
||||
cv2.putText(img, text, (x0 + ph, y0 + ph // 2 + ph // 6), cv2.FONT_HERSHEY_SIMPLEX, ph / 90.0, (45, 45, 45), 1)
|
||||
return img
|
||||
|
||||
|
||||
class TestLocate:
|
||||
def test_box_anchored_top_right(self):
|
||||
eng = MicrosoftEngine()
|
||||
loc = eng.locate(np.zeros((1024, 1024, 3), np.uint8))
|
||||
assert loc.x + loc.w == pytest.approx(1024 - int(0.004 * 1024), abs=2)
|
||||
assert loc.y == pytest.approx(int(0.003 * 1024), abs=2)
|
||||
|
||||
def test_box_scales_with_long_side_not_width(self):
|
||||
# Measured: the pill tracks the render dimension, so a 1024x1536 portrait
|
||||
# carries the SAME pill size as 1536x1024. A width basis undersized the
|
||||
# template by the aspect ratio and dropped every portrait carrier.
|
||||
eng = MicrosoftEngine()
|
||||
portrait = eng.locate(np.zeros((1536, 1024, 3), np.uint8))
|
||||
landscape = eng.locate(np.zeros((1024, 1536, 3), np.uint8))
|
||||
assert portrait.w == landscape.w
|
||||
small = eng.locate(np.zeros((720, 480, 3), np.uint8))
|
||||
assert small.w < portrait.w
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_provenance_relaxation_is_the_measured_07(self):
|
||||
# The relaxed band was measured on the OCR-censused MS cohort: 257
|
||||
# badge-less files max 0.251, so the 0.266 relaxed gate admits the three
|
||||
# faint badges in [0.251, 0.38) with zero measured false fills. Do not
|
||||
# move the factor without re-censusing the badge-less cohort.
|
||||
assert MicrosoftEngine().config.provenance_ncc_factor == 0.7
|
||||
|
||||
def test_long_scale_basis(self):
|
||||
assert MicrosoftEngine().config.scale_basis == "long"
|
||||
|
||||
def test_threshold_and_geometry_pins(self):
|
||||
from remove_ai_watermarks.microsoft_engine import (
|
||||
DETECT_NCC_THRESHOLD,
|
||||
MARGIN_RIGHT_FRAC,
|
||||
WM_WIDTH_FRAC,
|
||||
)
|
||||
|
||||
assert pytest.approx(0.38) == DETECT_NCC_THRESHOLD # controls max 0.293; carriers max 0.579
|
||||
assert pytest.approx(0.170) == WM_WIDTH_FRAC
|
||||
assert pytest.approx(0.004) == MARGIN_RIGHT_FRAC
|
||||
|
||||
def test_registry_row(self):
|
||||
mark = registry.get_mark("microsoft")
|
||||
assert mark.location == "top-right"
|
||||
assert mark.label == "Microsoft top-right AI badge"
|
||||
assert mark.in_auto
|
||||
assert mark.provenance_platform_tokens == ("microsoft",)
|
||||
assert mark.label_regime is None # not a China-TC260 mark
|
||||
|
||||
|
||||
class TestDetect:
|
||||
@pytest.mark.parametrize(("w", "h"), [(1024, 1024), (1536, 1024), (1024, 1536), (720, 480), (1206, 1194)])
|
||||
def test_composites_detected_across_sizes(self, w, h):
|
||||
eng = MicrosoftEngine()
|
||||
img, _box = _compose(w, h)
|
||||
det = eng.detect(img)
|
||||
assert det.detected, f"{w}x{h}: conf={det.confidence:.3f}"
|
||||
assert det.confidence >= 0.38
|
||||
|
||||
def test_portrait_composite_region_covers_pill(self):
|
||||
eng = MicrosoftEngine()
|
||||
img, (x, y, pw, ph) = _compose(1024, 1536)
|
||||
det = eng.detect(img)
|
||||
assert det.detected
|
||||
rx, ry, rw, _rh = det.region
|
||||
assert abs((rx + rw) - (x + pw)) < 0.08 * pw
|
||||
assert abs(ry - y) < 0.4 * ph
|
||||
|
||||
def test_clean_gradient_not_detected(self):
|
||||
eng = MicrosoftEngine()
|
||||
ramp = np.tile(np.linspace(0, 255, 1024, dtype=np.uint8), (1024, 1))
|
||||
img = cv2.cvtColor(ramp, cv2.COLOR_GRAY2BGR)
|
||||
assert not eng.detect(img).detected
|
||||
|
||||
def test_plain_white_pill_not_detected(self):
|
||||
# The expected internal holes are the discriminator: any other bright rounded
|
||||
# element in the corner must not attribute Microsoft.
|
||||
eng = MicrosoftEngine()
|
||||
assert not eng.detect(_plain_pill(1024, 1024)).detected
|
||||
|
||||
def test_foreign_text_pill_not_detected(self):
|
||||
eng = MicrosoftEngine()
|
||||
assert not eng.detect(_plain_pill(1024, 1024, text="Sample Text")).detected
|
||||
|
||||
def test_busy_content_corner_not_detected(self):
|
||||
# A photo-like textured corner must stay under the gate.
|
||||
eng = MicrosoftEngine()
|
||||
rng = np.random.default_rng(7)
|
||||
img = rng.integers(0, 255, (1024, 1024, 3), dtype=np.uint8)
|
||||
img = cv2.GaussianBlur(img, (0, 0), 3)
|
||||
assert not eng.detect(img).detected
|
||||
|
||||
|
||||
class TestMask:
|
||||
def test_footprint_covers_the_pill(self):
|
||||
eng = MicrosoftEngine()
|
||||
img, (x, y, pw, ph) = _compose(1536, 1024)
|
||||
det = eng.detect(img)
|
||||
assert det.detected
|
||||
mask = eng.footprint_mask(img, detection=det)
|
||||
assert mask.shape[:2] == img.shape[:2]
|
||||
ys, xs = np.where(mask > 0)
|
||||
assert xs.min() >= x - 0.15 * pw
|
||||
assert xs.max() <= x + pw + 0.15 * pw
|
||||
assert ys.min() >= y - 0.3 * ph
|
||||
assert ys.max() <= y + ph + 0.3 * ph
|
||||
# the fill must cover the pill area, not just the text glyphs
|
||||
assert float(mask[y : y + ph, x : x + pw].mean()) > 0.4
|
||||
+52
-2
@@ -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
|
||||
@@ -235,7 +285,7 @@ class TestResolveStrength:
|
||||
assert SDXL_ZIMAGE_UNKNOWN_STRENGTH == SDXL_ZIMAGE_GEMINI_STRENGTH
|
||||
assert resolve_strength(None, "openai", "sdxl-zimage") == SDXL_ZIMAGE_OPENAI_STRENGTH
|
||||
assert resolve_strength(None, "google", "sdxl-zimage") == SDXL_ZIMAGE_GEMINI_STRENGTH
|
||||
# An unrecognised issuer takes the stricter Gemini value, not the OpenAI one.
|
||||
# An unrecognized issuer takes the stricter Gemini value, not the OpenAI one.
|
||||
assert resolve_strength(None, "adobe", "sdxl-zimage") == SDXL_ZIMAGE_UNKNOWN_STRENGTH
|
||||
assert resolve_strength(None, None, "sdxl-zimage") == SDXL_ZIMAGE_UNKNOWN_STRENGTH
|
||||
|
||||
|
||||
@@ -659,8 +659,7 @@ def test_no_face_path_still_runs_verified_text_restoration(monkeypatch):
|
||||
restore.assert_called_with(source, anchor, donor, manifest.lines)
|
||||
|
||||
|
||||
def test_tiled_verified_text_runs_vae_donor_per_tile(monkeypatch):
|
||||
from remove_ai_watermarks._internal import qwen_zimage_pipeline, text_restoration
|
||||
def test_tiled_verified_text_is_rejected_before_model_work():
|
||||
from remove_ai_watermarks._internal.qwen_zimage_pipeline import QwenZImagePipeline
|
||||
from remove_ai_watermarks._internal.text_restoration import VerifiedTextLine, VerifiedTextManifest
|
||||
|
||||
@@ -668,12 +667,6 @@ def test_tiled_verified_text_runs_vae_donor_per_tile(monkeypatch):
|
||||
pipeline.device = "cuda"
|
||||
pipeline.progress_callback = None
|
||||
source = Image.new("RGB", (96, 80), (10, 20, 30))
|
||||
restored = Image.new("RGB", (96, 80), (130, 140, 150))
|
||||
pipeline._qwen_vae_roundtrip = MagicMock(side_effect=lambda tile: tile)
|
||||
pipeline._run_global = MagicMock(side_effect=lambda tile, _strength, _seed: tile)
|
||||
monkeypatch.setattr(qwen_zimage_pipeline, "detect_faces", lambda _image: [])
|
||||
restore = MagicMock(return_value=restored)
|
||||
monkeypatch.setattr(text_restoration, "restore_verified_text", restore)
|
||||
manifest = VerifiedTextManifest(
|
||||
"0" * 64,
|
||||
96,
|
||||
@@ -681,23 +674,16 @@ def test_tiled_verified_text_runs_vae_donor_per_tile(monkeypatch):
|
||||
(VerifiedTextLine((4, 4, 20, 16), "Exact", "alphabetic"),),
|
||||
)
|
||||
|
||||
result = pipeline.run(
|
||||
source,
|
||||
strength=0.1,
|
||||
seed=0,
|
||||
tile=True,
|
||||
tile_size=64,
|
||||
tile_overlap=16,
|
||||
text_manifest=manifest,
|
||||
)
|
||||
|
||||
assert result is restored
|
||||
assert pipeline._qwen_vae_roundtrip.call_count > 1
|
||||
assert pipeline._run_global.call_count > 1
|
||||
restore.assert_called_once()
|
||||
assert restore.call_args.args[0].size == (96, 80)
|
||||
assert restore.call_args.args[1].size == (96, 80)
|
||||
assert restore.call_args.args[2].size == (96, 80)
|
||||
with pytest.raises(ValueError, match="not calibrated with tiled diffusion"):
|
||||
pipeline.run(
|
||||
source,
|
||||
strength=0.1,
|
||||
seed=0,
|
||||
tile=True,
|
||||
tile_size=64,
|
||||
tile_overlap=16,
|
||||
text_manifest=manifest,
|
||||
)
|
||||
|
||||
|
||||
def test_watermark_remover_dispatches_to_full_pipeline(tmp_path, monkeypatch):
|
||||
@@ -757,6 +743,29 @@ def test_watermark_remover_dispatches_qwen_tiling_to_full_pipeline(tmp_path, mon
|
||||
assert output.exists()
|
||||
|
||||
|
||||
def test_watermark_remover_rejects_tiled_verified_text_before_pipeline(tmp_path, monkeypatch):
|
||||
from remove_ai_watermarks._internal.text_restoration import VerifiedTextLine, VerifiedTextManifest
|
||||
from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
|
||||
|
||||
_mock_watermark_runtime_deps(monkeypatch)
|
||||
source = tmp_path / "source.png"
|
||||
Image.new("RGB", (96, 80), (20, 30, 40)).save(source)
|
||||
manifest = VerifiedTextManifest(
|
||||
"0" * 64,
|
||||
96,
|
||||
80,
|
||||
(VerifiedTextLine((4, 4, 20, 16), "Exact", "alphabetic"),),
|
||||
)
|
||||
remover = WatermarkRemover(device="cuda", pipeline="qwen-zimage")
|
||||
runtime = MagicMock()
|
||||
monkeypatch.setattr(remover, "_load_qwen_zimage_pipeline", lambda: runtime)
|
||||
|
||||
with pytest.raises(ValueError, match="not calibrated with tiled diffusion"):
|
||||
remover.remove_watermark(source, text_manifest=manifest, tile=True)
|
||||
|
||||
runtime.run.assert_not_called()
|
||||
|
||||
|
||||
def test_qwen_tiling_runs_global_tiles_then_one_full_frame_face_stage(monkeypatch):
|
||||
from remove_ai_watermarks._internal.qwen_zimage_pipeline import (
|
||||
QwenZImagePipeline,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Contracts for the registered visible-mark calibration harness."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts" / "registered_mark_calibrate.py"
|
||||
SPEC = importlib.util.spec_from_file_location("registered_mark_calibrate", SCRIPT)
|
||||
assert SPEC
|
||||
assert SPEC.loader
|
||||
module = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(module)
|
||||
|
||||
|
||||
def test_manifest_preserves_evidence_arms(tmp_path: Path) -> None:
|
||||
manifest = tmp_path / "manifest.jsonl"
|
||||
manifest.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
json.dumps({"path": "carrier.png", "arm": "positive"}),
|
||||
json.dumps({"path": "provider.png", "arm": "metadata"}),
|
||||
json.dumps({"path": "comparison.png", "arm": "control"}),
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
rows = module.load_manifest(manifest)
|
||||
|
||||
assert [row["arm"] for row in rows] == ["positive", "metadata", "control"]
|
||||
assert all(Path(row["path"]).is_absolute() for row in rows)
|
||||
|
||||
|
||||
def test_manifest_rejects_one_file_in_multiple_arms(tmp_path: Path) -> None:
|
||||
manifest = tmp_path / "manifest.jsonl"
|
||||
manifest.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
json.dumps({"path": "same.png", "arm": "positive"}),
|
||||
json.dumps({"path": "same.png", "arm": "control"}),
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="duplicate image path"):
|
||||
module.load_manifest(manifest)
|
||||
|
||||
|
||||
def test_summary_never_relabels_a_control() -> None:
|
||||
summary = module.summarize([0.1, 0.4], unreadable=1, fires=1)
|
||||
|
||||
assert summary["n"] == 2
|
||||
assert summary["unreadable"] == 1
|
||||
assert summary["fires"] == 1
|
||||
@@ -71,7 +71,7 @@ class TestConfig:
|
||||
assert RunningHubEngine().config.provenance_ncc_factor == 1.0
|
||||
|
||||
def test_gate_above_clean_arm_max(self):
|
||||
# Clean arm scored p99 0.273 / max 0.295 on 286 hand-labelled frames.
|
||||
# Clean arm scored p99 0.273 / max 0.295 on 286 hand-labeled frames.
|
||||
assert RunningHubEngine().config.detect_ncc_threshold > 0.295
|
||||
|
||||
def test_registry_row(self):
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Basic command-line contracts for standalone maintainer scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"script",
|
||||
[
|
||||
"visible_groundtruth.py",
|
||||
"visible_recall_sample.py",
|
||||
"visible_sheets.py",
|
||||
"registered_mark_calibrate.py",
|
||||
],
|
||||
)
|
||||
def test_script_help_exits_cleanly(script: str) -> None:
|
||||
result = subprocess.run( # noqa: S603 -- fixed interpreter and repository-owned script path
|
||||
[sys.executable, str(ROOT / "scripts" / script), "--help"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "usage:" in result.stdout
|
||||
|
||||
|
||||
def test_visible_groundtruth_help_is_cp1252_safe() -> None:
|
||||
env = environ.copy()
|
||||
env["PYTHONIOENCODING"] = "cp1252"
|
||||
result = subprocess.run( # noqa: S603 -- fixed interpreter and repository-owned script path
|
||||
[sys.executable, str(ROOT / "scripts" / "visible_groundtruth.py"), "--help"],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "usage:" in result.stdout
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Policy-level tests for the shared text-mark engine config.
|
||||
|
||||
These assert calibrated TUNING, not algorithm behaviour --
|
||||
These assert calibrated TUNING, not algorithm behavior --
|
||||
they exist so a future edit cannot silently revert a calibrated constant back to a
|
||||
value that was measured to be wrong. The measurements themselves live in
|
||||
`docs/module-internals.md` and in the comment at
|
||||
@@ -74,7 +74,7 @@ class TestScaleBasis:
|
||||
assert doubao_engine._CONFIG.scale_basis == "short"
|
||||
|
||||
def test_jimeng_scales_with_width(self):
|
||||
"""Measured, not an oversight: the short-side basis took jimeng's labelled
|
||||
"""Measured, not an oversight: the short-side basis took jimeng's labeled
|
||||
landscape positives from 13/13 to 0/13."""
|
||||
from remove_ai_watermarks import jimeng_engine
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestFaintMaskStaysTight:
|
||||
loc = eng.locate(img)
|
||||
roi = loc.w * loc.h
|
||||
# The mark's own glyph box is ~40% of the corner ROI and the mask pads it, so a
|
||||
# correct mask lands near 60%. The pre-fix behaviour measured 120.9% (the whole
|
||||
# correct mask lands near 60%. The pre-fix behavior measured 120.9% (the whole
|
||||
# ROI plus padding), which this bound excludes.
|
||||
assert area < 0.85 * roi, f"mask covers {100 * area / roi:.0f}% of the corner box"
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class TestRunTiled:
|
||||
def test_identity_generate_reconstructs_image(self):
|
||||
# A blend of identical (unchanged) tiles must reproduce the input exactly,
|
||||
# regardless of overlap -- the feather weights are a partition-of-unity once
|
||||
# normalised. This is the seam-free guarantee.
|
||||
# normalized. This is the seam-free guarantee.
|
||||
rng = np.random.default_rng(0)
|
||||
arr = rng.integers(0, 256, size=(1500, 1300, 3), dtype=np.uint8)
|
||||
image = Image.fromarray(arr)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
TrustMark is an optional dependency (extra ``trustmark``) that downloads model
|
||||
weights on first use, so the decode path is only exercised when it is installed
|
||||
(mirrors the imwatermark handling). The always-on test pins the graceful
|
||||
absent/error behaviour: detect must return None, never raise.
|
||||
absent/error behavior: detect must return None, never raise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
+7
-1
@@ -860,6 +860,12 @@ class TestVideoMetadataApi:
|
||||
|
||||
|
||||
class TestVideoProvenanceApi:
|
||||
def test_c2pa_platform_keeps_the_bytedance_surface_name(self):
|
||||
from remove_ai_watermarks.video import _platform_from_video_metadata
|
||||
|
||||
assert _platform_from_video_metadata({"issuer": "BytePlus (ByteDance)"}) == "BytePlus (ByteDance)"
|
||||
assert _platform_from_video_metadata({"issuer": "ByteDance (Volcano Engine)"}) == "ByteDance Volcano Engine"
|
||||
|
||||
def test_identifies_metadata_without_pixel_scan(self, tmp_path: Path):
|
||||
from remove_ai_watermarks.video import identify_video
|
||||
|
||||
@@ -870,7 +876,7 @@ class TestVideoProvenanceApi:
|
||||
assert report.source == source
|
||||
assert report.is_ai_generated is True
|
||||
assert report.confidence == "high"
|
||||
assert report.platform == "OpenAI (ChatGPT / gpt-image / DALL-E / Sora)"
|
||||
assert report.platform == "OpenAI (ChatGPT / GPT Image / DALL·E / Sora)"
|
||||
assert report.visible_mark is None
|
||||
assert report.total_frames is None
|
||||
assert report.has_ai_metadata is True
|
||||
|
||||
@@ -142,7 +142,7 @@ def test_pairing_follows_the_engine_sampling_rule_not_just_the_frame_count(
|
||||
in the suite that constrains the sampler's phase at all.
|
||||
|
||||
The clips encode losslessly, so the aligned pairing is exact and clears any
|
||||
floor; the misaligned one compares frames a full colour step apart and lands
|
||||
floor; the misaligned one compares frames a full color step apart and lands
|
||||
near 9 dB, which is what leaves the 25 dB ceiling a wide moat rather than a
|
||||
tuned threshold.
|
||||
"""
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks import optional_deps, video_encoding, video_invisible
|
||||
@@ -92,6 +94,50 @@ def test_regeneration_rejects_noise_outside_unit_interval(tmp_path: Path) -> Non
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("kwargs", "message"),
|
||||
[
|
||||
({"fps": 0.0}, "fps must be at least 1"),
|
||||
({"batch_size": 0}, "batch_size must be at least 1"),
|
||||
({"duration": 0.0}, "duration must be positive"),
|
||||
({"device": "tpu"}, "device must be auto, cuda, mps, or cpu"),
|
||||
],
|
||||
)
|
||||
def test_regeneration_rejects_invalid_controls_before_probing(
|
||||
tmp_path: Path,
|
||||
kwargs: dict[str, Any],
|
||||
message: str,
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match=message):
|
||||
video_invisible.regenerate_video_candidate(
|
||||
tmp_path / "source.mp4",
|
||||
tmp_path / "candidate.mp4",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def test_fit_size_and_paired_psnr_validate_geometry() -> None:
|
||||
assert video_invisible._fit_size(1920, 1080, 512) == (512, 288)
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
video_invisible._fit_size(0, 1080, 512)
|
||||
with pytest.raises(ValueError, match="at least"):
|
||||
video_invisible._fit_size(1920, 1080, 4)
|
||||
|
||||
frame = np.zeros((2, 2, 3), dtype=np.uint8)
|
||||
assert video_invisible.paired_psnr(frame, frame) == math.inf
|
||||
with pytest.raises(ValueError, match="matching shapes"):
|
||||
video_invisible.paired_psnr(frame, frame[:1])
|
||||
|
||||
|
||||
def test_load_runtime_rejects_invalid_device_and_missing_extra(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with pytest.raises(ValueError, match="device must be"):
|
||||
video_invisible.load_video_vae_runtime(device="tpu")
|
||||
|
||||
monkeypatch.setattr(video_invisible, "is_available", lambda: False)
|
||||
with pytest.raises(RuntimeError, match="diffusion extra"):
|
||||
video_invisible.load_video_vae_runtime()
|
||||
|
||||
|
||||
def test_encoder_and_mux_commands_separate_streaming_frames_from_source_audio(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -298,8 +344,9 @@ def test_shipped_defaults_match_a_certified_manifest_row() -> None:
|
||||
rate now fails here until a ``not_detected`` row exists for that exact triple.
|
||||
|
||||
The tuple stops at three fields because the model is a fourth thing the oracle
|
||||
was shown and neither tracked row records it. That omission is data-driven: add
|
||||
``vae`` here in the same commit as the first row that records one.
|
||||
was shown and neither historical row recorded it. The manifest says
|
||||
``unrecorded`` rather than leaving an ambiguous empty field. Add ``vae`` here in
|
||||
the same commit as the first row that records one.
|
||||
"""
|
||||
with ORACLE_MANIFEST.open(newline="", encoding="utf-8") as stream:
|
||||
certified = {
|
||||
@@ -320,6 +367,15 @@ def test_shipped_defaults_match_a_certified_manifest_row() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_oracle_manifest_marks_missing_vae_identity_explicitly() -> None:
|
||||
with ORACLE_MANIFEST.open(newline="", encoding="utf-8") as stream:
|
||||
rows = list(csv.DictReader(stream))
|
||||
|
||||
assert rows
|
||||
assert all(row["vae"] for row in rows)
|
||||
assert all(row["vae"] == "unrecorded" or "/" in row["vae"] for row in rows)
|
||||
|
||||
|
||||
def test_stream_batches_consumes_only_one_batch_ahead() -> None:
|
||||
consumed: list[int] = []
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""The visible-mark example gallery is complete and self-consistent.
|
||||
|
||||
Two failures this suite exists to catch:
|
||||
* a mark registered without a committed example (the gallery lags the registry);
|
||||
* an engine that no longer detects its own canonical example (the gallery is
|
||||
generated from the engines' measured geometry, so this is a regression tripwire).
|
||||
|
||||
The examples are SYNTHETIC (``scripts/render_visible_examples.py`` composites the
|
||||
committed silhouettes onto a generated base). User uploads never enter the repo.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks import watermark_registry as wr
|
||||
from remove_ai_watermarks.image_io import imread
|
||||
from remove_ai_watermarks.video import VIDEO_VISIBLE_MARKS, identify_video
|
||||
|
||||
_ROOT = Path(__file__).resolve().parents[1]
|
||||
_GALLERY = _ROOT / "data" / "fixtures" / "visible"
|
||||
|
||||
_IMAGE_KEYS = [m.key for m in wr.known_marks()]
|
||||
|
||||
|
||||
class TestGallery:
|
||||
def test_every_registered_mark_has_an_example(self) -> None:
|
||||
missing = [key for key in _IMAGE_KEYS if not (_GALLERY / key / "example.png").is_file()]
|
||||
assert missing == [], f"registered without an example: {missing}; run scripts/render_visible_examples.py"
|
||||
|
||||
@pytest.mark.parametrize("key", _IMAGE_KEYS)
|
||||
def test_engine_detects_its_own_example(self, key: str) -> None:
|
||||
img = imread(str(_GALLERY / key / "example.png"))
|
||||
assert img is not None, key
|
||||
det = wr.get_mark(key).detect(img, provenance=False)
|
||||
assert det.detected, f"{key}: confidence {det.confidence:.3f} on its own example"
|
||||
|
||||
def test_gallery_has_no_stray_directories(self) -> None:
|
||||
known = set(_IMAGE_KEYS) | set(VIDEO_VISIBLE_MARKS) | {"README.md"}
|
||||
extra = sorted(p.name for p in _GALLERY.iterdir() if p.name not in known)
|
||||
assert extra == [], f"gallery holds unregistered examples: {extra}; remove or register them"
|
||||
|
||||
|
||||
class TestVideoGallery:
|
||||
def test_every_registered_video_mark_has_an_example(self) -> None:
|
||||
missing = [key for key in VIDEO_VISIBLE_MARKS if not (_GALLERY / key / "example.mp4").is_file()]
|
||||
assert missing == [], f"video mark without an example: {missing}; run scripts/render_visible_examples.py"
|
||||
|
||||
def test_selection_accepts_each_example(self) -> None:
|
||||
# The shipped temporal selection (not just the per-frame detector) must
|
||||
# accept the clip: table order resolves cross-template ties, so the example
|
||||
# must carry the discriminative variant of its mark.
|
||||
for key in VIDEO_VISIBLE_MARKS:
|
||||
rep = identify_video(_GALLERY / key / "example.mp4", check_visible=True)
|
||||
assert rep.visible_mark == key, f"{key}: selection returned {rep.visible_mark!r}"
|
||||
@@ -25,6 +25,7 @@ class TestCatalog:
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"microsoft",
|
||||
"jimeng_pill",
|
||||
]
|
||||
|
||||
@@ -113,6 +114,7 @@ class TestScan:
|
||||
"runninghub",
|
||||
"baidu",
|
||||
"liblib",
|
||||
"microsoft",
|
||||
"jimeng_pill",
|
||||
}
|
||||
|
||||
@@ -219,7 +221,7 @@ class TestProvenanceGate:
|
||||
# 0.38 is inside the measured 13%-precision band and above the engine's own
|
||||
# 0.35 floor, so the engine reports `detected` and only the registry gate can
|
||||
# reject it. Hardcoded on purpose: if the gate is ever lowered back under this
|
||||
# value, this test must fail on the BEHAVIOUR below, not on its own arithmetic.
|
||||
# value, this test must fail on the behavior below, not on its own arithmetic.
|
||||
self._stub(monkeypatch, 0.38)
|
||||
img = np.zeros((256, 256, 3), np.uint8)
|
||||
assert reg.get_mark("gemini").detect(img).detected is False
|
||||
@@ -529,7 +531,7 @@ class TestMarkKnowledgeIsOnTheRow:
|
||||
|
||||
Product family, label regime, the platform sentence and the metadata signals that
|
||||
confirm the vendor all used to live in separate hand-maintained tables across
|
||||
``watermark_registry``, ``identify`` and ``api``. That is how LibLibAI ended up
|
||||
``watermark_registry``, ``identify`` and ``api``. That is how LiblibAI ended up
|
||||
registered but absent from the pill veto.
|
||||
"""
|
||||
|
||||
@@ -555,15 +557,18 @@ class TestMarkKnowledgeIsOnTheRow:
|
||||
if mark.label_regime == "tc260" and mark.key != "jimeng_pill":
|
||||
assert "aigc" in mark.provenance_signals, mark.key
|
||||
|
||||
def test_only_gemini_claims_platform_tokens(self):
|
||||
def test_platform_token_marks_are_the_c2pa_attributed_ones(self):
|
||||
# Gemini (Google C2PA) and Microsoft (issuer "Microsoft") are the marks whose
|
||||
# vendor a C2PA platform string can confirm; every other mark reaches its
|
||||
# provenance through TC260 codes or product signals instead.
|
||||
by_token = {m.key for m in reg.known_marks() if m.provenance_platform_tokens}
|
||||
assert by_token == {"gemini"}
|
||||
assert by_token == {"gemini", "microsoft"}
|
||||
|
||||
|
||||
class TestPillSuppressors:
|
||||
"""The pill veto is derived from the registry, not hand-listed.
|
||||
|
||||
The hand-written list drifted: LibLibAI was registered in the same commit as
|
||||
The hand-written list drifted: LiblibAI was registered in the same commit as
|
||||
RunningHub and Baidu, both of which were added to the veto, and it was not. A
|
||||
derived set cannot be forgotten by the next registration.
|
||||
"""
|
||||
@@ -581,7 +586,7 @@ class TestPillSuppressors:
|
||||
assert not reg._keep_pill({"liblib"}, provenance=frozenset({"jimeng"}), footprint_flat=1.0)
|
||||
|
||||
def test_pill_dropped_on_liblib_even_with_the_jimeng_wordmark(self):
|
||||
"""The veto precedes the wordmark arm, so a co-firing LibLibAI wins.
|
||||
"""The veto precedes the wordmark arm, so a co-firing LiblibAI wins.
|
||||
|
||||
This is the broader half of the change: it needs neither TC260 provenance nor
|
||||
a flat footprint, so it is reachable on more inputs than the metadata arm.
|
||||
@@ -589,7 +594,7 @@ class TestPillSuppressors:
|
||||
assert not reg._keep_pill({"liblib", "jimeng"}, provenance=frozenset(), footprint_flat=1.0)
|
||||
|
||||
def test_pill_survives_gemini_and_samsung(self):
|
||||
"""Neither is a TC260 labeller, and neither can put "jimeng" into provenance,
|
||||
"""Neither is a TC260 labeler, and neither can put "jimeng" into provenance,
|
||||
so neither may veto the arm it could not have enabled."""
|
||||
assert reg._keep_pill({"gemini", "jimeng"}, provenance=frozenset(), footprint_flat=1.0)
|
||||
assert reg._keep_pill({"samsung", "jimeng"}, provenance=frozenset(), footprint_flat=1.0)
|
||||
@@ -632,4 +637,4 @@ class TestProvenanceMaskThreading:
|
||||
)
|
||||
monkeypatch.setattr(eng, "footprint_mask", lambda image, *, force=False, region=None, dilate=None: None)
|
||||
_, removed = reg.remove_auto_marks(np.zeros((256, 256, 3), np.uint8), sensitivity="strict", backend="cv2")
|
||||
assert "Google Gemini sparkle" not in removed
|
||||
assert "Google Gemini visible watermark (sparkle)" not in removed
|
||||
|
||||
Reference in New Issue
Block a user