mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-31 17:50:35 +02:00
Stop double-counting a named forensic mark as SynthID provenance
A manifest that names its own forensic soft-binding algorithm carries that vendor's mark; the generic watermark-action vendor-token inference must not add a second, differently-attributed invisible watermark from the same bytes. Microsoft Designer manifests triggered exactly that: signed by Microsoft, watermarked by InvisMark, with the generation agent named "Azure OpenAI ImageGen" - the OpenAI issuer token inside that service name plus the InvisMark watermarked action satisfied the OpenAI SynthID-evidence rule, and identify reported one forensic mark as two paid pixel watermarks. Three changes, one rule at every inference site (the verdict-scan comment's own lesson: a rule that lives in only one copy is a rule the others silently lack): - c2pa.py structured path: SynthID evidence now scopes to the signer/generator identity strings only (signature issuer, claim generator), never the raw chain, and is suppressed entirely when a soft-binding algorithm is named. - c2pa.py byte fallback and metadata.py synthid_source: suppressed when the scan names a soft-binding algorithm. - identify.py verdict scan: same suppression. Gemini and ChatGPT originals keep their provenance-asserted SynthID strings; the Designer regression is pinned by test_designer_synthid_suppression.py (agent name alone is not the vendor's provenance, and a named soft binding suppresses the inference).
This commit is contained in:
@@ -32,7 +32,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.32.0"
|
||||
__version__ = "0.32.1"
|
||||
|
||||
__all__ = [
|
||||
"BatchSummary",
|
||||
|
||||
@@ -426,6 +426,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
|
||||
source_types: list[str] = []
|
||||
soft_binding_algorithms: list[str] = []
|
||||
soft_binding_values: list[str] = []
|
||||
# Raw signer/generator identity strings, used to scope SynthID evidence to
|
||||
# the vendor that actually asserted the manifest. A vendor token appearing
|
||||
# anywhere else in the chain (e.g. Microsoft Designer's "Azure OpenAI
|
||||
# ImageGen" softwareAgent) is a service name, not that vendor's provenance.
|
||||
identity_strings: list[str] = []
|
||||
claim_generator_asserts_ai = False
|
||||
|
||||
def add_tool_matches(value: str, *, asserts_ai: bool = False) -> None:
|
||||
@@ -443,9 +448,11 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
|
||||
value = signature.get(key)
|
||||
if isinstance(value, str):
|
||||
issuers.extend(_ordered_matches(value.encode(), C2PA_ISSUERS))
|
||||
identity_strings.append(value)
|
||||
|
||||
direct_generator = manifest.get("claim_generator")
|
||||
if isinstance(direct_generator, str):
|
||||
identity_strings.append(direct_generator)
|
||||
add_tool_matches(direct_generator, asserts_ai=True)
|
||||
|
||||
candidates = manifest.get("claim_generator_info")
|
||||
@@ -456,6 +463,7 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
|
||||
name = cast("dict[object, object]", candidate_value).get("name")
|
||||
if isinstance(name, str):
|
||||
add_tool_matches(name, asserts_ai=True)
|
||||
identity_strings.append(name)
|
||||
|
||||
assertions = manifest.get("assertions")
|
||||
if not isinstance(assertions, list):
|
||||
@@ -528,9 +536,14 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
|
||||
has_watermark_action = any(action.startswith("watermarked") for action in actions)
|
||||
if has_watermark_action:
|
||||
info["watermarked"] = True
|
||||
if info.get("ai_source_kind"):
|
||||
selected_bytes = json.dumps(chain, ensure_ascii=False).encode()
|
||||
synthid = synthid_evidence_vendors_in(selected_bytes, has_watermark_action=has_watermark_action)
|
||||
if info.get("ai_source_kind") and not soft_binding_algorithms:
|
||||
# Evidence scope: only the signer/generator identity strings above, never
|
||||
# the whole chain - a vendor named inside another vendor's manifest (the
|
||||
# Designer case) must not turn into that vendor's SynthID provenance. A
|
||||
# manifest that names its own forensic soft-binding algorithm carries
|
||||
# that vendor's mark and is excluded from the generic inference entirely.
|
||||
identity_bytes = json.dumps(identity_strings, ensure_ascii=False).encode()
|
||||
synthid = synthid_evidence_vendors_in(identity_bytes, has_watermark_action=has_watermark_action)
|
||||
if synthid:
|
||||
info["synthid_vendors"] = synthid
|
||||
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
|
||||
@@ -611,7 +624,11 @@ def _populate_registry_fields(buffer: bytes, info: dict[str, Any]) -> bool:
|
||||
|
||||
if b"c2pa.watermarked" in buffer:
|
||||
info["watermarked"] = True
|
||||
synthid = synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
|
||||
synthid = (
|
||||
[]
|
||||
if soft_binding_vendors_in(buffer)
|
||||
else synthid_evidence_vendors_in(buffer, has_watermark_action=info.get("watermarked", False))
|
||||
)
|
||||
if ai_source and synthid:
|
||||
info["synthid_vendors"] = synthid
|
||||
info["synthid_watermark"] = synthid_verdict(", ".join(synthid))
|
||||
|
||||
@@ -1273,7 +1273,19 @@ def _identify_from_evidence(
|
||||
# reusing the derived `has_c2pa` / `source_kind` above, which are broader:
|
||||
# the file path's answer must not move.
|
||||
trained_source = b"trainedAlgorithmicMedia" in head or b"TrainedAlgorithmicMedia" in head
|
||||
if not synthid and trained_source and c2pa_marker_in(head) and (vendors := synthid_evidence_vendors_in(region)):
|
||||
# Same suppression as every other inference site: bytes that name their own
|
||||
# forensic soft-binding algorithm carry that vendor's mark, and the generic
|
||||
# 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").
|
||||
|
||||
if (
|
||||
not synthid
|
||||
and trained_source
|
||||
and c2pa_marker_in(head)
|
||||
and not soft_binding_vendors_in(region)
|
||||
and (vendors := synthid_evidence_vendors_in(region))
|
||||
):
|
||||
synthid = synthid_verdict(", ".join(vendors))
|
||||
if synthid:
|
||||
watermarks.append(
|
||||
|
||||
@@ -839,6 +839,13 @@ def synthid_source(image_path: Path, *, c2pa_info: dict[str, Any] | None = None)
|
||||
ai_source = b"trainedAlgorithmicMedia" in data or b"TrainedAlgorithmicMedia" in data
|
||||
if not (has_c2pa and ai_source):
|
||||
return None
|
||||
from remove_ai_watermarks._internal.c2pa import soft_binding_vendors_in
|
||||
|
||||
# A scan that names its own forensic soft-binding algorithm carries that
|
||||
# vendor's mark; the generic vendor-token inference must not add a second,
|
||||
# differently-attributed invisible watermark from the same bytes.
|
||||
if soft_binding_vendors_in(data):
|
||||
return None
|
||||
matched = synthid_evidence_vendors_in(data)
|
||||
return ", ".join(matched) if matched else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user