Unplug lattice detector from package, expand research docs

This commit is contained in:
Victor Kuznetsov
2026-08-23 11:13:56 -07:00
parent 2445086fa4
commit d22872f84d
34 changed files with 2210 additions and 1709 deletions
+7 -3
View File
@@ -18,12 +18,10 @@ CHATGPT = SAMPLES / "chatgpt-1.png"
class TestTopLevelExports:
def test_lazy_reexports_resolve(self):
from remove_ai_watermarks import openai_provenance, synthid_detector
from remove_ai_watermarks import openai_provenance
assert raiw.remove_visible is api.remove_visible
assert raiw.visible_provenance is api.visible_provenance
assert raiw.detect_synthid is synthid_detector.detect_synthid
assert raiw.SynthIDDetection is synthid_detector.SynthIDDetection
assert raiw.verify_openai_synthid is openai_provenance.verify_openai_synthid
assert raiw.OpenAIProvenanceError is openai_provenance.OpenAIProvenanceError
assert raiw.OpenAISynthIDDetection is openai_provenance.OpenAISynthIDDetection
@@ -32,6 +30,12 @@ class TestTopLevelExports:
with pytest.raises(AttributeError):
_ = raiw.does_not_exist
def test_local_synthid_detector_is_not_a_package_export(self):
with pytest.raises(AttributeError):
_ = raiw.detect_synthid
with pytest.raises(AttributeError):
_ = raiw.SynthIDDetection
def test_bare_import_is_light(self):
# importing the package must not pull the heavy cv2/torch stack (PEP 562 lazy).
# Checked in a FRESH interpreter -- another test in this process may already
+4 -47
View File
@@ -736,54 +736,11 @@ class TestIdentifyCommand:
assert result.exit_code != 0
class TestDetectSynthIDCommand:
def test_help(self, runner):
class TestDetectSynthIDCommandRemoved:
def test_command_is_not_registered(self, runner):
result = runner.invoke(main, ["detect-synthid", "--help"])
assert result.exit_code == 0
assert "calibrated image sizes" in result.output
assert "--register-scale" in result.output
assert "--fixed-period" in result.output
def test_unsupported_geometry_is_machine_readable(self, runner, tmp_clean_png):
result = runner.invoke(main, ["detect-synthid", str(tmp_clean_png), "--json"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["status"] == "unsupported"
assert payload["score"] is None
def test_registered_scale_mode_is_machine_readable(self, runner, tmp_clean_png):
from remove_ai_watermarks.synthid_detector import (
REGISTERED_DETECTOR_ID,
REGISTERED_THRESHOLD,
)
result = runner.invoke(
main,
["detect-synthid", str(tmp_clean_png), "--register-scale", "--json"],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["status"] == "unsupported"
assert payload["threshold"] == REGISTERED_THRESHOLD
assert payload["detector"] == REGISTERED_DETECTOR_ID
def test_non_json_output_preserves_negative_scope(self, runner, tmp_clean_png):
result = runner.invoke(main, ["detect-synthid", str(tmp_clean_png)])
assert result.exit_code == 0, result.output
assert "unsupported" in result.output
assert "not proof that SynthID is absent" in result.output
def test_registered_non_json_output_names_the_bounded_search(self, runner, tmp_clean_png):
result = runner.invoke(
main,
["detect-synthid", str(tmp_clean_png), "--register-scale"],
)
assert result.exit_code == 0, result.output
assert "Bounded spatial-scale registration was explicitly enabled" in result.output
assert result.exit_code != 0
assert "No such command" in result.output
class TestVerifyOpenAISynthIDCommand:
-34
View File
@@ -886,40 +886,6 @@ class TestIdentifyVisibleTextMarks:
# ── Caveats and serialization ───────────────────────────────────────
class TestGenerationPipelineLattice:
def test_positive_lattice_is_ai_evidence_but_never_a_watermark(self, tmp_clean_png: Path):
"""The lattice may support an AI verdict; it may not enter the watermark list.
It accepts 24% of Adobe Firefly output and dies on a seven-pixel crop, so
reporting it beside C2PA watermark assertions would misrepresent both. The
watermark assertion is checked by absence, because that is the failure that
actually shipped.
"""
with (
patch("remove_ai_watermarks.identify._invisible_watermark", return_value=None),
patch("remove_ai_watermarks.identify._pipeline_lattice", return_value=True),
patch("remove_ai_watermarks.identify._trustmark", return_value=None),
):
report = identify(tmp_clean_png, check_visible=False, check_invisible=True)
assert report.is_ai_generated is True
assert any(signal.name == "pipeline_lattice" for signal in report.signals)
assert not any("synthid" in watermark.lower() for watermark in report.watermarks)
assert not any("watermark" in watermark.lower() for watermark in report.watermarks)
assert any("not a watermark" in caveat for caveat in report.caveats)
def test_negative_lattice_does_not_claim_clean(self, tmp_clean_png: Path):
with (
patch("remove_ai_watermarks.identify._invisible_watermark", return_value=None),
patch("remove_ai_watermarks.identify._pipeline_lattice", return_value=False),
patch("remove_ai_watermarks.identify._trustmark", return_value=None),
):
report = identify(tmp_clean_png, check_visible=False, check_invisible=True)
assert report.is_ai_generated is None
assert not any(signal.name == "pipeline_lattice" for signal in report.signals)
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
class TestIdentifyCaveats:
def test_legacy_openai_has_no_synthid_claim(self):
+1 -2
View File
@@ -14,8 +14,7 @@ from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import synthid_affine_lattice_probe as probe
from remove_ai_watermarks._synthid_confirmation import RegisteredConfirmationComponents
from synthid_runtime._synthid_confirmation import RegisteredConfirmationComponents
def test_webp_lossless_round_trip_preserves_pixels() -> None:
+1 -2
View File
@@ -11,8 +11,7 @@ import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import synthid_affine_lattice_probe as research_probe
from remove_ai_watermarks._synthid_confirmation import (
from synthid_runtime._synthid_confirmation import (
RegisteredConfirmationComponents,
registered_confirmation_components,
)
+18 -19
View File
@@ -7,10 +7,9 @@ from pathlib import Path
import numpy as np
import pytest
import synthid_runtime.synthid_detector as detector
from PIL import Image
import remove_ai_watermarks.synthid_detector as detector
@pytest.fixture(scope="module")
def supported_images(tmp_path_factory: pytest.TempPathFactory) -> tuple[Path, Path]:
@@ -105,7 +104,7 @@ def fine_opponent_registered_positive(tmp_path_factory: pytest.TempPathFactory)
def test_bundled_model_is_the_frozen_calibrated_artifact() -> None:
model = Path(detector.__file__).parent / "assets" / detector.MODEL_FILENAME
model = Path(detector.__file__).parent / detector.MODEL_FILENAME
assert hashlib.sha256(model.read_bytes()).hexdigest() == (
"ee7838da8542c206c3403284b68e98f0ac99429e82f262c1a438f50a638b488b"
@@ -330,7 +329,7 @@ def test_registered_mode_falls_back_to_the_opponent_color_expert(
monkeypatch: pytest.MonkeyPatch,
opponent_registered_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
monkeypatch.setattr(registered_detector, "registered_score", lambda *_args: 0.0)
@@ -346,7 +345,7 @@ def test_opponent_registered_threshold_mutation_changes_the_real_verdict(
monkeypatch: pytest.MonkeyPatch,
opponent_registered_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
monkeypatch.setattr(registered_detector, "registered_score", lambda *_args: 0.0)
baseline = detector.detect_synthid(opponent_registered_positive, register_scale=True)
@@ -368,7 +367,7 @@ def test_opponent_fallback_recovers_period8_without_codec_grid(
monkeypatch: pytest.MonkeyPatch,
opponent_period8_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
monkeypatch.setattr(registered_detector, "registered_score", lambda *_args: 0.0)
@@ -382,7 +381,7 @@ def test_fine_opponent_fallback_recovers_off_grid_period(
monkeypatch: pytest.MonkeyPatch,
fine_opponent_registered_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
monkeypatch.setattr(registered_detector, "registered_score", lambda *_args: 0.0)
monkeypatch.setattr(registered_detector, "opponent_registered_score", lambda *_args: 0.0)
@@ -399,7 +398,7 @@ def test_fine_opponent_threshold_mutation_changes_the_real_verdict(
monkeypatch: pytest.MonkeyPatch,
fine_opponent_registered_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
monkeypatch.setattr(registered_detector, "registered_score", lambda *_args: 0.0)
monkeypatch.setattr(registered_detector, "opponent_registered_score", lambda *_args: 0.0)
@@ -421,7 +420,7 @@ def test_fine_opponent_threshold_mutation_changes_the_real_verdict(
def test_fine_opponent_selector_recovers_the_fractional_period(
fine_opponent_registered_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
template, sigma, *_model = detector._load_template()
pixels = np.asarray(Image.open(fine_opponent_registered_positive).convert("RGB"), dtype=np.uint8)
@@ -436,7 +435,7 @@ def test_period8_codec_veto_threshold_mutation_changes_real_components(
monkeypatch: pytest.MonkeyPatch,
opponent_period8_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
template, sigma, *_model = detector._load_template()
pixels = np.asarray(Image.open(opponent_period8_positive).convert("RGB"), dtype=np.uint8)
@@ -450,7 +449,7 @@ def test_period8_codec_veto_threshold_mutation_changes_real_components(
def test_opponent_registered_period_band_and_codec_veto_are_required() -> None:
from remove_ai_watermarks._synthid_registered import OpponentRegisteredComponents
from synthid_runtime._synthid_registered import OpponentRegisteredComponents
values = {
"spectral_score": 0.8,
@@ -503,7 +502,7 @@ def test_registered_threshold_mutation_changes_the_real_verdict(
def test_registered_period_thresholds_cover_the_bounded_search() -> None:
from remove_ai_watermarks._synthid_registered import _period_threshold
from synthid_runtime._synthid_registered import _period_threshold
assert _period_threshold(7.5) == pytest.approx(0.3770629524888979)
assert _period_threshold(12.0) == pytest.approx(0.19794247706938645)
@@ -516,7 +515,7 @@ def test_registered_amplitude_threshold_mutation_changes_the_real_verdict(
monkeypatch: pytest.MonkeyPatch,
registered_scale_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
baseline = detector.detect_synthid(registered_scale_positive, register_scale=True)
assert baseline.status == "detected"
@@ -532,8 +531,8 @@ def test_registered_amplitude_threshold_mutation_changes_the_real_verdict(
def test_registered_spectral_candidate_disagreement_blocks_decision() -> None:
from remove_ai_watermarks._synthid_confirmation import RegisteredConfirmationComponents
from remove_ai_watermarks._synthid_registered import RegisteredComponents
from synthid_runtime._synthid_confirmation import RegisteredConfirmationComponents
from synthid_runtime._synthid_registered import RegisteredComponents
confirmation = RegisteredConfirmationComponents(12.8, 0.5, 0.2, 0.5, 8, 8)
matching = RegisteredComponents(0.5, 0.25, 12.8, 12.8, 0.15, confirmation)
@@ -550,7 +549,7 @@ def test_registered_high_band_mutation_changes_the_real_verdict(
monkeypatch: pytest.MonkeyPatch,
registered_scale_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_registered as registered_detector
components = registered_detector.registered_components(
np.asarray(Image.open(registered_scale_positive).convert("RGB"), dtype=np.uint8),
@@ -573,8 +572,8 @@ def test_registered_confirmation_mutation_changes_the_real_verdict(
monkeypatch: pytest.MonkeyPatch,
registered_scale_positive: Path,
) -> None:
import remove_ai_watermarks._synthid_confirmation as confirmation_detector
import remove_ai_watermarks._synthid_registered as registered_detector
import synthid_runtime._synthid_confirmation as confirmation_detector
import synthid_runtime._synthid_registered as registered_detector
components = registered_detector.registered_components(
np.asarray(Image.open(registered_scale_positive).convert("RGB"), dtype=np.uint8),
@@ -656,7 +655,7 @@ def test_supported_geometry_requires_pixel_dependencies(
_positive, negative = supported_images
monkeypatch.setattr(detector, "is_available", lambda: False)
with pytest.raises(RuntimeError, match="pixel extra"):
with pytest.raises(RuntimeError, match="needs numpy and OpenCV"):
detector.detect_synthid(negative, register_scale=False)