mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-21 16:21:42 +02:00
Attribute discovered C2PA signers
This commit is contained in:
13 files changed
+312
-51
No files matched your search
@@ -33,7 +33,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.40.2"
|
||||
__version__ = "0.40.3"
|
||||
|
||||
__all__ = [
|
||||
"BatchItemResult",
|
||||
|
||||
@@ -18,6 +18,7 @@ from remove_ai_watermarks._internal.constants import (
|
||||
C2PA_AI_TOOLS,
|
||||
C2PA_AI_VENDORS,
|
||||
C2PA_CHUNK_TYPE,
|
||||
C2PA_IDENTITY_AI_ORGS,
|
||||
C2PA_ISSUERS,
|
||||
C2PA_SIGNATURES,
|
||||
C2PA_SOFT_BINDING_REGISTRY,
|
||||
@@ -529,7 +530,8 @@ def _structured_manifest_fields(store: dict[str, Any]) -> dict[str, Any]:
|
||||
info["ai_tool"] = ", ".join(tools)
|
||||
if actions:
|
||||
info["actions"] = ", ".join(actions)
|
||||
if claim_generator_asserts_ai:
|
||||
signer_identity_asserts_ai = not C2PA_IDENTITY_AI_ORGS.isdisjoint(issuers)
|
||||
if claim_generator_asserts_ai or signer_identity_asserts_ai:
|
||||
info["c2pa_identity_ai"] = True
|
||||
|
||||
generated = any(
|
||||
|
||||
@@ -32,7 +32,7 @@ C2PA_SIGNATURES = tuple(
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class C2paAiVendor:
|
||||
"""One issuer signature and its normalized product attribution."""
|
||||
"""One C2PA identity and its AI-product or neutral-signer attribution."""
|
||||
|
||||
issuer: bytes
|
||||
org: str
|
||||
@@ -41,6 +41,8 @@ class C2paAiVendor:
|
||||
synthid: bool = False
|
||||
asserts_ai: bool = False
|
||||
synthid_requires_watermark_action: bool = False
|
||||
signer_platform: str | None = None
|
||||
raw_signer_platforms: tuple[tuple[bytes, str], ...] = ()
|
||||
|
||||
|
||||
def _vendor(
|
||||
@@ -52,6 +54,8 @@ def _vendor(
|
||||
synthid: bool = False,
|
||||
asserts_ai: bool = False,
|
||||
synthid_requires_watermark_action: bool = False,
|
||||
signer_platform: str | None = None,
|
||||
raw_signer_platforms: tuple[tuple[bytes, str], ...] = (),
|
||||
) -> C2paAiVendor:
|
||||
token = issuer.encode() if isinstance(issuer, str) else issuer
|
||||
return C2paAiVendor(
|
||||
@@ -62,6 +66,8 @@ def _vendor(
|
||||
synthid=synthid,
|
||||
asserts_ai=asserts_ai,
|
||||
synthid_requires_watermark_action=synthid_requires_watermark_action,
|
||||
signer_platform=signer_platform,
|
||||
raw_signer_platforms=raw_signer_platforms,
|
||||
)
|
||||
|
||||
|
||||
@@ -109,11 +115,68 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = (
|
||||
# "Bria Artificial Intelligence"). Found as an unmapped signer on 4 corpus
|
||||
# uploads 2026-08-08 that identify reported as unknown-signer C2PA.
|
||||
_vendor(b"Ideogram", "Ideogram", "Ideogram", "Ideogram", asserts_ai=True),
|
||||
_vendor(b"xAI Grok Imagine", "xAI Grok Imagine", "xAI Grok Imagine", "xAI", asserts_ai=True),
|
||||
_vendor(b"Producer.ai", "Producer.ai", "Producer.ai", "Producer.ai", asserts_ai=True),
|
||||
# This signature names only SPRING's legal entity, not a product. Preserve
|
||||
# that signer identity without guessing which product produced a file.
|
||||
_vendor(b"SPRING (SG) PTE. LTD.", "SPRING (SG) PTE. LTD.", "SPRING (SG) PTE. LTD.", "SPRING"),
|
||||
# These companies also sign ordinary edits and publications. Register the
|
||||
# signer identity for provenance display, but provide no AI platform and do
|
||||
# not let the identity assert an AI verdict.
|
||||
_vendor(
|
||||
b"TikTok Inc.",
|
||||
"TikTok",
|
||||
None,
|
||||
None,
|
||||
signer_platform="TikTok (C2PA signer)",
|
||||
),
|
||||
_vendor(
|
||||
b"Bytedance Pte",
|
||||
"ByteDance",
|
||||
None,
|
||||
None,
|
||||
signer_platform="ByteDance (C2PA signer)",
|
||||
raw_signer_platforms=(
|
||||
(b"Bytedance Pte. Ltd.", "ByteDance (C2PA signer)"),
|
||||
(b"CapCut/", "CapCut (C2PA signer)"),
|
||||
(b"capcut c2pa-rs", "CapCut (C2PA signer)"),
|
||||
),
|
||||
),
|
||||
_vendor(
|
||||
b"Anthropic Claude Content Signing",
|
||||
"Anthropic Claude",
|
||||
None,
|
||||
None,
|
||||
signer_platform="Anthropic Claude (C2PA signer)",
|
||||
),
|
||||
_vendor(
|
||||
b"Samsung Galaxy",
|
||||
"Samsung Galaxy",
|
||||
None,
|
||||
None,
|
||||
signer_platform="Samsung Galaxy (C2PA)",
|
||||
),
|
||||
_vendor(
|
||||
b"com.asus.gallery",
|
||||
"ASUS Gallery",
|
||||
None,
|
||||
None,
|
||||
signer_platform="ASUS Gallery (C2PA signer)",
|
||||
),
|
||||
_vendor(b"Truepic", "Truepic", None, None),
|
||||
)
|
||||
|
||||
C2PA_ISSUERS = {vendor.issuer: vendor.org for vendor in C2PA_AI_VENDORS}
|
||||
C2PA_IDENTITY_AI_ORGS = frozenset(vendor.org for vendor in C2PA_AI_VENDORS if vendor.asserts_ai)
|
||||
C2PA_SIGNER_PLATFORM_BY_ORG = {
|
||||
vendor.org: vendor.signer_platform for vendor in C2PA_AI_VENDORS if vendor.signer_platform is not None
|
||||
}
|
||||
C2PA_SIGNER_PLATFORMS = tuple(
|
||||
pair
|
||||
for vendor in C2PA_AI_VENDORS
|
||||
if vendor.signer_platform is not None
|
||||
for pair in (vendor.raw_signer_platforms or ((vendor.issuer, vendor.signer_platform),))
|
||||
)
|
||||
|
||||
# Product-specific claim generators can sign through a different upstream issuer.
|
||||
# Keep this attribution beside the issuer registry so every C2PA consumer has one
|
||||
@@ -128,6 +191,8 @@ C2PA_CLAIM_GENERATOR_PLATFORMS: tuple[tuple[str, str], ...] = (
|
||||
("recraft.ai", "Recraft"),
|
||||
("topaz labs image api", "Topaz Labs"),
|
||||
("tiktok ad creative toolbox", "TikTok Ad Creative Toolbox"),
|
||||
("fastvid", "FastVid"),
|
||||
("capcut", "CapCut (C2PA signer)"),
|
||||
)
|
||||
|
||||
C2PA_AI_TOOLS = {
|
||||
|
||||
@@ -44,6 +44,8 @@ from remove_ai_watermarks._internal.constants import (
|
||||
C2PA_CLAIM_GENERATOR_PLATFORMS,
|
||||
C2PA_IDENTITY_AI_ORGS,
|
||||
C2PA_ISSUERS,
|
||||
C2PA_SIGNER_PLATFORM_BY_ORG,
|
||||
C2PA_SIGNER_PLATFORMS,
|
||||
)
|
||||
from remove_ai_watermarks._internal.schema import require_schema_version
|
||||
from remove_ai_watermarks.metadata import (
|
||||
@@ -631,7 +633,8 @@ def _ai_tools_in(data: bytes) -> list[str]:
|
||||
# Only tokens verified against a real signed file are listed (Leica, Nikon,
|
||||
# Sony, Truepic, Google Pixel); add Canon/Bria as real samples are captured.
|
||||
# Samsung Galaxy is an AI-capable editing device, not a pure-capture camera, so
|
||||
# it lives in `_SIGNER_C2PA_PLATFORM` below (it must not feed the camera clash).
|
||||
# it lives in the neutral `C2PA_SIGNER_PLATFORMS` registry (it must not feed the
|
||||
# camera clash).
|
||||
_DEVICE_C2PA_PLATFORM: tuple[tuple[bytes, str], ...] = (
|
||||
(b"lc_c2pa", "Leica (camera, C2PA capture)"),
|
||||
(b"Leica Camera", "Leica (camera, C2PA capture)"),
|
||||
@@ -646,6 +649,8 @@ _DEVICE_C2PA_PLATFORM: tuple[tuple[bytes, str], ...] = (
|
||||
# Truepic is a C2PA signing authority whose name appears in the trust chain
|
||||
# of unrelated manifests (e.g. OpenAI), so the bare token mis-attributes.
|
||||
(b"Truepic_Lens", "Truepic Lens (verified capture)"),
|
||||
(b"vivo Camera", "vivo (camera, C2PA capture)"),
|
||||
(b"vivo X300 Pro", "vivo (camera, C2PA capture)"),
|
||||
)
|
||||
|
||||
|
||||
@@ -743,15 +748,14 @@ def _device_platform(head: bytes) -> str | None:
|
||||
# (distinct from the EXIF "SM-xxxx" model string on ordinary Samsung photos).
|
||||
# com.asus.gallery -- ASUS Gallery claim_generator (a C2PA-signed edit, no AI
|
||||
# source type or genAIType on the samples, so it never asserts is_ai).
|
||||
_SIGNER_C2PA_PLATFORM: tuple[tuple[bytes, str], ...] = (
|
||||
(b"Samsung Galaxy", "Samsung Galaxy (C2PA)"),
|
||||
(b"com.asus.gallery", "ASUS Gallery (C2PA signer)"),
|
||||
)
|
||||
|
||||
|
||||
def _signer_platform(head: bytes) -> str | None:
|
||||
"""Map a C2PA editing-app / AI-capable-device signer token to a platform."""
|
||||
return _first_token_match(head, _SIGNER_C2PA_PLATFORM)
|
||||
def _signer_platform(head: bytes, issuers: list[str]) -> str | None:
|
||||
"""Map a raw or normalized C2PA signer identity to a neutral platform."""
|
||||
raw = _first_token_match(head, C2PA_SIGNER_PLATFORMS)
|
||||
normalized = next(
|
||||
(C2PA_SIGNER_PLATFORM_BY_ORG[org] for org in issuers if org in C2PA_SIGNER_PLATFORM_BY_ORG),
|
||||
None,
|
||||
)
|
||||
return raw or normalized
|
||||
|
||||
|
||||
def _attribute_platform(issuers: list[str], *, is_ai: bool = True) -> str | None:
|
||||
@@ -1178,7 +1182,6 @@ def _identify_from_evidence(
|
||||
# below keeps the full buffer: their markers are long and distinctive.
|
||||
region = _metadata_region(head)
|
||||
camera_label = _device_platform(region)
|
||||
signer_label = _signer_platform(region)
|
||||
|
||||
# ── C2PA Content Credentials ────────────────────────────────────
|
||||
has_c2pa = bool(info) or c2pa_marker_in(head)
|
||||
@@ -1189,6 +1192,7 @@ def _identify_from_evidence(
|
||||
# by substring made the displayed reason a second, looser rule than the verdict.
|
||||
failed_c2pa_codes = [str(code) for code in cast("list[object]", info.get("c2pa_failed_codes", []))]
|
||||
issuers = [info["issuer"]] if info.get("issuer") else _issuers_in(region)
|
||||
signer_label = _signer_platform(region, issuers)
|
||||
# Full AI generation (trainedAlgorithmicMedia) vs an AI-enhanced real photo
|
||||
# (compositeWithTrainedAlgorithmicMedia). The structured kind is parsed once in
|
||||
# _internal.c2pa._structured_manifest_fields (covers PNG + any container the c2pa-python
|
||||
@@ -1216,15 +1220,18 @@ def _identify_from_evidence(
|
||||
or (", ".join(tools) if (tools := _ai_tools_in(region)) else None)
|
||||
)
|
||||
# Platform: a distinctive device/camera token in the manifest wins (it is the
|
||||
# signer/producer), then an editing-app/AI-device signer (Samsung Galaxy,
|
||||
# ASUS Gallery), with the issuer byte-scan only as fallback. The issuer scan
|
||||
# alone mis-attributed real samples (Leica->Truepic timestamp authority,
|
||||
# Nikon->Adobe namespace, Pixel->Google Gemini) -- the token scans fix that.
|
||||
# signer/producer), then an exact product generator, then an editing-app or
|
||||
# AI-device signer (Samsung Galaxy, ASUS Gallery), with the issuer byte-scan
|
||||
# only as fallback. The issuer scan alone mis-attributed real samples
|
||||
# (Leica->Truepic timestamp authority, Nikon->Adobe namespace, Pixel->Google
|
||||
# Gemini) -- the token scans fix that.
|
||||
platform = (
|
||||
(
|
||||
camera_label
|
||||
# Exact product generators are useful provenance even when the
|
||||
# signed operation is a non-AI edit (for example, CapCut).
|
||||
or _claim_generator_platform(generator)
|
||||
or signer_label
|
||||
or (_claim_generator_platform(generator) if c2pa_is_ai else None)
|
||||
or (_claim_generator_platform(str(info.get("ai_tool"))) if c2pa_is_ai and info.get("ai_tool") else None)
|
||||
or _attribute_platform(issuers, is_ai=c2pa_is_ai)
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user