mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-09 15:36:01 +02:00
Fix SynthID provenance evidence and release 0.26.1
This commit is contained in:
@@ -56,7 +56,9 @@ Ground-truth quality, strongest first:
|
||||
- `gemini-app` — checked via the Gemini app "Verify with SynthID" feature. Gold standard for the pixel watermark (Google models).
|
||||
- `openai-verify` — checked via openai.com/verify (gold standard for OpenAI ChatGPT/Codex/API images).
|
||||
- `synthid-portal` — checked via Google's SynthID Detector portal.
|
||||
- `c2pa-metadata` — issuer-only proxy (Google/OpenAI C2PA manifest present). Weaker: the C2PA can be stripped while the pixel watermark remains.
|
||||
- `c2pa-metadata` — supported provenance evidence (Google AI C2PA, or OpenAI
|
||||
C2PA with an explicit `c2pa.watermarked.*` action). Weaker than a provider
|
||||
oracle: C2PA can be stripped while the pixel watermark remains.
|
||||
- `third-party` — label asserted by an external dataset, not independently verified.
|
||||
- `none` — unverified.
|
||||
|
||||
|
||||
@@ -66,9 +66,10 @@ still alter small text or difficult faces.
|
||||
|
||||
### Removal cannot be verified locally for proprietary SynthID
|
||||
|
||||
The project has no public local SynthID pixel decoder. It can infer likely
|
||||
presence from supported provenance metadata, but a missing metadata proxy is
|
||||
not a negative pixel verdict.
|
||||
The project has no public local SynthID pixel decoder. It recognizes presence
|
||||
from supported provenance metadata: Google AI C2PA under Google's all-media
|
||||
policy, or current OpenAI C2PA with an explicit watermark action. A missing
|
||||
provenance signal is not a negative pixel verdict.
|
||||
|
||||
For important outputs:
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ metadata extraction from verdict logic:
|
||||
- `identify_from_evidence` evaluates that evidence without reopening the source. Rules
|
||||
that decide a verdict live here, not in extraction: extraction has two
|
||||
implementations, and a rule in only one of them is a rule the other lacks. The
|
||||
SynthID proxy is the worked example — its structured form comes from the manifest,
|
||||
SynthID provenance evidence is the worked example — its structured form comes from the manifest,
|
||||
and the byte-scan fallback for containers no parser reaches runs in the verdict, so
|
||||
both extractors reach the same answer. It did not, and the record path silently
|
||||
reported no SynthID for images the file path flagged.
|
||||
|
||||
@@ -122,9 +122,12 @@ Current pipeline values, both CUDA-only:
|
||||
The `controlnet`, `sdxl`, `qwen` and `default` values were removed. A retired name
|
||||
is rejected at parse time rather than remapped onto a surviving profile.
|
||||
|
||||
SynthID does not have a public local pixel decoder in this project. The tool can
|
||||
infer likely presence from supported provenance metadata, but after that
|
||||
metadata is removed a local negative result is inconclusive.
|
||||
SynthID does not have a public local pixel decoder in this project. The tool
|
||||
recognizes presence from supported provenance: Google AI C2PA under Google's
|
||||
all-media watermark policy, and current OpenAI C2PA carrying an explicit
|
||||
`c2pa.watermarked.*` action. Legacy OpenAI C2PA without that action does not
|
||||
assert SynthID. After provenance metadata is removed, a local negative result
|
||||
is still inconclusive.
|
||||
|
||||
For MP4, MOV, and M4V, `video invisible` or the explicit
|
||||
`video all --invisible` option can regenerate the video through a VAE and strip
|
||||
|
||||
+17
-11
@@ -289,14 +289,19 @@ The external variant SynthID-O is available "through partnerships" only. Our
|
||||
tool cannot locally detect SynthID presence or absence -- this is by design,
|
||||
not a gap we can fill.
|
||||
|
||||
### 3.2 How our tool detects SynthID (metadata proxy)
|
||||
### 3.2 How our tool recognizes SynthID from provenance
|
||||
|
||||
We detect SynthID indirectly: if the image's C2PA manifest is signed by a
|
||||
known SynthID-using issuer (Google, OpenAI), we infer SynthID is present. This
|
||||
is a **metadata proxy**, not a pixel watermark decode. It works while the C2PA
|
||||
manifest is intact, and is silent once the manifest is stripped or the image
|
||||
is re-encoded without C2PA (e.g., a screenshot, a social-media re-upload, or
|
||||
after `metadata --remove`).
|
||||
We recognize SynthID indirectly from supported C2PA evidence; this is not a
|
||||
pixel watermark decode. Google states that all media generated by its tools is
|
||||
watermarked, so Google AI C2PA establishes SynthID. OpenAI C2PA predates its
|
||||
SynthID rollout, but current manifests add the signed
|
||||
`c2pa.watermarked.unbound` action; OpenAI provenance establishes SynthID only
|
||||
when that action is present. Legacy OpenAI C2PA without it remains valid origin
|
||||
evidence but does not assert a pixel watermark.
|
||||
|
||||
This works while the C2PA manifest is intact and is silent once the manifest is
|
||||
stripped or the image is re-encoded without C2PA (e.g., a screenshot, a
|
||||
social-media re-upload, or after `metadata --remove`).
|
||||
|
||||
This is why:
|
||||
- `identify` on a GitHub-recompressed issue attachment returns Unknown (C2PA is
|
||||
@@ -409,8 +414,9 @@ OpenAI confirmed SynthID adoption (Help Center, updated 2026-05-21):
|
||||
> "ChatGPT images include both C2PA metadata and SynthID watermarks."
|
||||
|
||||
This is time-gated: pre-rollout ChatGPT/gpt-image images carry C2PA without
|
||||
SynthID. Our C2PA proxy therefore over-reports SynthID presence on old images
|
||||
(hence the `_OPENAI_CAVEAT` hedging flag in the codebase).
|
||||
SynthID. Current OpenAI manifests distinguish the watermarked output with the
|
||||
signed `c2pa.watermarked.unbound` action. The detector requires that action, so
|
||||
old OpenAI C2PA remains an origin signal without becoming a SynthID claim.
|
||||
|
||||
### 4.3 Other vendors
|
||||
|
||||
@@ -558,7 +564,7 @@ for `sdxl`, not a separate certification (no local SynthID detector to self-veri
|
||||
The higher strength costs little quality where it matters, because `controlnet` is now
|
||||
the default pipeline, so `sdxl` is reached only via an explicit `--pipeline sdxl` (a
|
||||
deliberate opt-down), where over-regeneration has no faces/text to damage.
|
||||
This uses the vendor signal we DO have locally (the C2PA SynthID proxy) to avoid the
|
||||
This uses the vendor signal we DO have locally (the C2PA SynthID provenance) to avoid the
|
||||
overkill of a single high default on OpenAI images, without needing a local pixel
|
||||
detector. An explicit `--strength` always wins. If the watermark still survives (e.g. a
|
||||
large native Gemini beyond the capped-1536 validation), raise toward 0.35-0.40 (0.40
|
||||
@@ -617,7 +623,7 @@ Two constraints on reading this:
|
||||
- **The correct oracle for OpenAI images is openai.com/verify**, not the Gemini
|
||||
app. The two oracles detect different payloads.
|
||||
- **A quiet `identify` output after processing is not proof of removal.** It
|
||||
means the metadata proxy is gone. The pixel watermark state is unknown without
|
||||
means the provenance evidence is gone. The pixel watermark state is unknown without
|
||||
an oracle check.
|
||||
- **After removal, the output may carry forensic artifacts** detectable by an
|
||||
independent classifier even if the vendor oracle reads negative. Defeating the
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "remove-ai-watermarks"
|
||||
version = "0.26.0"
|
||||
version = "0.26.1"
|
||||
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10.1"
|
||||
|
||||
@@ -32,7 +32,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.26.0"
|
||||
__version__ = "0.26.1"
|
||||
|
||||
__all__ = [
|
||||
"BatchSummary",
|
||||
|
||||
@@ -15,12 +15,12 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
from remove_ai_watermarks._internal.constants import (
|
||||
C2PA_ACTIONS,
|
||||
C2PA_AI_TOOLS,
|
||||
C2PA_AI_VENDORS,
|
||||
C2PA_CHUNK_TYPE,
|
||||
C2PA_ISSUERS,
|
||||
C2PA_SIGNATURES,
|
||||
C2PA_SOFT_BINDINGS,
|
||||
PNG_SIGNATURE,
|
||||
SYNTHID_C2PA_ISSUERS,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -200,18 +200,34 @@ def _claim_generator_from_store(store: dict[str, Any]) -> str | None:
|
||||
|
||||
|
||||
def synthid_verdict(vendors: str) -> str:
|
||||
"""Describe why metadata implies a likely pixel-level SynthID watermark."""
|
||||
return f"likely present ({vendors} embeds SynthID with C2PA)"
|
||||
"""Describe why supported provenance establishes a SynthID watermark."""
|
||||
return f"present according to {vendors} provenance"
|
||||
|
||||
|
||||
def _names_present(buffer: bytes, registry: dict[bytes, str]) -> list[str]:
|
||||
return sorted({label for token, label in registry.items() if token in buffer})
|
||||
|
||||
|
||||
def synthid_vendors_in(buffer: bytes) -> list[str]:
|
||||
"""List matching C2PA issuers known to pair their manifests with SynthID."""
|
||||
registry = {token: label for token, label in C2PA_ISSUERS.items() if token in SYNTHID_C2PA_ISSUERS}
|
||||
return _names_present(buffer, registry)
|
||||
def synthid_evidence_vendors_in(buffer: bytes, *, has_watermark_action: bool | None = None) -> list[str]:
|
||||
"""List issuers whose provenance establishes SynthID for this asset.
|
||||
|
||||
Google applies SynthID to all media generated by its tools, so its AI C2PA
|
||||
provenance is sufficient. OpenAI C2PA predates OpenAI's SynthID rollout;
|
||||
current manifests distinguish the watermarked generation with the explicit
|
||||
``c2pa.watermarked.*`` action. A legacy OpenAI issuer token alone therefore
|
||||
remains provenance evidence, but not SynthID evidence.
|
||||
"""
|
||||
if has_watermark_action is None:
|
||||
has_watermark_action = b"c2pa.watermarked" in buffer
|
||||
return sorted(
|
||||
{
|
||||
vendor.org
|
||||
for vendor in C2PA_AI_VENDORS
|
||||
if vendor.synthid
|
||||
and vendor.issuer in buffer
|
||||
and (has_watermark_action or not vendor.synthid_requires_watermark_action)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def soft_binding_vendors_in(buffer: bytes) -> list[str]:
|
||||
@@ -244,7 +260,9 @@ def _populate_registry_fields(buffer: bytes, info: dict[str, Any]) -> bool:
|
||||
elif b"algorithmicMedia" in buffer:
|
||||
info["source_type"] = "algorithmicMedia"
|
||||
|
||||
synthid = synthid_vendors_in(buffer)
|
||||
if b"c2pa.watermarked" in buffer:
|
||||
info["watermarked"] = True
|
||||
synthid = 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))
|
||||
|
||||
@@ -38,6 +38,7 @@ class C2paAiVendor:
|
||||
needle: str | None
|
||||
synthid: bool = False
|
||||
asserts_ai: bool = False
|
||||
synthid_requires_watermark_action: bool = False
|
||||
|
||||
|
||||
def _vendor(
|
||||
@@ -48,16 +49,32 @@ def _vendor(
|
||||
*,
|
||||
synthid: bool = False,
|
||||
asserts_ai: bool = False,
|
||||
synthid_requires_watermark_action: bool = False,
|
||||
) -> C2paAiVendor:
|
||||
token = issuer.encode() if isinstance(issuer, str) else issuer
|
||||
return C2paAiVendor(token, org, platform, needle, synthid, asserts_ai)
|
||||
return C2paAiVendor(
|
||||
token,
|
||||
org,
|
||||
platform,
|
||||
needle,
|
||||
synthid=synthid,
|
||||
asserts_ai=asserts_ai,
|
||||
synthid_requires_watermark_action=synthid_requires_watermark_action,
|
||||
)
|
||||
|
||||
|
||||
# Order is product priority when a manifest mentions more than one organization.
|
||||
C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = (
|
||||
_vendor(b"Microsoft", "Microsoft", "Microsoft (Bing Image Creator / Designer)", "Microsoft"),
|
||||
_vendor(b"Adobe", "Adobe", "Adobe Firefly", "Adobe"),
|
||||
_vendor(b"OpenAI", "OpenAI", "OpenAI (ChatGPT / gpt-image / DALL-E / Sora)", "OpenAI", synthid=True),
|
||||
_vendor(
|
||||
b"OpenAI",
|
||||
"OpenAI",
|
||||
"OpenAI (ChatGPT / gpt-image / DALL-E / Sora)",
|
||||
"OpenAI",
|
||||
synthid=True,
|
||||
synthid_requires_watermark_action=True,
|
||||
),
|
||||
_vendor(b"Google", "Google LLC", "Google (Gemini / Imagen)", "Google", synthid=True),
|
||||
_vendor(b"Stability AI", "Stability AI", "Stability AI (Stable Image / DreamStudio)", "Stability AI"),
|
||||
_vendor(b"Black Forest Labs", "Black Forest Labs", "Black Forest Labs (FLUX)", "Black Forest Labs"),
|
||||
@@ -85,7 +102,6 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = (
|
||||
|
||||
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)
|
||||
SYNTHID_C2PA_ISSUERS = frozenset(vendor.issuer for vendor in C2PA_AI_VENDORS if vendor.synthid)
|
||||
|
||||
C2PA_AI_TOOLS = {
|
||||
token.encode(): label
|
||||
@@ -148,7 +164,7 @@ AI_GENERATOR_TOKENS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
_C2PA_ACTION_NAMES = _tokens("created|converted|edited|filtered|cropped|resized|opened|placed")
|
||||
_C2PA_ACTION_NAMES = _tokens("created|converted|edited|filtered|cropped|resized|opened|placed|watermarked.unbound")
|
||||
C2PA_ACTIONS = {f"c2pa.{action}".encode(): action for action in _C2PA_ACTION_NAMES}
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ def resolve_strength(
|
||||
|
||||
|
||||
def vendor_for_strength(image_path: Path) -> Literal["openai", "google"] | None:
|
||||
"""Select the strength cohort using the input's SynthID provenance proxy."""
|
||||
"""Select the strength cohort using the input's SynthID provenance evidence."""
|
||||
try:
|
||||
from remove_ai_watermarks.metadata import synthid_source
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ def _no_invisible_signal_exit(source: Path) -> NoReturn:
|
||||
the scrub regardless.
|
||||
"""
|
||||
console.print(
|
||||
" No invisible AI watermark detected (no C2PA/SynthID proxy, no open\n"
|
||||
" No invisible AI watermark detected (no C2PA/SynthID provenance, no 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: a pixel watermark such as SynthID\n"
|
||||
@@ -882,7 +882,7 @@ def _print_metadata_report(source: Path, has_ai: bool, metadata: dict[str, str])
|
||||
|
||||
console.print(f" Warning: AI metadata detected in {source.name}:")
|
||||
if synthid := metadata.get("synthid_watermark"):
|
||||
console.print(f" Warning: SynthID watermark (inferred from C2PA metadata) {synthid}")
|
||||
console.print(f" Warning: SynthID watermark {synthid}")
|
||||
table = Table(show_header=True, header_style="bold")
|
||||
table.add_column("Key", style="cyan")
|
||||
table.add_column("Value")
|
||||
|
||||
@@ -6,8 +6,8 @@ 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 metadata proxy** -- a C2PA companion from a SynthID-using vendor
|
||||
(Google / OpenAI) implies the invisible pixel watermark.
|
||||
- **SynthID provenance evidence** -- Google AI C2PA follows Google's all-media
|
||||
policy; current OpenAI C2PA explicitly declares a watermark action.
|
||||
- **Registered visible marks** (optional; needs cv2/numpy, no GPU) through the
|
||||
shared watermark registry.
|
||||
|
||||
@@ -31,7 +31,7 @@ from remove_ai_watermarks._internal.c2pa import (
|
||||
cbor_text_after,
|
||||
extract_c2pa_info,
|
||||
soft_binding_vendors_in,
|
||||
synthid_vendors_in,
|
||||
synthid_evidence_vendors_in,
|
||||
synthid_verdict,
|
||||
)
|
||||
from remove_ai_watermarks._internal.constants import (
|
||||
@@ -110,12 +110,8 @@ _STRIP_CAVEAT = (
|
||||
"text chunks are stripped by re-encoding, screenshots, or social-media upload."
|
||||
)
|
||||
_SYNTHID_CAVEAT = (
|
||||
"SynthID is a metadata proxy here; the pixel watermark is not locally "
|
||||
"verifiable (proprietary decoder). Confirm via the Gemini app or openai.com/verify."
|
||||
)
|
||||
_OPENAI_CAVEAT = (
|
||||
"OpenAI began pairing SynthID with C2PA around 2026-05; OpenAI images from "
|
||||
"before the rollout carry C2PA without SynthID, so the SynthID verdict is 'likely'."
|
||||
"SynthID presence comes from supported provenance here; the pixel watermark is not locally "
|
||||
"decoded (proprietary decoder). Confirm via the Gemini app or openai.com/verify."
|
||||
)
|
||||
_IPTC_ONLY_CAVEAT = "The IPTC 'Made with AI' tag flags AI provenance but does not identify the specific platform."
|
||||
_INVISIBLE_WM_CAVEAT = (
|
||||
@@ -451,7 +447,7 @@ class ProvenanceReport:
|
||||
# signal: AIGC, local gen params, xAI, ...).
|
||||
ai_source_kind: str | None = None
|
||||
# True when the AI verdict rests on a metadata or embedded-invisible signal
|
||||
# (C2PA AI issuer / SynthID proxy, IPTC, AIGC, local gen params, EXIF/xAI, or
|
||||
# (C2PA AI issuer / SynthID provenance, IPTC, AIGC, local gen params, EXIF/xAI, or
|
||||
# an open DWT-DCT / TrustMark decode) -- as opposed to a visible mark or a
|
||||
# weak medium-confidence hint (hf-job, Samsung genAIType). It is exactly the
|
||||
# set of signals an invisible/diffusion scrub targets: a visible-only or
|
||||
@@ -697,7 +693,7 @@ def _attribute_platform(issuers: list[str], *, is_ai: bool = True) -> str | None
|
||||
|
||||
# Coarse origin-vendor normalization for integrity-clash detection. Two signals
|
||||
# that resolve to the SAME key are consistent (a C2PA "Google (Gemini)" issuer
|
||||
# and a SynthID-Google proxy, or Adobe Firefly + its Adobe TrustMark soft
|
||||
# and Google SynthID provenance, or Adobe Firefly + its Adobe TrustMark soft
|
||||
# binding); two DIFFERENT keys from independent generator stamps are a
|
||||
# contradiction (a C2PA OpenAI manifest on an image whose EXIF says "Ideogram
|
||||
# AI"). Substring match on the lowercased platform/detail string; first hit wins,
|
||||
@@ -755,14 +751,14 @@ def _vendor_of(text: str | None) -> str | None:
|
||||
|
||||
# Clash-detection provenance sources. Rule 1 (below) flags two AI vendors only
|
||||
# when they come from *independent* signals. The C2PA issuer attribution and the
|
||||
# SynthID proxy are NOT independent -- the proxy is inferred from the same C2PA
|
||||
# SynthID evidence are NOT independent -- both are read from the same C2PA
|
||||
# manifest -- so they share one source. A multi-actor manifest (a product wrapping
|
||||
# another vendor's engine, e.g. Microsoft+OpenAI or Microsoft+Google; or an edit
|
||||
# chain like Adobe over a Gemini original) legitimately names several vendors in
|
||||
# one valid chain and must not read as spoofing. Families not listed here are each
|
||||
# their own independent source (EXIF/XMP generator, IPTC AISystemUsed, AIGC, ...).
|
||||
# The single C2PA-manifest source shared by the issuer attribution and the SynthID
|
||||
# proxy (both inferred from the same embedded manifest). Rule 2 keys off it too:
|
||||
# evidence (both read from the same embedded manifest). Rule 2 keys off it too:
|
||||
# the camera device label is read from this manifest, so an AI marker is a clash
|
||||
# only when its source differs from this (i.e. it is genuinely independent).
|
||||
_C2PA_MANIFEST_SOURCE = "c2pa_manifest"
|
||||
@@ -788,7 +784,7 @@ def _integrity_clashes(
|
||||
Args:
|
||||
ai_vendors: family name -> normalized AI-origin vendor, one entry per
|
||||
generator-stamped signal (C2PA issuer when the source is AI, SynthID
|
||||
proxy, EXIF/XMP generator tag, IPTC AISystemUsed, xAI, AIGC label).
|
||||
provenance, EXIF/XMP generator tag, IPTC AISystemUsed, xAI, AIGC label).
|
||||
camera_label: a camera/verified-capture C2PA device platform, if one was
|
||||
identified (Pixel, Leica, Sony, Nikon, Truepic), else None.
|
||||
camera_has_ai_marker: True when an AI-generation stamp coexists with the
|
||||
@@ -802,7 +798,7 @@ def _integrity_clashes(
|
||||
# Rule 1: two genuinely INDEPENDENT signals naming different AI vendors. Two
|
||||
# families clash only when they belong to different provenance sources (see
|
||||
# _CLASH_SOURCE) AND name different vendors -- so multiple vendors named within
|
||||
# one C2PA manifest (c2pa issuer + synthid proxy) do not flag.
|
||||
# one C2PA manifest (C2PA issuer + SynthID provenance) do not flag.
|
||||
# The generic TC260 AIGC label is a Chinese regulatory "this is AI" stamp. When a
|
||||
# Chinese TC260-applying vendor (ByteDance) is ALSO attributed, the label is that
|
||||
# vendor's own stamp on its own output, so attribute it to that vendor -- a legit
|
||||
@@ -835,7 +831,7 @@ def _integrity_clashes(
|
||||
# a contradiction. A device that both captures and runs on-device generative
|
||||
# AI (Google Pixel Magic Editor / Pixel Studio) records the capture and the
|
||||
# AI edit in ONE manifest, so the AI vendor is named only from that same
|
||||
# manifest (c2pa issuer + synthid proxy) -- a legitimate edit chain, not a
|
||||
# manifest (C2PA issuer + SynthID provenance) -- a legitimate edit chain, not a
|
||||
# spoof. An EXIF/XMP generator, IPTC field, TC260 AIGC label, or second
|
||||
# manifest naming AI on a camera capture is the real laundering tell.
|
||||
independent_ai_marker = any(grp != _C2PA_MANIFEST_SOURCE for grp in source.values())
|
||||
@@ -1134,7 +1130,7 @@ def _identify_from_evidence(
|
||||
if platform is None:
|
||||
platform = f"C2PA signer: {cloud_vendor} (cloud manifest)"
|
||||
|
||||
# ── SynthID metadata proxy ──────────────────────────────────────
|
||||
# ── SynthID provenance evidence ─────────────────────────────────
|
||||
# Structured first (the PNG caBX parser and the manifest store both fill
|
||||
# `synthid_watermark`), then the byte scan for the containers that keep the
|
||||
# manifest where no parser reaches it.
|
||||
@@ -1151,13 +1147,11 @@ 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_vendors_in(region)):
|
||||
if not synthid and trained_source and c2pa_marker_in(head) and (vendors := synthid_evidence_vendors_in(region)):
|
||||
synthid = synthid_verdict(", ".join(vendors))
|
||||
if synthid:
|
||||
watermarks.append(f"SynthID watermark, inferred from C2PA metadata ({synthid})")
|
||||
watermarks.append(f"SynthID watermark ({synthid})")
|
||||
caveats.append(_SYNTHID_CAVEAT)
|
||||
if _vendor_of(synthid) == "OpenAI":
|
||||
caveats.append(_OPENAI_CAVEAT)
|
||||
if v := _vendor_of(synthid):
|
||||
ai_vendor_claims["synthid"] = v
|
||||
|
||||
@@ -1423,7 +1417,7 @@ 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 proxy, IPTC, AIGC, local
|
||||
``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: SynthID is detectable
|
||||
|
||||
@@ -787,18 +787,18 @@ def _iptc_ai_system_impl(image_path: Path) -> str | None:
|
||||
|
||||
|
||||
def synthid_source(image_path: Path) -> str | None:
|
||||
"""Return the vendor name(s) if the image carries a SynthID pixel watermark.
|
||||
"""Return the vendor name(s) when provenance establishes SynthID.
|
||||
|
||||
This is a *metadata-based* proxy: Google (Imagen/Gemini) and OpenAI
|
||||
(ChatGPT/DALL-E/gpt-image) embed an invisible SynthID watermark alongside
|
||||
a C2PA manifest, so a C2PA manifest signed by one of them on AI-generated
|
||||
content implies SynthID in the pixels. Adobe Firefly / Microsoft Designer
|
||||
sign C2PA but do not use SynthID, so they return None.
|
||||
This is provenance-based, not a local pixel decode. Google states that all
|
||||
media generated by its tools carries SynthID, so Google AI C2PA establishes
|
||||
the mark. OpenAI C2PA existed before OpenAI adopted SynthID, so OpenAI also
|
||||
requires the explicit ``c2pa.watermarked.*`` action used by current manifests.
|
||||
Adobe Firefly and Microsoft sign C2PA but do not use SynthID, so they return
|
||||
None.
|
||||
|
||||
The verdict is reliable only while the C2PA manifest is intact -- absence
|
||||
is not proof, because C2PA can be stripped while the pixel watermark
|
||||
survives, and the pixel watermark itself is not locally detectable
|
||||
(proprietary decoder).
|
||||
The evidence is readable only while the C2PA manifest is intact. Absence is
|
||||
not proof: C2PA can be stripped while the pixel watermark survives, and the
|
||||
pixel watermark itself is not locally detectable (proprietary decoder).
|
||||
|
||||
Args:
|
||||
image_path: Path to the image (PNG, JPEG, WebP, or ISOBMFF container).
|
||||
@@ -806,7 +806,7 @@ def synthid_source(image_path: Path) -> str | None:
|
||||
Returns:
|
||||
Comma-joined vendor name(s) (e.g. ``"OpenAI"``) or None.
|
||||
"""
|
||||
from remove_ai_watermarks._internal.c2pa import extract_c2pa_info, synthid_vendors_in
|
||||
from remove_ai_watermarks._internal.c2pa import extract_c2pa_info, synthid_evidence_vendors_in
|
||||
|
||||
# PNG: the caBX chunk parser gives a clean, structured issuer.
|
||||
vendors = extract_c2pa_info(image_path).get("synthid_vendors")
|
||||
@@ -822,7 +822,7 @@ def synthid_source(image_path: Path) -> str | None:
|
||||
ai_source = b"trainedAlgorithmicMedia" in data or b"TrainedAlgorithmicMedia" in data
|
||||
if not (has_c2pa and ai_source):
|
||||
return None
|
||||
matched = synthid_vendors_in(data)
|
||||
matched = synthid_evidence_vendors_in(data)
|
||||
return ", ".join(matched) if matched else None
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -398,7 +398,7 @@ class TestInvisibleCommand:
|
||||
"""P0#5: when no invisible AI watermark is locally detectable, the diffusion
|
||||
scrub must NOT run (it would only degrade a clean image). Mirrors the visible
|
||||
no-mark contract: write no output, exit 2, and DO NOT imply the image is
|
||||
clean (a stripped SynthID proxy is not proof of absence)."""
|
||||
clean (stripped SynthID provenance is not proof of absence)."""
|
||||
mock_cls, mock_engine = _mock_invisible_engine()
|
||||
output = tmp_path / "clean.png"
|
||||
with (
|
||||
|
||||
+36
-18
@@ -284,7 +284,10 @@ class TestIdentifyNonPng:
|
||||
assert "Imagen" in c2pa_signal.detail
|
||||
|
||||
def test_openai_jpeg_has_synthid(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, b"OpenAI DALL-E ... trainedAlgorithmicMedia")
|
||||
path = self._c2pa_jpeg(
|
||||
tmp_path,
|
||||
b"OpenAI DALL-E ... trainedAlgorithmicMedia ... c2pa.watermarked.unbound",
|
||||
)
|
||||
r = identify(path, check_visible=False)
|
||||
assert any("SynthID" in w for w in r.watermarks)
|
||||
|
||||
@@ -446,7 +449,7 @@ class TestIdentifyRealSamples:
|
||||
assert r.platform
|
||||
assert "OpenAI" in r.platform
|
||||
assert any("C2PA" in w for w in r.watermarks)
|
||||
assert any("SynthID" in w for w in r.watermarks)
|
||||
assert not any("SynthID" in w for w in r.watermarks)
|
||||
|
||||
def test_adobe_firefly_has_no_synthid(self):
|
||||
r = identify(SAMPLES_DIR / "firefly-1.png", check_visible=False)
|
||||
@@ -853,24 +856,25 @@ class TestIdentifyVisibleTextMarks:
|
||||
|
||||
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
|
||||
class TestIdentifyCaveats:
|
||||
def test_openai_hedge_caveat_present(self):
|
||||
def test_legacy_openai_has_no_synthid_claim(self):
|
||||
r = identify(SAMPLES_DIR / "chatgpt-1.png", check_visible=False)
|
||||
assert any("before the rollout" in c for c in r.caveats)
|
||||
assert not any("SynthID" in watermark for watermark in r.watermarks)
|
||||
assert not any("before the rollout" in c for c in r.caveats)
|
||||
|
||||
def test_synthid_proxy_caveat_present(self):
|
||||
def test_legacy_openai_has_no_synthid_proxy_caveat(self):
|
||||
r = identify(SAMPLES_DIR / "chatgpt-1.png", check_visible=False)
|
||||
assert any("not locally" in c for c in r.caveats)
|
||||
assert not any("not locally" in c for c in r.caveats)
|
||||
|
||||
def test_caveats_are_deduplicated(self):
|
||||
r = identify(SAMPLES_DIR / "chatgpt-1.png", check_visible=False)
|
||||
assert len(r.caveats) == len(set(r.caveats))
|
||||
|
||||
|
||||
class TestOpenAiCaveatVendorScoped:
|
||||
"""The OpenAI rollout caveat keys on the normalized SynthID vendor, not a raw
|
||||
"OpenAI" substring over the issuer + verdict blob -- so a Google-SynthID
|
||||
manifest with an incidental "OpenAI" byte elsewhere is not mislabeled, while
|
||||
a genuine OpenAI manifest still gets the hedge.
|
||||
class TestSynthIDProvenanceEvidence:
|
||||
"""Google's provider policy and OpenAI's explicit watermark action are evidence.
|
||||
|
||||
A bare legacy OpenAI C2PA manifest is not: those credentials existed before
|
||||
OpenAI adopted SynthID.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -906,14 +910,28 @@ class TestOpenAiCaveatVendorScoped:
|
||||
self._png_chunk(b"tEXt", b"note\x00signed via OpenAI trust chain"),
|
||||
)
|
||||
r = identify(png, check_visible=False, check_invisible=False)
|
||||
assert any("SynthID watermark, inferred from C2PA metadata (likely present (Google" in w for w in r.watermarks)
|
||||
assert any("SynthID watermark (present according to Google" in w for w in r.watermarks)
|
||||
assert not any("before the rollout" in c for c in r.caveats)
|
||||
|
||||
def test_openai_synthid_still_gets_caveat(self, tmp_path: Path):
|
||||
png = self._png(tmp_path, "oa.png", self._png_chunk(b"caBX", b"jumbc2pa OpenAI ... trainedAlgorithmicMedia"))
|
||||
def test_openai_watermark_action_asserts_synthid(self, tmp_path: Path):
|
||||
png = self._png(
|
||||
tmp_path,
|
||||
"oa.png",
|
||||
self._png_chunk(
|
||||
b"caBX",
|
||||
b"jumbc2pa OpenAI ... trainedAlgorithmicMedia ... c2pa.watermarked.unbound",
|
||||
),
|
||||
)
|
||||
r = identify(png, check_visible=False, check_invisible=False)
|
||||
assert any("SynthID watermark, inferred from C2PA metadata (likely present (OpenAI" in w for w in r.watermarks)
|
||||
assert any("before the rollout" in c for c in r.caveats)
|
||||
assert any("SynthID watermark (present according to OpenAI" in w for w in r.watermarks)
|
||||
assert not any("before the rollout" in c for c in r.caveats)
|
||||
|
||||
def test_legacy_openai_without_watermark_action_does_not_assert_synthid(self, tmp_path: Path):
|
||||
png = self._png(
|
||||
tmp_path, "oa-old.png", self._png_chunk(b"caBX", b"jumbc2pa OpenAI ... trainedAlgorithmicMedia")
|
||||
)
|
||||
r = identify(png, check_visible=False, check_invisible=False)
|
||||
assert not any("SynthID" in watermark for watermark in r.watermarks)
|
||||
|
||||
def test_dreamina_png_cabx_without_source_type(self, tmp_path: Path):
|
||||
# Real Dreamina PNGs carry the "Dreamina/x.y" generator in an ingredient
|
||||
@@ -1160,13 +1178,13 @@ class TestIntegrityClashesHelper:
|
||||
assert "Ideogram" in clashes[0]
|
||||
|
||||
def test_same_vendor_two_signals_no_clash(self):
|
||||
# C2PA Google + SynthID-Google proxy is consistent, not a contradiction.
|
||||
# C2PA Google + Google SynthID provenance is consistent, not a contradiction.
|
||||
assert _integrity_clashes({"c2pa": "Google", "synthid": "Google"}, None, camera_has_ai_marker=True) == []
|
||||
|
||||
def test_multi_actor_manifest_no_clash(self):
|
||||
# A multi-actor C2PA manifest names a product + the engine it wraps in ONE
|
||||
# valid chain (Microsoft Designer on OpenAI, Microsoft on Google, Adobe over
|
||||
# a Gemini original). The c2pa issuer attribution and the SynthID proxy share
|
||||
# a Gemini original). The C2PA issuer attribution and SynthID provenance share
|
||||
# the same manifest source, so the differing vendors must NOT read as a clash.
|
||||
for c2pa_vendor, synthid_vendor in (("Microsoft", "OpenAI"), ("Microsoft", "Google"), ("Adobe", "Google")):
|
||||
assert (
|
||||
|
||||
+29
-15
@@ -497,7 +497,7 @@ class TestGetAiMetadataRealSample:
|
||||
meta = get_ai_metadata(SAMPLES_DIR / "chatgpt-1.png")
|
||||
assert "claim_generator" in meta
|
||||
assert "OpenAI" in meta["issuer"]
|
||||
assert "OpenAI" in meta["synthid_watermark"]
|
||||
assert "synthid_watermark" not in meta
|
||||
assert "trainedAlgorithmicMedia" in meta["source_type"]
|
||||
|
||||
|
||||
@@ -538,20 +538,18 @@ def test_bare_algorithmic_media_not_flagged_ai(tmp_path: Path):
|
||||
class TestSynthIDSource:
|
||||
"""SynthID detection via the C2PA companion manifest.
|
||||
|
||||
Google (Imagen/Gemini) and OpenAI (ChatGPT/DALL-E/gpt-image) pair an
|
||||
invisible SynthID pixel watermark with a C2PA manifest. Adobe Firefly and
|
||||
Microsoft Designer sign C2PA Content Credentials but do NOT use SynthID,
|
||||
so the discriminating signal is the C2PA *issuer*, not the mere presence
|
||||
of a manifest. These tests run against real, committed sample images.
|
||||
Google provenance implies SynthID under Google's all-media policy. OpenAI
|
||||
provenance requires the explicit C2PA watermark action: older ChatGPT
|
||||
Content Credentials predate SynthID and must not be upgraded into a pixel
|
||||
watermark claim merely because the issuer is OpenAI.
|
||||
"""
|
||||
|
||||
def test_openai_chatgpt_is_synthid_source(self):
|
||||
assert synthid_source(SAMPLES_DIR / "chatgpt-1.png") == "OpenAI"
|
||||
def test_legacy_openai_chatgpt_is_not_synthid_source(self):
|
||||
assert synthid_source(SAMPLES_DIR / "chatgpt-1.png") is None
|
||||
|
||||
def test_openai_verdict_in_get_ai_metadata(self):
|
||||
def test_legacy_openai_verdict_absent_from_get_ai_metadata(self):
|
||||
meta = get_ai_metadata(SAMPLES_DIR / "chatgpt-1.png")
|
||||
assert "synthid_watermark" in meta
|
||||
assert "OpenAI" in meta["synthid_watermark"]
|
||||
assert "synthid_watermark" not in meta
|
||||
|
||||
def test_adobe_firefly_is_not_synthid_source(self):
|
||||
# Adobe signs C2PA (trainedAlgorithmicMedia) but embeds no SynthID.
|
||||
@@ -570,17 +568,33 @@ class TestSynthIDSourceNonPng:
|
||||
misses them. These use synthetic byte blobs (real fixtures aren't shipped).
|
||||
"""
|
||||
|
||||
def _c2pa_jpeg(self, tmp_path: Path, name: str, issuer: bytes, marker: bytes = b"trainedAlgorithmicMedia") -> Path:
|
||||
def _c2pa_jpeg(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
name: str,
|
||||
issuer: bytes,
|
||||
marker: bytes = b"trainedAlgorithmicMedia",
|
||||
action: bytes = b"",
|
||||
) -> Path:
|
||||
path = tmp_path / name
|
||||
# Minimal JPEG shell with an embedded C2PA-ish blob.
|
||||
blob = b"jumbc2pa" + issuer + b"..." + marker
|
||||
blob = b"jumbc2pa" + issuer + b"..." + marker + b"..." + action
|
||||
path.write_bytes(b"\xff\xd8\xff\xe1" + blob + b"\xff\xd9")
|
||||
return path
|
||||
|
||||
def test_openai_c2pa_in_jpeg(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, "chatgpt.jpg", b"OpenAI")
|
||||
def test_openai_watermark_action_in_jpeg(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(
|
||||
tmp_path,
|
||||
"chatgpt.jpg",
|
||||
b"OpenAI",
|
||||
action=b"c2pa.watermarked.unbound",
|
||||
)
|
||||
assert synthid_source(path) == "OpenAI"
|
||||
|
||||
def test_legacy_openai_c2pa_without_watermark_action_is_none(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, "legacy-chatgpt.jpg", b"OpenAI")
|
||||
assert synthid_source(path) is None
|
||||
|
||||
def test_google_c2pa_in_jpeg(self, tmp_path: Path):
|
||||
path = self._c2pa_jpeg(tmp_path, "gemini.jpg", b"Google")
|
||||
assert synthid_source(path) == "Google LLC"
|
||||
|
||||
@@ -155,6 +155,13 @@ class TestC2PA:
|
||||
|
||||
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
|
||||
CURRENT_OPENAI_SAMPLE = (
|
||||
Path(__file__).resolve().parent.parent
|
||||
/ "data"
|
||||
/ "synthid"
|
||||
/ "originals"
|
||||
/ "ChatGPT Image May 30, 2026, 10_31_08 AM.png"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
|
||||
@@ -173,7 +180,14 @@ class TestC2PARealSamples:
|
||||
# CBOR-clean claim generator, no regex artifacts (e.g. "fGPT-4o").
|
||||
assert info["claim_generator"]
|
||||
assert not info["claim_generator"].startswith("f")
|
||||
assert "synthid_watermark" in info
|
||||
assert "synthid_watermark" not in info
|
||||
|
||||
@pytest.mark.skipif(not CURRENT_OPENAI_SAMPLE.exists(), reason="current OpenAI SynthID fixture not present")
|
||||
def test_current_openai_watermark_action_asserts_synthid(self):
|
||||
info = extract_c2pa_info(CURRENT_OPENAI_SAMPLE)
|
||||
assert info["watermarked"] is True
|
||||
assert "watermarked.unbound" in info["actions"]
|
||||
assert "OpenAI" in info["synthid_watermark"]
|
||||
|
||||
def test_extract_info_adobe_has_no_synthid(self):
|
||||
info = extract_c2pa_info(SAMPLES_DIR / "firefly-1.png")
|
||||
@@ -222,7 +236,7 @@ class TestC2PARealSamples:
|
||||
assert info["c2pa_manifest"].startswith("C2PA manifest (") # chunk path
|
||||
assert "OpenAI" in info["issuer"]
|
||||
assert "trainedAlgorithmicMedia" in info["source_type"]
|
||||
assert "synthid_watermark" in info
|
||||
assert "synthid_watermark" not in info
|
||||
|
||||
|
||||
class TestC2PAInjectValidation:
|
||||
@@ -269,7 +283,7 @@ class TestCborTextAfter:
|
||||
|
||||
class TestSynthIDVerdict:
|
||||
def test_format(self):
|
||||
assert synthid_verdict("OpenAI") == "likely present (OpenAI embeds SynthID with C2PA)"
|
||||
assert synthid_verdict("OpenAI") == "present according to OpenAI provenance"
|
||||
|
||||
def test_multiple_vendors(self):
|
||||
assert "Google LLC, OpenAI" in synthid_verdict("Google LLC, OpenAI")
|
||||
@@ -285,7 +299,7 @@ class TestParseChunkGuards:
|
||||
|
||||
def test_clean_generator_kept(self):
|
||||
# "name" + CBOR text-string (head 0x69 = 0x60+9) "gpt-image"
|
||||
chunk = b"...name" + bytes([0x69]) + b"gpt-image" + b"OpenAI trainedAlgorithmicMedia"
|
||||
chunk = b"...name" + bytes([0x69]) + b"gpt-image" + b"OpenAI trainedAlgorithmicMedia c2pa.watermarked.unbound"
|
||||
info: dict = {}
|
||||
_parse_c2pa_chunk(chunk, info)
|
||||
assert info["claim_generator"] == "gpt-image"
|
||||
@@ -305,7 +319,7 @@ class TestC2PADigitalSourceType:
|
||||
"""The three IPTC digitalSourceType variants drive the AI verdict.
|
||||
|
||||
Only *trained* and *composite-with-trained* mean AI-generated (and so imply
|
||||
a SynthID proxy for a SynthID vendor); plain ``algorithmicMedia`` is
|
||||
SynthID provenance for a supported vendor); plain ``algorithmicMedia`` is
|
||||
procedural (not trained) and must NOT be flagged as AI.
|
||||
"""
|
||||
|
||||
@@ -317,7 +331,12 @@ class TestC2PADigitalSourceType:
|
||||
assert "synthid_watermark" not in info # procedural, not AI-generated
|
||||
|
||||
def test_composite_with_trained_is_ai_and_synthid(self):
|
||||
chunk = b"...name" + bytes([0x69]) + b"some-tool" + b" OpenAI compositeWithTrainedAlgorithmicMedia"
|
||||
chunk = (
|
||||
b"...name"
|
||||
+ bytes([0x69])
|
||||
+ b"some-tool"
|
||||
+ b" OpenAI compositeWithTrainedAlgorithmicMedia c2pa.watermarked.unbound"
|
||||
)
|
||||
info: dict = {}
|
||||
_parse_c2pa_chunk(chunk, info)
|
||||
assert "compositeWithTrainedAlgorithmicMedia" in info["source_type"]
|
||||
@@ -577,19 +596,19 @@ class TestC2paBufferScans:
|
||||
assert soft_binding_vendors_in(b"") == []
|
||||
assert soft_binding_vendors_in(b"no soft-binding assertion here") == []
|
||||
|
||||
def test_synthid_vendors_in_requires_synthid_issuer(self):
|
||||
from remove_ai_watermarks._internal.c2pa import C2PA_ISSUERS, SYNTHID_C2PA_ISSUERS, synthid_vendors_in
|
||||
def test_synthid_evidence_requires_openai_watermark_action_but_not_google_action(self):
|
||||
from remove_ai_watermarks._internal.c2pa import synthid_evidence_vendors_in
|
||||
|
||||
syn_sig = next(s for s in C2PA_ISSUERS if s in SYNTHID_C2PA_ISSUERS)
|
||||
non_sig = next(s for s in C2PA_ISSUERS if s not in SYNTHID_C2PA_ISSUERS)
|
||||
assert C2PA_ISSUERS[syn_sig] in synthid_vendors_in(b"x" + syn_sig + b"x")
|
||||
# an issuer that does NOT pair SynthID with C2PA must not be reported as one
|
||||
assert C2PA_ISSUERS[non_sig] not in synthid_vendors_in(b"x" + non_sig + b"x")
|
||||
assert synthid_evidence_vendors_in(b"c2pa OpenAI trainedAlgorithmicMedia") == []
|
||||
assert synthid_evidence_vendors_in(b"c2pa OpenAI trainedAlgorithmicMedia c2pa.watermarked.unbound") == [
|
||||
"OpenAI"
|
||||
]
|
||||
assert synthid_evidence_vendors_in(b"c2pa Google trainedAlgorithmicMedia") == ["Google LLC"]
|
||||
|
||||
def test_synthid_verdict_format(self):
|
||||
from remove_ai_watermarks._internal.c2pa import synthid_verdict
|
||||
|
||||
assert synthid_verdict("Google LLC") == "likely present (Google LLC embeds SynthID with C2PA)"
|
||||
assert synthid_verdict("Google LLC") == "present according to Google LLC provenance"
|
||||
|
||||
|
||||
def _amf0_str(value: bytes, *, long: bool = False) -> bytes:
|
||||
|
||||
@@ -247,7 +247,7 @@ class TestResolveStrength:
|
||||
|
||||
|
||||
class TestVendorForStrength:
|
||||
"""vendor_for_strength normalizes the C2PA SynthID proxy to openai/google/None."""
|
||||
"""vendor_for_strength normalizes SynthID provenance to openai/google/None."""
|
||||
|
||||
@staticmethod
|
||||
def _patch(value):
|
||||
|
||||
@@ -18,6 +18,7 @@ import synthid_corpus
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
|
||||
CORPUS_DIR = Path(__file__).resolve().parent.parent / "data" / "synthid"
|
||||
QUALITY_SET = CORPUS_DIR / "full-pipeline-quality.csv"
|
||||
CURRENT_OPENAI_SAMPLE = CORPUS_DIR / "originals" / "ChatGPT Image May 30, 2026, 10_31_08 AM.png"
|
||||
|
||||
EXPECTED_QUALITY_SOURCE_FILENAMES = {
|
||||
"ChatGPT Image May 30, 2026, 10_31_08 AM.png",
|
||||
@@ -57,11 +58,12 @@ def test_manifest_matches_canonical_originals() -> None:
|
||||
|
||||
@pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/fixtures/provenance not present")
|
||||
class TestIngest:
|
||||
def test_ingest_openai_flags_synthid_metadata(self, tmp_path: Path):
|
||||
@pytest.mark.skipif(not CURRENT_OPENAI_SAMPLE.exists(), reason="current OpenAI SynthID fixture not present")
|
||||
def test_ingest_current_openai_flags_synthid_metadata(self, tmp_path: Path):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
synthid_corpus.cli,
|
||||
["ingest", str(SAMPLES_DIR / "chatgpt-1.png"), "--label", "pos", "--root", str(tmp_path)],
|
||||
["ingest", str(CURRENT_OPENAI_SAMPLE), "--label", "pos", "--root", str(tmp_path)],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -72,7 +74,7 @@ class TestIngest:
|
||||
assert row["synthid_metadata"] == "yes"
|
||||
assert int(row["width"]) > 0
|
||||
assert int(row["height"]) > 0
|
||||
assert row["filename"] == "chatgpt-1.png"
|
||||
assert row["filename"] == CURRENT_OPENAI_SAMPLE.name
|
||||
# Every label shares one canonical originals/ directory.
|
||||
assert (tmp_path / "originals" / row["filename"]).exists()
|
||||
|
||||
|
||||
+19
-4
@@ -37,7 +37,7 @@ def _box(box_type: bytes, payload: bytes) -> bytes:
|
||||
|
||||
|
||||
def _video_with_c2pa(path: Path) -> Path:
|
||||
manifest = C2PA_UUID + b"OpenAI trainedAlgorithmicMedia"
|
||||
manifest = C2PA_UUID + b"OpenAI trainedAlgorithmicMedia c2pa.watermarked.unbound"
|
||||
path.write_bytes(_MP4_FTYP + _box(b"uuid", manifest) + _box(b"mdat", _VIDEO_PAYLOAD))
|
||||
return path
|
||||
|
||||
@@ -277,7 +277,12 @@ def _write_synthetic_sora_clip(
|
||||
check=True,
|
||||
)
|
||||
with path.open("ab") as stream:
|
||||
stream.write(_box(b"uuid", C2PA_UUID + b"OpenAI Sora trainedAlgorithmicMedia"))
|
||||
stream.write(
|
||||
_box(
|
||||
b"uuid",
|
||||
C2PA_UUID + b"OpenAI Sora trainedAlgorithmicMedia c2pa.watermarked.unbound",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _write_vfr_sora_clip(
|
||||
@@ -345,7 +350,12 @@ def _write_vfr_sora_clip(
|
||||
command.append(str(path))
|
||||
subprocess.run(command, capture_output=True, check=True) # noqa: S603
|
||||
with path.open("ab") as stream:
|
||||
stream.write(_box(b"uuid", C2PA_UUID + b"OpenAI Sora trainedAlgorithmicMedia"))
|
||||
stream.write(
|
||||
_box(
|
||||
b"uuid",
|
||||
C2PA_UUID + b"OpenAI Sora trainedAlgorithmicMedia c2pa.watermarked.unbound",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _decode_video(path: Path) -> tuple[list[np.ndarray], float]:
|
||||
@@ -612,7 +622,12 @@ class TestVideoMetadataApi:
|
||||
media_payload = b"x" * (8 * 1024 * 1024)
|
||||
source = _video_with_tc260(tmp_path / "source.mp4", media_payload=media_payload)
|
||||
with source.open("ab") as stream:
|
||||
stream.write(_box(b"uuid", C2PA_UUID + b"OpenAI trainedAlgorithmicMedia"))
|
||||
stream.write(
|
||||
_box(
|
||||
b"uuid",
|
||||
C2PA_UUID + b"OpenAI trainedAlgorithmicMedia c2pa.watermarked.unbound",
|
||||
)
|
||||
)
|
||||
output = tmp_path / "clean.mp4"
|
||||
source_size = source.stat().st_size
|
||||
source_media_digest = hashlib.sha256(media_payload).digest()
|
||||
|
||||
Reference in New Issue
Block a user