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
-8
View File
@@ -13,7 +13,6 @@ High-level API (lazy, so ``import remove_ai_watermarks`` stays cheap)::
raiw.remove_video_metadata("in.mp4", "out.mp4") # verified metadata strip
raiw.remove_video_invisible("in.mp4", "out.mp4") # oracle-certified SynthID removal
raiw.remove_video_visible("in.mp4", "out.mp4") # stable visible video-mark removal
raiw.detect_synthid("in.png") # -> SynthIDDetection
raiw.verify_openai_synthid("in.png", acknowledge_upload=True) # remote
For a provenance verdict use the ``identify`` submodule::
@@ -43,9 +42,7 @@ __all__ = [
"OpenAIProvenanceError",
"OpenAISynthIDDetection",
"RemoveAllResult",
"SynthIDDetection",
"__version__",
"detect_synthid",
"identify_video",
"inspect_video_metadata",
"remove_all",
@@ -76,7 +73,6 @@ if TYPE_CHECKING:
OpenAISynthIDDetection,
verify_openai_synthid,
)
from remove_ai_watermarks.synthid_detector import SynthIDDetection, detect_synthid
from remove_ai_watermarks.video import (
identify_video,
inspect_video_metadata,
@@ -116,10 +112,6 @@ def __getattr__(name: str) -> object:
from remove_ai_watermarks import video
return getattr(video, name)
if name in ("SynthIDDetection", "detect_synthid"):
from remove_ai_watermarks import synthid_detector
return getattr(synthid_detector, name)
if name in ("OpenAIProvenanceError", "OpenAISynthIDDetection", "verify_openai_synthid"):
from remove_ai_watermarks import openai_provenance
+16 -82
View File
@@ -297,8 +297,8 @@ _force_option = click.option(
help=(
"Run the diffusion scrub even when no invisible AI watermark is locally "
"detectable. Default: skip it (regeneration only degrades a clean image; a "
"skip never claims the image is watermark-free -- the local SynthID detector "
"covers one carrier family in a calibrated image-size range)."
"skip never claims the image is watermark-free -- this package has no local "
"SynthID pixel decoder)."
),
)
_cpu_offload_option = click.option(
@@ -455,20 +455,18 @@ def _no_invisible_signal_exit(source: Path) -> NoReturn:
:func:`identify` finds no locally-detectable invisible AI signal, running it
anyway would damage a clean image for nothing -- the dominant paid score-0
cause on no-watermark uploads. So skip it, but do NOT imply the image is
clean: only one SynthID carrier family in a calibrated image-size range has
a local detector, so other sizes or epochs can still be present after
their metadata proxy is gone. Write no output and exit
:data:`EXIT_NO_INVISIBLE_SIGNAL`; ``--force`` runs the scrub regardless.
clean: Google does not publish the SynthID payload decoder, and this package
does not ship one, so a mark can still be present after its metadata proxy
is gone. Write no output and exit :data:`EXIT_NO_INVISIBLE_SIGNAL`;
``--force`` runs the scrub regardless.
"""
console.print(
" No supported invisible AI watermark detected (no provenance, supported\n"
" SynthID carrier, or open watermark). Skipped the diffusion scrub --\n"
" regenerating the pixels would only degrade the image with nothing to\n"
" remove, so no output was written.\n"
" This does NOT prove the image is clean: the local SynthID detector covers\n"
" one carrier family in a calibrated image-size range. If you know the image\n"
" is AI-generated and want the pixels regenerated regardless, re-run with\n"
" --force:\n"
" No supported invisible AI watermark detected (no provenance or open\n"
" watermark). Skipped the diffusion scrub -- regenerating the pixels would\n"
" only degrade the image with nothing to remove, so no output was written.\n"
" This does NOT prove the image is clean: this package has no local SynthID\n"
" pixel decoder. If you know the image is AI-generated and want the pixels\n"
" regenerated regardless, re-run with --force:\n"
f" remove-ai-watermarks invisible {source.name} --force"
)
raise SystemExit(EXIT_NO_INVISIBLE_SIGNAL)
@@ -1318,68 +1316,6 @@ def cmd_video_batch(
raise SystemExit(1)
# ── SynthID pixel detection ──
@main.command("detect-synthid")
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option("--json", "as_json", is_flag=True, help="Emit the detector result as JSON.")
@click.option(
"--register-scale/--fixed-period",
default=None,
help="Force registered production search or the legacy fixed-period diagnostic.",
)
def cmd_detect_synthid(source: Path, as_json: bool, register_scale: bool | None) -> None:
"""Detect a generation-pipeline pixel lattice at calibrated image sizes.
EXPERIMENTAL. The supported route for SynthID is signed provenance, which
`identify` reads and `verify-openai-synthid` confirms against the provider.
This command does NOT detect the SynthID watermark. The statistic it reports is
destroyed by a seven-pixel crop, while SynthID's published evaluation keeps
99.97% of its detection rate under aggressive crop and resize, so what
crosses the threshold identifies the generation pipeline rather than the
mark. Read a positive as "these pixels came from a pipeline that leaves this
lattice", never as "this image is watermarked", and read an indeterminate
result as neither.
"""
from remove_ai_watermarks.synthid_detector import detect_synthid
source = _validate_image(source)
try:
result = detect_synthid(source, register_scale=register_scale)
except RuntimeError as exc:
raise click.ClickException(str(exc)) from exc
if as_json:
click.echo(json.dumps(result.to_dict(), indent=2))
return
_banner()
console.print(f"\n Generation-pipeline lattice (experimental): {result.status}")
console.print(f" Geometry: {result.width}x{result.height}")
if result.score is not None:
console.print(f" Score: {result.score:.6f} (threshold: {result.threshold:.6f})")
console.print(f" Detector: {result.detector}")
if result.reason is not None:
console.print(f" Reason: {result.reason}")
if register_scale is True:
scale_scope = " Bounded spatial-scale registration was explicitly enabled. An indeterminate or\n"
elif register_scale is False:
scale_scope = " The legacy fixed-period diagnostic was explicitly enabled. An indeterminate or\n"
else:
scale_scope = (
" The production router selected the calibrated registered or large-image expert. An indeterminate or\n"
)
console.print(
" Scope: experimental. One periodic lattice family in a calibrated image-size range,\n"
" secondary to signed provenance, which remains the supported SynthID route. This is a\n"
" generation-pipeline signature, not the SynthID watermark: it disappears when the\n"
" image is cropped off the tile grid, and it changes when the generator's pipeline\n"
" changes. A positive says the pixels came from such a pipeline. It does not say the\n"
" image carries a watermark, and it does not say it lacks one.\n"
+ scale_scope
+ " unsupported result is not proof that SynthID is absent."
)
# ── Official OpenAI SynthID verification ──
@main.command("verify-openai-synthid")
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@@ -1470,9 +1406,8 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo
if report.is_ai_generated is None:
console.print(
" No locally-readable AI signal found. This is not the same as 'clean': "
"metadata is often stripped by re-encoding, screenshots, or upload, and the local "
"SynthID pixel detector covers one carrier family in a calibrated image-size range. "
"See caveats below."
"metadata is often stripped by re-encoding, screenshots, or upload, and this "
"package has no local SynthID pixel decoder. See caveats below."
)
if report.integrity_clashes:
@@ -1565,9 +1500,8 @@ def cmd_all(
stage_text = {
("invisible", "no-signal"): (
"Skipped (no invisible AI watermark detected; pixels left intact).\n"
" Not a clean-image guarantee: the local SynthID detector covers one\n"
" carrier family in a calibrated image-size range. Re-run with --force\n"
" to scrub regardless."
" Not a clean-image guarantee: this package has no local SynthID pixel\n"
" decoder. Re-run with --force to scrub regardless."
),
("invisible", "unavailable"): (
f"Warning: Skipped - GPU dependencies not installed.\n Install them with: pip install {INVISIBLE_EXTRA}"
+11 -52
View File
@@ -6,14 +6,13 @@ Aggregates every locally-readable signal into a single :class:`ProvenanceReport`
the signing platform (OpenAI, Google, Adobe, Microsoft).
- **IPTC ``digitalSourceType``** "Made with AI" marker (Meta, X, others).
- **PNG text / EXIF generation parameters** (Stable Diffusion, ComfyUI, InvokeAI).
- **SynthID evidence** -- supported C2PA provenance plus a positive-only local
detector for one confirmed periodic carrier family in a calibrated image-size range.
- **SynthID evidence** -- supported C2PA provenance (Google AI, or current OpenAI
with an explicit watermark action). There is no local SynthID pixel decoder.
- **Registered visible marks** (optional; needs cv2/numpy, no GPU) through the
shared watermark registry.
Hard limit: Google does not publish its payload decoder. The local pixel detector
covers only one measured carrier family in a calibrated image-size range, so
absence of signals is reported as ``Unknown``, never as "clean".
Hard limit: Google does not publish its payload decoder, and this package does
not ship one. Absence of signals is reported as ``Unknown``, never as "clean".
"""
from __future__ import annotations
@@ -109,18 +108,7 @@ _STRIP_CAVEAT = (
"Absence of metadata is not proof the image is clean: C2PA, EXIF, and PNG "
"text chunks are stripped by re-encoding, screenshots, or social-media upload."
)
_SYNTHID_CAVEAT = (
"SynthID presence comes from supported provenance here. The separate local pixel detector "
"covers one measured carrier family in a calibrated image-size range; confirm other cases with "
"the provider oracle."
)
_PIPELINE_LATTICE_CAVEAT = (
"EXPERIMENTAL. Signed provenance is the primary route for SynthID; this pixel result is not a "
"watermark at all but a generation-pipeline lattice: it is destroyed by a "
"crop of seven pixels, while the published SynthID evaluation survives aggressive crop and resize. "
"It accepted 29 of 223 images from other generators, 24% of Adobe Firefly, so it does not identify "
"the provider. A negative or unsupported result is not proof of absence."
)
_SYNTHID_CAVEAT = "SynthID presence comes from supported provenance here. Confirm other cases with the provider oracle."
_IPTC_ONLY_CAVEAT = "The IPTC 'Made with AI' tag flags AI provenance but does not identify the specific platform."
_INVISIBLE_WM_CAVEAT = (
"The open invisible watermark is fragile: it does not survive JPEG re-encoding "
@@ -958,21 +946,6 @@ def _trustmark(image_path: Path) -> str | None:
return detect_trustmark(image_path)
def _pipeline_lattice(image_path: Path, decode: _SharedDecode) -> bool:
"""Whether the supported generation-pipeline lattice is detected.
Named for what it measures. The underlying expert is still called a SynthID
detector in its own module, but its statistic is a lattice anchored at the
image origin that a seven-pixel crop removes, so nothing here may present it
as a watermark.
"""
from remove_ai_watermarks.synthid_detector import detect_synthid, is_available
if not is_available() or (image := decode.get()) is None:
return False
return detect_synthid(image_path, image=image).detected
class _SharedDecode:
"""One decode of the source pixels, shared by every detector in a single report.
@@ -1317,18 +1290,6 @@ def _identify_from_evidence(
if platform is None:
platform = f"{scheme} (open DWT-DCT watermark)"
# ── Generation-pipeline lattice, experimental ───────────────────
# Signed provenance above is the primary SynthID route; this is a secondary
# pixel observation and is kept out of the watermark inventory on purpose. This reads a periodic
# lattice anchored at the image origin, which identifies the pipeline that
# produced the pixels; it is not SynthID and not any watermark, so listing
# it beside C2PA watermark assertions would misrepresent both.
if check_invisible and pixel_path is not None and _pipeline_lattice(pixel_path, decode):
signals.append(Signal("pipeline_lattice", "generation-pipeline lattice (experimental)", "medium"))
caveats.append(_PIPELINE_LATTICE_CAVEAT)
if platform is None:
platform = "generation-pipeline lattice detected (provider not attributed locally)"
# ── Adobe TrustMark invisible watermark (open decoder, no key) ───
# The watermark behind Adobe Durable Content Credentials. Decoded locally,
# but it binds provenance for human-authored content too, so it enriches the
@@ -1341,7 +1302,6 @@ def _identify_from_evidence(
# ── Verdict so far (metadata + embedded watermark) ──────────────
invisible_wm = any(s.name == "invisible_watermark" for s in signals)
pipeline_lattice = any(s.name == "pipeline_lattice" for s in signals)
exif_gen = any(s.name == "exif_generator" for s in signals)
xai_sig = any(s.name == "xai_signature" for s in signals)
ai_from_metadata = bool(
@@ -1351,7 +1311,6 @@ def _identify_from_evidence(
or aigc
or local_keys
or invisible_wm
or pipeline_lattice
or exif_gen
or xai_sig
)
@@ -1473,13 +1432,13 @@ def has_invisible_target(image_path: Path) -> bool:
to remove. Runs :func:`identify` with ``check_visible=False`` -- a visible mark
is handled by the separate visible pass and is NOT a diffusion target -- and
``check_invisible=True`` so an open watermark counts. Returns
``report.ai_from_metadata`` (C2PA AI issuer / SynthID provenance or periodic
carrier, IPTC, AIGC, local gen params, EXIF/xAI, open DWT-DCT / TrustMark).
``report.ai_from_metadata`` (C2PA AI issuer / SynthID provenance, IPTC,
AIGC, local gen params, EXIF/xAI, open DWT-DCT / TrustMark).
IMPORTANT -- this cannot prove a pixel SynthID is absent: the local detector
covers one carrier family in a calibrated image-size range. A False therefore
means "no supported locally-detectable invisible target", not "clean". Callers
must NOT present a skip as a finished clean result.
IMPORTANT -- this cannot prove a pixel SynthID is absent: Google does not
publish the payload decoder, and this package does not ship one. A False
therefore means "no supported locally-detectable invisible target", not
"clean". Callers must NOT present a skip as a finished clean result.
Fail-safe: any error resolves to True so the removal still runs -- leaving a
watermark on a paid removal is worse than over-regenerating a clean image.