Restructure documentation, validate metadata removal, consolidate assets

This commit is contained in:
Victor Kuznetsov
2026-07-25 21:08:04 -07:00
parent 214c9bb3e7
commit 03cd00f132
172 changed files with 2356 additions and 2863 deletions
+10 -11
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import cv2
import numpy as np
@@ -10,21 +10,20 @@ import pytest
from PIL import Image
from PIL.PngImagePlugin import PngInfo
CORPUS_NEG_DIR = Path(__file__).resolve().parent.parent / "data" / "synthid_corpus" / "images" / "neg"
if TYPE_CHECKING:
from pathlib import Path
@pytest.fixture
def clean_photo() -> Path:
"""A verified-negative real photo from the corpus neg/ set.
def clean_photo(tmp_path: Path) -> Path:
"""Create a deterministic image with no provenance metadata.
Used by the "non-AI image" assertions (no SynthID, verdict unknown). These
are real photos with no AI provenance, the ground truth for "must not false-
positive". Skips if the corpus is not checked out.
The assertions using this fixture exercise metadata and provenance behavior,
so a generated fixture is sufficient and avoids committing personal photos.
"""
files = sorted(CORPUS_NEG_DIR.glob("*")) if CORPUS_NEG_DIR.exists() else []
if not files:
pytest.skip("no corpus neg/ images present")
return files[0]
path = tmp_path / "clean-control.png"
Image.new("RGB", (128, 96), color=(90, 140, 190)).save(path)
return path
@pytest.fixture
+1 -1
View File
@@ -11,7 +11,7 @@ import pytest
import remove_ai_watermarks as raiw
from remove_ai_watermarks import api
SAMPLES = Path(__file__).resolve().parents[1] / "data" / "samples"
SAMPLES = Path(__file__).resolve().parents[1] / "data" / "fixtures" / "provenance"
DOUBAO = SAMPLES / "doubao-1.png"
CHATGPT = SAMPLES / "chatgpt-1.png"
+4 -8
View File
@@ -75,17 +75,13 @@ class TestConfig:
assert BaiduEngine().config.provenance_ncc_factor == 1.0
def test_gate_above_clean_arm_max(self):
# Clean arm scored p99 0.314 / max 0.352 on 278 hand-labelled frames;
# the 741-frame eval set surfaced cross-fires up to 0.426 (a 抖音
# AI创作 mark no rival can suppress), and the full-corpus sweep put the
# outside-cohort false arm at 0.47 max vs true carriers at 0.50-0.66,
# so the gate sits at 0.48.
# Compatibility testing separated true Baidu marks from unrelated
# bottom-right text, so the gate sits at 0.48.
assert BaiduEngine().config.detect_ncc_threshold >= 0.48
def test_qwen_is_a_rival(self):
# 百度 vs 千问 are near-identical after binarization: 12 of 14 full-corpus
# cross-fires at the 0.37 gate were Qwen marks (Qwen's template beats
# Baidu's there by 0.17-0.35, so the margin suppresses them).
# 百度 and 千问 are near-identical after binarization, so Qwen's template
# must act as a rival.
assert "qwen_alpha.png" in BaiduEngine().config.rivals
def test_registry_row(self):
+27 -4
View File
@@ -545,6 +545,29 @@ class TestAllCommand:
assert "remove-ai-watermarks[gpu]" in result.output
assert output.exists() # visible + metadata still produced a file
def test_all_reports_metadata_that_survived_stripping(self, runner, sample_png, tmp_path):
"""The full pipeline must verify the metadata result before reporting success.
``remove_ai_metadata`` is deliberately fail-safe and may copy an undecodable
input through unchanged. Calling it directly let ``all`` print a successful
strip even when a marker survived.
"""
output = tmp_path / "clean.png"
with (
patch("remove_ai_watermarks.invisible_engine.is_available", return_value=True),
patch("remove_ai_watermarks.cli._should_skip_invisible_scrub", return_value=True),
patch(
"remove_ai_watermarks.metadata.strip_and_verify",
return_value=(tmp_path / "intermediate.png", {"c2pa": True}),
),
):
result = runner.invoke(main, ["all", str(sample_png), "-o", str(output)])
assert result.exit_code != 0
assert "metadata" in result.output.lower()
assert "survived" in result.output.lower()
assert "AI metadata stripped" not in result.output
def test_all_preserves_rgba_across_invisible_step(self, runner, tmp_path):
"""Regression: ``all`` must keep transparency even when the invisible
step writes a 3-channel result (as the real diffusion engine does).
@@ -610,8 +633,8 @@ class TestMetadataCommand:
That is correct (never crash a worker on a partial upload) but the command used
to print "AI metadata stripped ->" and exit 0 for it, so a caller could not tell
a real strip from a no-op and the output still read as AI. Found on real Samsung
Galaxy S22 C2PA PNGs during the corpus parity audit, 2026-07-19.
a real strip from a no-op and the output still read as AI. A Samsung C2PA
compatibility case exposed the defect.
"""
# PNG signature + a C2PA (caBX) chunk, then garbage: the byte scanner sees the
# marker, PIL cannot decode it.
@@ -672,7 +695,7 @@ class TestIdentifyCommand:
'AI-generated (fully synthetic)' at the CLI (the ai_source_kind branch)."""
from pathlib import Path
sample = Path(__file__).resolve().parent.parent / "data" / "samples" / "chatgpt-1.png"
sample = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance" / "chatgpt-1.png"
if not sample.exists():
pytest.skip("chatgpt sample not present")
result = runner.invoke(main, ["identify", str(sample), "--no-visible"])
@@ -1009,7 +1032,7 @@ def test_visible_backend_runtime_error_exits_cleanly(runner, tmp_path, monkeypat
from remove_ai_watermarks import region_eraser
doubao = Path(__file__).resolve().parent.parent / "data" / "samples" / "doubao-1.png"
doubao = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance" / "doubao-1.png"
if not doubao.exists():
pytest.skip("doubao sample not present")
+2 -2
View File
@@ -65,8 +65,8 @@ class TestFailedWriteIsReported:
def test_batch_counts_a_failed_write_instead_of_exiting_zero(self, tmp_path, bgr, monkeypatch):
"""The worst shape of this bug: no output files AND a success exit code.
Corpus-reproduced 2026-07-20 -- `batch --mode visible` into a read-only directory
wrote ZERO files for 2 inputs and exited 0, so a wrapping service would treat an
Regression: `batch --mode visible` into a read-only directory wrote no files
and exited 0, so a wrapping service would treat an
empty output directory as a completed run. The batch loop counts per-image
exceptions, so the write must RAISE there, never `SystemExit` (which would abort
the whole run instead of failing one image).
+1 -1
View File
@@ -21,7 +21,7 @@ from remove_ai_watermarks.doubao_engine import (
load_image_bgr,
)
SAMPLE = Path(__file__).resolve().parents[1] / "data" / "samples" / "doubao-1.png"
SAMPLE = Path(__file__).resolve().parents[1] / "data" / "fixtures" / "provenance" / "doubao-1.png"
def _compose(w: int, h: int, bg: float = 100.0):
+10 -10
View File
@@ -1,7 +1,7 @@
"""Tests for the provenance identifier (identify.py).
Pure attribution logic is unit-tested directly; end-to-end verdicts assert
against the real committed C2PA / IPTC fixtures in data/samples/.
against the real committed C2PA / IPTC fixtures in data/fixtures/provenance/.
"""
from __future__ import annotations
@@ -30,7 +30,7 @@ from remove_ai_watermarks.watermark_registry import GEMINI_SPARKLE_TRUST_CONF
# Where the lazy import inside identify._visible_sparkle resolves the detector.
_SPARKLE_TARGET = "remove_ai_watermarks.gemini_engine.detect_sparkle_confidence"
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples"
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
# ── Pure attribution logic (no file IO) ─────────────────────────────
@@ -173,7 +173,7 @@ class TestIdentifyNonPng:
def test_fal_ai_attributed(self, tmp_path: Path):
# fal.ai signs as "fal - Features & Labels Inc." with a "fal-ai/<model>"
# claim generator; corpus-measured 2026-07-23 (17 files).
# claim generator.
path = self._c2pa_jpeg(tmp_path, b"fal - Features & Labels Inc. fal-ai/seedvr trainedAlgorithmicMedia")
r = identify(path, check_visible=False, check_invisible=False)
assert r.is_ai_generated is True
@@ -182,7 +182,7 @@ class TestIdentifyNonPng:
def test_bria_attributed_without_source_type(self, tmp_path: Path):
# Bria signs as "Bria Artificial Intelligence" with source type ``empty``
# (NO trainedAlgorithmicMedia) -- a pure-generator asserts_ai vendor, so
# the issuer/generator strings alone must flag AI. Corpus-found 2026-07-23.
# the issuer/generator strings alone must flag AI.
path = self._c2pa_jpeg(tmp_path, b"Bria Artificial Intelligence Bria Ai c2pa.created c2pa.edited")
r = identify(path, check_visible=False, check_invisible=False)
assert r.is_ai_generated is True
@@ -279,7 +279,7 @@ class TestIdentifySamsungGalaxy:
# ── End-to-end verdicts on real fixtures ────────────────────────────
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestIdentifyRealSamples:
def test_openai_chatgpt(self):
r = identify(SAMPLES_DIR / "chatgpt-1.png", check_visible=False)
@@ -306,7 +306,7 @@ class TestIdentifyRealSamples:
# Apple Photos Clean Up (Apple Intelligence object removal) marks the
# AI edit via photoshop:Credit next to compositeWithTrainedAlgorithmicMedia
# -- it must be attributed, not reported as a generic made-with-AI tag.
# Corpus-measured 2026-07-23 (35 files).
# This attribution must survive metadata consolidation.
p = tmp_path / "apple_cleanup.jpg"
p.write_bytes(
b'\xff\xd8\xff\xe1<x:xmpmeta photoshop:Credit="Apple Photos Clean Up" '
@@ -491,7 +491,7 @@ class TestIdentifyHuggingFaceJob:
class TestIdentifyVisibleSparkle:
"""The visible-sparkle signal gates on the corpus-tuned threshold (0.5)."""
"""The visible-sparkle signal gates on the calibrated threshold (0.5)."""
def test_above_threshold_promotes_to_medium(self, tmp_clean_png: Path):
with patch(_SPARKLE_TARGET, return_value=0.7):
@@ -537,7 +537,7 @@ _DEMO_AFTER = REPO_ROOT / "demo_banana_after.png"
@pytest.mark.skipif(not (_DEMO_BEFORE.exists() and _DEMO_AFTER.exists()), reason="demo banana pair not present")
class TestSparkleDetectRemoveAlignment:
"""Detect (identify) and remove (registry.detect_marks) must agree on the
same image -- the retained-corpus desync where identify reported a sparkle the
same image. A prior desync let identify report a sparkle that
removal arbitration declined (or vice versa). Both gate on the single shared
GEMINI_SPARKLE_TRUST_CONF, so a sparkle just over the line is taken by BOTH
and one just under is declined by BOTH. Fixtures composite the real captured
@@ -666,7 +666,7 @@ class TestIdentifyVisibleTextMarks:
# ── Caveats and serialization ───────────────────────────────────────
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestIdentifyCaveats:
def test_openai_hedge_caveat_present(self):
r = identify(SAMPLES_DIR / "chatgpt-1.png", check_visible=False)
@@ -1126,7 +1126,7 @@ class TestIntegrityClashEndToEnd:
assert payload["integrity_clashes"] == r.integrity_clashes
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
@pytest.mark.parametrize("fixture", ["chatgpt-1.png", "firefly-1.png", "doubao-1.png", "grok-1.jpg", "mj-1.png"])
class TestRealSamplesHaveNoClash:
"""Every real single-origin fixture must report zero clashes (false-positive guard)."""
+2 -4
View File
@@ -65,13 +65,11 @@ class TestConfig:
assert LibLibEngine().config.provenance_ncc_factor == 1.0
def test_gate_above_clean_arm_max(self):
# With the Arial silhouette the full-corpus false arm (latin UI text)
# tops out at 0.398 while the cohort sits at 0.43-0.59; gate 0.42.
# The Arial silhouette separates the wordmark from generic Latin UI text.
assert LibLibEngine().config.detect_ncc_threshold >= 0.42
def test_small_image_size_floor(self):
# The one full-corpus false fire with the final template was a 200x200
# icon on a 20px template; the engine refuses small images outright.
# Small generic icons can resemble the wordmark, so the engine rejects them.
eng = LibLibEngine()
assert not eng.detect(np.full((200, 200, 3), 100, np.uint8)).detected
wm, _ = _compose(200, 200)
+15 -17
View File
@@ -27,7 +27,7 @@ from remove_ai_watermarks.metadata import (
)
# Real, committed C2PA sample images used to ground the SynthID-source tests.
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples"
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
# ── Key detection ───────────────────────────────────────────────────
@@ -156,7 +156,7 @@ class TestHasAiMetadata:
path.write_bytes(b"\xff\xd8\xff\xe1" + xmp + b"\xff\xd9")
assert has_ai_metadata(path)
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
def test_jpeg_metadata_strip_is_pixel_lossless(self, tmp_path: Path):
"""A JPEG metadata strip must NOT re-encode the DCT scan: pixels stay
bit-identical, only the AI provenance APP segments are removed. Verified on real
@@ -181,8 +181,7 @@ class TestHasAiMetadata:
def test_strip_preserves_lossless_content_with_mismatched_extension(self, tmp_path: Path):
"""F1 regression: the save format is chosen by CONTENT, not the file extension.
A PNG served with a .jpg name (common on real uploads -- ~1% of the corpus is a
PNG/WebP under a .jpg extension) must be stripped losslessly as PNG, NOT
PNG content served with a .jpg name must be stripped losslessly as PNG, NOT
re-encoded into a real JPEG. The extension-driven path silently degraded it,
breaking the 'work with originals' invariant."""
import numpy as np
@@ -261,8 +260,7 @@ class TestHasAiMetadata:
"""Regression: a truncated / corrupt image must NOT crash remove_ai_metadata (a
direct library caller like a web worker would 500 on a partial upload). PIL raises
OSError decoding a truncated PNG; the strip must fail SAFE -- copy the input through
unchanged and return, mirroring strip_c2pa_boxes. Real prod corpus: ~0.2% of
uploads are truncated."""
unchanged and return, mirroring strip_c2pa_boxes."""
import cv2
import numpy as np
@@ -337,7 +335,7 @@ class TestSamsungGenai:
def test_remove_strips_post_eoi_trailer(self, tmp_path: Path):
"""Regression: Galaxy AI appends ``PhotoEditor_Re_Edit_Data`` as a trailer AFTER
the JPEG EOI, so the verbatim scan copy in the lossless strip carried it through
(survived 0/8 on the corpus). The strip now truncates a Samsung AI trailer at EOI;
in compatibility testing. The strip now truncates a Samsung AI trailer at EOI;
pixels stay bit-identical and a non-Samsung trailer is preserved."""
import cv2
import numpy as np
@@ -435,7 +433,7 @@ class TestGetAiMetadata:
assert get_ai_metadata(path) == {}
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestGetAiMetadataRealSample:
"""get_ai_metadata surfaces the consolidated C2PA fields on real images."""
@@ -480,7 +478,7 @@ def test_bare_algorithmic_media_not_flagged_ai(tmp_path: Path):
# ── SynthID-source detection (metadata proxy) ────────────────────────
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestSynthIDSource:
"""SynthID detection via the C2PA companion manifest.
@@ -728,7 +726,7 @@ class TestExifGenerator:
assert exif_generator(path) == "Midjourney"
def test_novelai_png_text_chunk_detected(self, tmp_path: Path):
# NovelAI (mined corpus) stamps its generator in PNG tEXt Software/Source/
# NovelAI stamps its generator in PNG tEXt Software/Source/
# Title chunks, not EXIF -- the PNG-text path must catch it.
from PIL.PngImagePlugin import PngInfo
@@ -741,7 +739,7 @@ class TestExifGenerator:
assert exif_generator(path) == "NovelAI"
def test_reve_software_detected(self, tmp_path: Path):
# Reve Image (mined corpus) writes EXIF Software="reve.com".
# Reve Image writes EXIF Software="reve.com".
path = _img_with_software(tmp_path, "jpg", "reve.com")
assert exif_generator(path) == "reve.com"
@@ -751,7 +749,7 @@ class TestExifGenerator:
assert exif_generator(path) is None
def test_aphrodite_make_detected(self, tmp_path: Path):
# Aphrodite AI (mined corpus) writes EXIF Make="Aphrodite AI".
# Aphrodite AI writes EXIF Make="Aphrodite AI".
exif = piexif.dump({"0th": {piexif.ImageIFD.Make: b"Aphrodite AI"}, "Exif": {}, "GPS": {}, "1st": {}})
path = tmp_path / "aphrodite.jpg"
Image.new("RGB", (64, 64)).save(path, exif=exif)
@@ -779,7 +777,7 @@ class TestExifGenerator:
def test_apple_clean_up_removal_parity(self, tmp_path: Path):
# The "Apple Photos Clean Up" credit is an AI-edit VALUE under a
# non-AI key, so removal must drop it by value too (corpus 2026-07-23).
# non-AI key, so removal must drop it by value too.
from PIL.PngImagePlugin import PngInfo
from remove_ai_watermarks.metadata import remove_ai_metadata
@@ -851,7 +849,7 @@ class TestXaiSignature:
assert xai_signature(_grok_jpeg(tmp_path)) is True
def test_real_grok_sample_detected(self):
# Real committed Grok download (data/samples/grok-1.jpg); the EXIF
# Real committed Grok download (data/fixtures/provenance/grok-1.jpg); the EXIF
# Signature + UUID-Artist pair is the only AI signal it carries.
assert xai_signature(SAMPLES_DIR / "grok-1.jpg") is True
@@ -1135,7 +1133,7 @@ class TestAIGCLabel:
as a bare ``AIGC{...}`` blob inside a JPEG APP segment (no ``"AIGC":``
key wrapper, no PNG chunk, no namespaced XMP) -- seen near the JFIF
header on real 2026-06 downloads. ``marker`` selects the APP segment
(default APP9; the real corpus also uses APP11)."""
(default APP9; APP11 is covered by a separate regression)."""
p = tmp_path / "aigc_bare.jpg"
Image.new("RGB", (32, 32)).save(p)
raw = p.read_bytes()
@@ -1168,7 +1166,7 @@ class TestAIGCLabel:
assert not has_ai_metadata(out)
def test_remove_strips_bare_aigc_in_app11(self, tmp_path: Path):
"""Regression (real corpus, 19/27 survivors): the bare ``AIGC{...}`` blob lives
"""Regression: the bare ``AIGC{...}`` blob lives
in APP11 (0xEB) on many China gens. That marker's branch in _jpeg_app_carries_ai
only checked for a C2PA/JUMBF manifest and RETURNED, so the AIGC blob slipped past
the generic check -> survived the strip. The specific checks must fall through to
@@ -1183,7 +1181,7 @@ class TestAIGCLabel:
assert not has_ai_metadata(out)
def test_remove_strips_aigc_in_png_text_chunk(self, tmp_path: Path):
"""Regression (real corpus, 2 survivors): the TC260 ``{"AIGC":{...}}`` block in a
"""Regression: the TC260 ``{"AIGC":{...}}`` block in a
STANDARD PNG text chunk (Description) -- _is_ai_key keeps that key, so removal
must also drop it on the VALUE carrying an AIGC block."""
from PIL.PngImagePlugin import PngInfo
+2 -2
View File
@@ -152,10 +152,10 @@ class TestC2PA:
assert not has_c2pa_metadata(tmp_jpeg_path)
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples"
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestC2PARealSamples:
"""Parser behavior on real committed C2PA images."""
+1 -1
View File
@@ -112,7 +112,7 @@ class TestDetectAndMask:
def test_anchor_window_rejects_off_corner_match(self):
"""The raw-gray front-end false-fires on text-like structure ANYWHERE in
the box (37/42009 outside-cohort frames in the 2026-07-22 sweep); the
the box during compatibility testing; the
anchor window is what keeps it about THIS mark. A composed mark placed
off the measured corner anchor must NOT be detected."""
eng = RunningHubEngine()
+15 -6
View File
@@ -15,9 +15,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import synthid_corpus
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples"
CORPUS_DIR = Path(__file__).resolve().parent.parent / "data" / "synthid_corpus"
QUALITY_SET = CORPUS_DIR / "quality_sets" / "full_pipeline_quality_2026-07-25.csv"
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
CORPUS_DIR = Path(__file__).resolve().parent.parent / "data" / "synthid"
QUALITY_SET = CORPUS_DIR / "full-pipeline-quality.csv"
EXPECTED_QUALITY_SOURCE_FILENAMES = {
"ChatGPT Image May 30, 2026, 10_31_08 AM.png",
@@ -47,7 +47,15 @@ def test_reusable_quality_set_has_expected_inputs_and_valid_hashes() -> None:
assert hashlib.sha256(corpus_path.read_bytes()).hexdigest() == row["sha256"]
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present")
def test_manifest_matches_canonical_originals() -> None:
rows = _manifest_rows(CORPUS_DIR)
originals = {path.name for path in (CORPUS_DIR / "originals").iterdir() if path.is_file()}
assert {row["filename"] for row in rows} == originals
assert len({row["sha256"] for row in rows}) == len(rows)
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestIngest:
def test_ingest_openai_flags_synthid_metadata(self, tmp_path: Path):
runner = CliRunner()
@@ -64,8 +72,9 @@ class TestIngest:
assert row["synthid_metadata"] == "yes"
assert int(row["width"]) > 0
assert int(row["height"]) > 0
# The copied file lands under images/pos/ with a sha-prefixed name.
assert (tmp_path / "images" / "pos" / row["filename"]).exists()
assert row["filename"] == "chatgpt-1.png"
# Every label shares one canonical originals/ directory.
assert (tmp_path / "originals" / row["filename"]).exists()
def test_ingest_firefly_not_flagged(self, tmp_path: Path):
runner = CliRunner()
+12 -22
View File
@@ -1,6 +1,6 @@
"""Policy-level tests for the shared text-mark engine config.
These assert TUNING that was set by corpus measurement, not algorithm behaviour --
These assert calibrated TUNING, not algorithm behaviour --
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
@@ -13,11 +13,9 @@ class TestRivalMargin:
Doubao "豆包AI生成" and Jimeng "★ 即梦AI" both sit bottom-right in near-white CJK
and survive binarization as similar blobs, so an absolute NCC gate cannot tell
them apart -- 33 of jimeng's 68 false additions were Doubao marks. Measured
separability scoring both templates on the SAME blob (n=40 jimeng / 75 doubao):
absolute ncc_jimeng 0.96, ncc_jimeng MINUS ncc_doubao 0.99. Corpus effect of the
margin gate: jimeng precision 38% -> 63% with genuine detections unchanged at 40
(false fires 65 -> 23).
them apart because many Jimeng false additions were Doubao marks. Calibration
showed that the relative template margin separates them without reducing genuine
Jimeng detections.
"""
def test_jimeng_competes_against_doubao(self):
@@ -54,12 +52,8 @@ class TestRivalMargin:
class TestPerMarkProvenanceRelaxation:
"""The provenance NCC relaxation is PER MARK, not one shared multiplier.
Corpus-measured 2026-07-18 on the default `auto` path (4417 unique TC260
carriers, blind hand-label, two-sided control): the single shared 0.7 ran at
76% precision on doubao but 17% on jimeng, because jimeng's relaxed silhouette
keys on "text in the bottom-right corner" rather than the wordmark -- 33 of its
68 false additions were DOUBAO marks. Full table at
`_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`.
Calibration showed that one shared relaxation factor was too permissive for
Jimeng because its relaxed silhouette confuses other bottom-right text marks.
"""
@@ -68,12 +62,10 @@ class TestScaleBasis:
Every tuned fraction was calibrated on PORTRAIT captures, where width and short
side coincide, so the basis was never exercised until landscape inputs were
measured. Corpus-measured 2026-07-18 (2572 unique TC260 carriers): doubao
detection was portrait 60% / square 41% / **landscape 0% of 435** -- a width-scaled
box is inflated by the aspect ratio on a wide image and the glyph never lands in
it. A short-side basis recovered 56% of the previously-undetected landscape set.
The same switch broke JIMENG (labelled landscape positives 13/13 -> 0/13), whose
wordmark tracks the width -- hence per-mark, not a house rule.
measured. Calibration showed that a width-scaled Doubao box is inflated by the
aspect ratio on a wide image and can miss the glyph. A short-side basis recovered
the affected landscape cases. The same switch broke Jimeng, whose wordmark tracks
the width, hence per-mark rather than a house rule.
"""
def test_doubao_scales_with_the_short_side(self):
@@ -89,8 +81,7 @@ class TestScaleBasis:
assert jimeng_engine._CONFIG.scale_basis == "width"
def test_samsung_keeps_width_because_it_is_unmeasured(self):
"""1 addition corpus-wide, so there is no evidence either way; an unmeasured
change is not an improvement."""
"""There is no calibration evidence for changing this basis."""
from remove_ai_watermarks import samsung_engine
assert samsung_engine._CONFIG.scale_basis == "width"
@@ -133,8 +124,7 @@ class TestTophatFrontend:
the gate). The `tophat` front-end never binarizes: the saturation/luma gates become
weights, and the response is max-normalized so the score is contrast-invariant.
Corpus effect on the 240-image unbiased recall sample: doubao recall 89% -> 92% at
an unchanged 99% precision.
Calibration showed improved Doubao recall without reducing precision.
"""
def test_doubao_uses_the_continuous_frontend(self):
+7 -9
View File
@@ -1,13 +1,11 @@
"""A mark the `tophat` front-end DETECTS must also be MASKABLE.
Corpus-found 2026-07-20: Doubao's detection moved to the continuous `tophat` front-end
(which does not binarize, and that is where its recall 89% -> 92% came from), but the
Doubao's detection moved to the continuous `tophat` front-end, which does not
binarize, but the
removal mask still came from the BINARIZED glyph blob. A mark faint enough to be found
only by the continuous response therefore produced an empty binary blob, `localize`
returned mask=None, and `remove()` was a silent no-op: `identify` reported
`visible_doubao` while `visible` said "no visible mark" on the same file. Measured on the
full corpus parity sweep: 57 of 60 sampled still-detected Doubao marks were untouched
no-ops, ~8% of all Doubao detections.
`visible_doubao` while `visible` said "no visible mark" on the same file.
"""
from __future__ import annotations
@@ -99,11 +97,11 @@ class TestFaintMarkIsMaskable:
class TestFaintMaskStaysTight:
"""The faint fallback must cover the mark WITHOUT filling the whole corner.
Corpus-found 2026-07-20: the first version of the fallback thresholded the continuous
The first version of the fallback thresholded the continuous
response at 0.5, but `tophat_response` returns uint8 0..255 -- so it selected every
non-zero pixel, not "half the peak" as its comment claimed. On 12 of 12 real frames
taking this path the resulting box covered 100% of the corner ROI, so removal inpainted
the entire corner instead of the glyph. Parity could not see it: parity asks whether
non-zero pixel, not "half the peak" as its comment claimed. On compatibility frames
taking this path the resulting box covered the corner ROI, so removal inpainted the
entire corner instead of the glyph. Parity could not see it: parity asks whether
the detector is clean afterwards, and a mask that fills everything passes trivially.
The cost, not the outcome, was the defect.
"""
+2 -2
View File
@@ -9,7 +9,7 @@ import pytest
from remove_ai_watermarks import watermark_registry as reg
DOUBAO_SAMPLE = Path(__file__).resolve().parents[1] / "data" / "samples" / "doubao-1.png"
DOUBAO_SAMPLE = Path(__file__).resolve().parents[1] / "data" / "fixtures" / "provenance" / "doubao-1.png"
class TestCatalog:
@@ -383,7 +383,7 @@ class TestArbiter:
def test_weak_pill_detection_does_not_confirm_the_jimeng_wordmark(self):
"""The pill is too false-fire-prone (~7%) to grant a sibling `confirmed` trust.
Corpus-measured defect (2026-07-18): a pill false fire on clean non-ByteDance
Regression: a pill false fire on clean non-ByteDance
content confirmed jimeng, relaxing its NCC gate 0.45 -> 0.3825; jimeng then
false-fired, and _keep_pill's wordmark arm removed the pill UNRESTRICTED,
skipping the flatness guard. Closed loop on the default `auto` path.