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:
Victor Kuznetsov
2026-06-01 19:29:47 -07:00
co-authored by Claude Opus 4.8
parent 1708857772
commit 96038f960f
8 changed files with 243 additions and 87 deletions
+62 -6
View File
@@ -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 ────────────────────────────────────────────────