Emit the Content Seal as its own signal on standalone-AI-tag files

The strength router already bets that a file whose only provenance is
the standalone AI digital-source tag is Meta Muse output (C2PA issuers
win first), and Muse stamps every output with the invisible Content
Seal. identify now surfaces that same bet as evidence: the additive
content_seal signal (medium confidence - an attribution, not a decode,
since no public decoder exists) plus the watermark string, emitted on
exactly the standalone_iptc condition that routes the cohort. Clients
select pixel removal from the signal list instead of parsing caveats,
the way InvisMark is additive over soft_binding. The API's invisible
gate already runs on ai_from_metadata, so all/invisible behavior is
unchanged; has_invisible_target needs no edit.
This commit is contained in:
Victor Kuznetsov
2026-08-27 16:25:11 -07:00
parent d8fcd0f79b
commit dcc1bf5e07
6 changed files with 56 additions and 7 deletions
+6 -1
View File
@@ -165,7 +165,12 @@ This project has no local Content Seal decoder. Meta Model API outputs and
Meta CDN copies carry an XMP `iptcExt:DigitalSourceType =
trainedAlgorithmicMedia` companion tag, which `identify` reports through the
existing Made-with-AI path; that IPTC code is a standard, not a Meta-exclusive
signal, so it cannot key a strength cohort the way the C2PA issuer does. The
signal, so it cannot key a strength cohort the way the C2PA issuer does. Since
0.33.0 a standalone AI digital-source tag (no C2PA manifest) additionally emits
the additive `content_seal` signal - the strength router's Meta bet as evidence,
medium confidence, with the same caveat - so clients select pixel removal from
the signal list exactly the way InvisMark is additive over `soft_binding`. It
is an attribution, never a decode. The
external oracle is `https://meta.ai/identification`: anonymous, no login,
accepts image, video, and audio, enforces an unspecified daily identification
limit, and answers with model attribution (`Muse Image 1 - Meta`) plus a
+1 -1
View File
@@ -4,7 +4,7 @@
# on it, including the ComfyUI node package. The console script below carries
# the same weight, since users have it on PATH.
name = "remove-ai-watermarks"
version = "0.32.1"
version = "0.33.0"
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
readme = "README.md"
requires-python = ">=3.11,<3.15"
+1 -1
View File
@@ -32,7 +32,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
__version__ = "0.32.1"
__version__ = "0.33.0"
__all__ = [
"BatchSummary",
+18 -3
View File
@@ -1278,12 +1278,12 @@ def _identify_from_evidence(
# vendor-token inference must not add a second, differently-attributed
# invisible watermark (Microsoft Designer: "Azure OpenAI ImageGen" agent +
# the InvisMark watermarked action read as "SynthID per OpenAI").
soft_binding_vendors = soft_binding_vendors_in(region)
if (
not synthid
and trained_source
and c2pa_marker_in(head)
and not soft_binding_vendors_in(region)
and not soft_binding_vendors
and (vendors := synthid_evidence_vendors_in(region))
):
synthid = synthid_verdict(", ".join(vendors))
@@ -1300,7 +1300,7 @@ def _identify_from_evidence(
# ── C2PA soft-binding: a named forensic/third-party watermark vendor ─
# (Adobe TrustMark, Digimarc, Imatag, ...). Present in the manifest even when
# the watermark itself can't be decoded; names whose watermark stamped the pixels.
soft_binding = meta.get("soft_binding") or (", ".join(v) if (v := soft_binding_vendors_in(region)) else None)
soft_binding = meta.get("soft_binding") or (", ".join(soft_binding_vendors) if soft_binding_vendors else None)
if soft_binding:
soft_binding_algorithm = meta.get("soft_binding_algorithm") or info.get("soft_binding_algorithm")
soft_binding_value = meta.get("soft_binding_value") or info.get("soft_binding_value")
@@ -1334,6 +1334,21 @@ def _identify_from_evidence(
if standalone_iptc:
signals.append(Signal("iptc", "digitalSourceType (Made with AI)", "high"))
watermarks.append("IPTC digitalSourceType (Made with AI)")
# Muse Image stamps every output with the invisible Content Seal, and this
# tag is the only provenance such a file carries - the same measured bet
# the strength router makes (vendor_for_strength -> "meta"). Emit the seal
# as its own stable signal, the way InvisMark is additive over
# soft_binding, so clients select pixel removal from the signal list
# instead of parsing caveats. It is an attribution, not a decode: no
# public Content Seal decoder exists, hence "medium".
signals.append(
Signal(
"content_seal",
"Meta Muse Content Seal pixel watermark (attributed by the standalone AI digital-source tag)",
"medium",
)
)
watermarks.append("Invisible Content Seal watermark (Meta Muse attribution)")
caveats.append(_IPTC_ONLY_CAVEAT)
caveats.append(_CONTENT_SEAL_CAVEAT)
if platform is None:
+29
View File
@@ -666,6 +666,35 @@ class TestIdentifyRealSamples:
assert r.is_ai_generated is True
assert r.ai_source_kind == "enhanced"
def test_standalone_ai_tag_attributes_the_content_seal(self, tmp_path: Path):
"""A standalone AI digital-source tag emits the seal as its own signal.
Muse Image outputs carry no C2PA; this tag is their only provenance, and
Muse stamps every output with the invisible Content Seal. The signal is
the strength router's Meta bet as evidence - an attribution, not a decode
(no public decoder exists), so its confidence is medium and the caveat
still points at the oracle.
"""
p = tmp_path / "muse-tag.jpg"
p.write_bytes(
b'\xff\xd8\xff\xe1<x:xmpmeta Iptc4xmpExt:DigitalSourceType="trainedAlgorithmicMedia"></x:xmpmeta>\xff\xd9'
)
r = identify(p, check_visible=False, check_invisible=False)
names = [s.name for s in r.signals]
assert "iptc" in names
assert "content_seal" in names
seal = next(s for s in r.signals if s.name == "content_seal")
assert seal.confidence == "medium"
assert "Invisible Content Seal watermark (Meta Muse attribution)" in r.watermarks
assert any("meta.ai/identification" in c for c in r.caveats)
def test_c2pa_backed_file_gets_no_content_seal_attribution(self):
"""C2PA issuers win first: a manifest-backed file is not Meta-routed."""
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False, check_invisible=False)
assert "content_seal" not in [s.name for s in r.signals]
def test_flux_bfl_c2pa_png(self):
# flux-1.png: real Black Forest Labs FLUX.2 Playground output (signed C2PA).
r = identify(SAMPLES_DIR / "flux-1.png", check_visible=False)
Generated
+1 -1
View File
@@ -3169,7 +3169,7 @@ wheels = [
[[package]]
name = "remove-ai-watermarks"
version = "0.32.1"
version = "0.33.0"
source = { editable = "." }
dependencies = [
{ name = "c2pa-python" },