mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-31 01:30:35 +02:00
Unplug lattice detector from package, expand research docs
This commit is contained in:
+7
-3
@@ -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
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user