mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-31 03:27:24 +02:00
feat(invisible): vendor-adaptive default strength (OpenAI 0.10 / Google 0.15)
The default img2img strength is now chosen from the detected SynthID vendor (C2PA issuer) instead of a single fixed 0.30: OpenAI gpt-image -> 0.10, Google Gemini -> 0.15, unknown source -> 0.15. Explicit --strength always wins. Basis: an oracle-verified June 2026 controlled study (clean v0.8.6, text/face protection OFF, per-image openai.com/verify or Gemini-app verdict). OpenAI's SynthID clears at 0.05 across 1024-1600 px (n=4, resolution-independent); Google's is ~3x more robust and needs 0.15 on the capped-1536 path (n=4). The dominant factor is the VENDOR, not resolution. The earlier single 0.30 default and the "resolution dependence" lore came from contaminated tests run with the protect-text bug ON (issue #14) -- re-running those same 1600x1600 images clean removes SynthID at 0.05. `vendor_for_strength(path)` reads metadata.synthid_source on the ORIGINAL input and is threaded through cli (invisible/all/batch) -> invisible_engine -> watermark_remover -> resolve_strength(strength, profile, vendor), so display and execution use the same vendor (the engine sees a temp path whose C2PA the visible pass already stripped, so detection must happen in the CLI on the pristine source). Caveat: Google's 0.15 was validated only on --max-resolution 1536; native 2816 Gemini was not locally measurable (OOM on Apple Silicon) and is pending GPU validation on raiw.cc. Docs: docs/synthid.md sections 2.2/4.4/5.2 corrected (the contaminated resolution-dependence findings replaced with the clean oracle-verified table); README and CLAUDE.md updated; CLI --strength help reflects the adaptive default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1708857772
commit
96038f960f
+62
-6
@@ -6,6 +6,7 @@ code paths work correctly on CPU, MPS (macOS), and CUDA (Linux/Windows).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -15,6 +16,9 @@ from remove_ai_watermarks.noai.utils import get_image_format, is_supported_forma
|
||||
from remove_ai_watermarks.noai.watermark_profiles import (
|
||||
CTRLREGEN_DEFAULT_STRENGTH,
|
||||
DEFAULT_STRENGTH,
|
||||
GEMINI_STRENGTH,
|
||||
OPENAI_STRENGTH,
|
||||
UNKNOWN_STRENGTH,
|
||||
detect_model_profile,
|
||||
get_model_id_for_profile,
|
||||
resolve_strength,
|
||||
@@ -125,19 +129,31 @@ class TestModelProfiles:
|
||||
|
||||
|
||||
class TestResolveStrength:
|
||||
"""resolve_strength applies the profile default only when strength is unset."""
|
||||
"""resolve_strength applies the profile/vendor default only when strength is unset."""
|
||||
|
||||
def test_none_default_profile_uses_sdxl_default(self):
|
||||
assert resolve_strength(None, "default") == DEFAULT_STRENGTH
|
||||
def test_none_default_profile_is_vendor_adaptive(self):
|
||||
# No vendor -> unknown default; OpenAI lower, Google == unknown.
|
||||
assert resolve_strength(None, "default") == UNKNOWN_STRENGTH
|
||||
assert resolve_strength(None, "default", "openai") == OPENAI_STRENGTH
|
||||
assert resolve_strength(None, "default", "google") == GEMINI_STRENGTH
|
||||
assert resolve_strength(None, "default", None) == UNKNOWN_STRENGTH
|
||||
# An unrecognized vendor string falls through to the unknown default.
|
||||
assert resolve_strength(None, "default", "adobe") == UNKNOWN_STRENGTH
|
||||
|
||||
def test_default_strength_alias_is_unknown_vendor_value(self):
|
||||
assert DEFAULT_STRENGTH == UNKNOWN_STRENGTH
|
||||
assert OPENAI_STRENGTH < UNKNOWN_STRENGTH
|
||||
|
||||
def test_none_ctrlregen_uses_clean_noise_default(self):
|
||||
# ctrlregen must NOT inherit the SDXL DEFAULT_STRENGTH (that makes it a no-op);
|
||||
# clean-noise regeneration is the lever against robust marks.
|
||||
# ctrlregen must NOT inherit the SDXL vendor defaults (that makes it a no-op);
|
||||
# clean-noise regeneration is the lever against robust marks. Vendor is ignored.
|
||||
assert resolve_strength(None, "ctrlregen") == CTRLREGEN_DEFAULT_STRENGTH
|
||||
assert resolve_strength(None, "ctrlregen", "openai") == CTRLREGEN_DEFAULT_STRENGTH
|
||||
assert CTRLREGEN_DEFAULT_STRENGTH > DEFAULT_STRENGTH
|
||||
|
||||
def test_explicit_value_overrides_both_profiles(self):
|
||||
def test_explicit_value_overrides_profile_and_vendor(self):
|
||||
assert resolve_strength(0.3, "default") == 0.3
|
||||
assert resolve_strength(0.3, "default", "openai") == 0.3
|
||||
assert resolve_strength(0.3, "ctrlregen") == 0.3
|
||||
|
||||
def test_explicit_zero_is_respected_not_treated_as_unset(self):
|
||||
@@ -145,6 +161,46 @@ class TestResolveStrength:
|
||||
# (the old `strength or DEFAULT` bug would have). Range validation lives in
|
||||
# remove_watermark, not here.
|
||||
assert resolve_strength(0.0, "ctrlregen") == 0.0
|
||||
assert resolve_strength(0.0, "default", "google") == 0.0
|
||||
|
||||
|
||||
class TestVendorForStrength:
|
||||
"""vendor_for_strength normalizes the C2PA SynthID proxy to openai/google/None."""
|
||||
|
||||
@staticmethod
|
||||
def _patch(value):
|
||||
return patch("remove_ai_watermarks.metadata.synthid_source", return_value=value)
|
||||
|
||||
def test_openai(self):
|
||||
from remove_ai_watermarks.noai.watermark_profiles import vendor_for_strength
|
||||
|
||||
with self._patch("OpenAI"):
|
||||
assert vendor_for_strength(Path("x.png")) == "openai"
|
||||
|
||||
def test_google(self):
|
||||
from remove_ai_watermarks.noai.watermark_profiles import vendor_for_strength
|
||||
|
||||
with self._patch("Google"):
|
||||
assert vendor_for_strength(Path("x.png")) == "google"
|
||||
|
||||
def test_both_issuers_google_wins(self):
|
||||
# The more-robust watermark wins -> safer (higher) strength.
|
||||
from remove_ai_watermarks.noai.watermark_profiles import vendor_for_strength
|
||||
|
||||
with self._patch("OpenAI, Google"):
|
||||
assert vendor_for_strength(Path("x.png")) == "google"
|
||||
|
||||
def test_none_when_no_synthid_source(self):
|
||||
from remove_ai_watermarks.noai.watermark_profiles import vendor_for_strength
|
||||
|
||||
with self._patch(None):
|
||||
assert vendor_for_strength(Path("x.png")) is None
|
||||
|
||||
def test_unreadable_metadata_is_none(self):
|
||||
from remove_ai_watermarks.noai.watermark_profiles import vendor_for_strength
|
||||
|
||||
with patch("remove_ai_watermarks.metadata.synthid_source", side_effect=OSError):
|
||||
assert vendor_for_strength(Path("x.png")) is None
|
||||
|
||||
|
||||
# ── Format utilities ────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user