feat(identify): close 3 detector gaps found on the spaces corpus (06-05..06-11)

- AIGC: parse the bare ``AIGC{...}`` blob form (label glued to its JSON in a
  JPEG APP segment near the JFIF header), and scan both raw-JSON forms in one
  fall-through loop so a quoted ``"AIGC"`` later in an XMP packet no longer
  shadows a real bare label earlier in the file (3 files read unknown before).
- Integrity clash rule 2: a camera device + an AI marker from the SAME C2PA
  manifest (Google Pixel Magic Editor / Pixel Studio edit chain) is a legitimate
  edit chain, not a contradiction. Fire only when the AI marker's source is
  independent of the camera's manifest; pure cameras (Leica/Sony/Nikon) are
  unaffected (2 Pixel files mis-flagged before).
- New c2pa_cloud_manifest detector: surface a C2PA 2.4 Durable Content
  Credentials cloud-manifest reference (Adobe cai-manifests.adobe.com) as a
  medium provenance signal when the embedded manifest is stripped. Provenance
  only, never asserts is_ai (2 files read fully unknown before).

identify reuses its already-loaded scan head for the cloud check (no second
read). +7 tests; CLAUDE.md + README synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-06-11 09:28:15 -07:00
co-authored by Claude Fable 5
parent 22bc171806
commit ad7e4ee08b
6 changed files with 241 additions and 26 deletions
+41
View File
@@ -772,6 +772,31 @@ class TestIntegrityClashesHelper:
# must NOT raise a clash.
assert _integrity_clashes({}, "Leica (camera, C2PA capture)", camera_has_ai_marker=False) == []
def test_pixel_generative_edit_same_manifest_no_clash(self):
# A Google Pixel that BOTH captures and runs on-device generative AI
# (Magic Editor / Pixel Studio) records the capture and the AI edit in
# ONE C2PA manifest -- the AI vendor is named only from that same
# manifest (c2pa / synthid), independent of nothing. That is a legitimate
# edit chain, NOT a camera-vs-AI contradiction, so rule 2 must stay quiet.
assert (
_integrity_clashes(
{"c2pa": "Google", "synthid": "Google"},
"Google Pixel (camera, C2PA capture)",
camera_has_ai_marker=True,
)
== []
)
def test_camera_plus_independent_ai_marker_still_clashes(self):
# But a camera capture next to an AI marker from a genuinely INDEPENDENT
# source (EXIF/XMP generator, TC260 AIGC, ...) is still a laundering tell.
clashes = _integrity_clashes(
{"c2pa": "Google", "aigc": "China AIGC (TC260)"},
"Google Pixel (camera, C2PA capture)",
camera_has_ai_marker=True,
)
assert any("Camera-capture" in c for c in clashes)
class TestIntegrityClashEndToEnd:
def _c2pa_jpeg(self, tmp_path: Path, blob: bytes) -> Path:
@@ -806,6 +831,22 @@ class TestIntegrityClashEndToEnd:
assert r.platform == "Google Pixel (camera, C2PA capture)"
assert any("Camera-capture C2PA credentials" in c and "AI-generation markers" in c for c in r.integrity_clashes)
def test_pixel_generative_edit_no_clash(self, tmp_path: Path):
# A real Google Pixel generative edit (Magic Editor / Pixel Studio) signs
# ONE manifest carrying both the Pixel Camera capture and a Google
# Generative AI edit (trainedAlgorithmicMedia + "Applied imperceptible
# SynthID watermark"). The AI marker lives in the SAME manifest as the
# device, so it is an edit chain, not a camera-vs-AI contradiction.
path = self._c2pa_jpeg(
tmp_path,
b"Pixel Camera ... Created by Pixel Camera ... computationalCapture ... "
b"Created by Google Generative AI ... trainedAlgorithmicMedia ... "
b"Applied imperceptible SynthID watermark",
)
r = identify(path, check_visible=False, check_invisible=False)
assert r.is_ai_generated is True
assert r.integrity_clashes == []
def test_clash_serializes_to_json(self, tmp_path: Path):
path = self._c2pa_jpeg(tmp_path, b"OpenAI ... trainedAlgorithmicMedia ... TC260:AIGC label")
r = identify(path, check_visible=False, check_invisible=False)
+82
View File
@@ -790,6 +790,42 @@ class TestAIGCLabel:
def test_has_ai_metadata_detects_raw_json_exif_form(self, tmp_path: Path):
assert has_ai_metadata(self._aigc_exif_jpeg(tmp_path))
def _aigc_bare_jpeg(self, tmp_path: Path, producer: str = "00119144030008867405X210002") -> Path:
"""Some China-served generators glue the TC260 label straight to its JSON
as a bare ``AIGC{...}`` blob inside a JPEG APP segment (no ``"AIGC":``
key wrapper, no PNG chunk, no namespaced XMP) -- seen near the JFIF
header on real 2026-06 downloads."""
p = tmp_path / "aigc_bare.jpg"
Image.new("RGB", (32, 32)).save(p)
raw = p.read_bytes()
blob = b'AIGC{"Label":"1","ContentProducer":"' + producer.encode() + b'","ProduceID":"8F995586"}'
segment = b"\xff\xe9" + (len(blob) + 2).to_bytes(2, "big") + blob # APP9
p.write_bytes(raw[:2] + segment + raw[2:]) # splice after SOI
return p
def test_parses_bare_aigc_jpeg_segment_form(self, tmp_path: Path):
from remove_ai_watermarks.metadata import aigc_label
info = aigc_label(self._aigc_bare_jpeg(tmp_path))
assert info is not None
assert info["Label"] == "1"
assert info["ContentProducer"] == "00119144030008867405X210002"
def test_has_ai_metadata_detects_bare_aigc_jpeg_form(self, tmp_path: Path):
assert has_ai_metadata(self._aigc_bare_jpeg(tmp_path))
def test_bare_aigc_without_tc260_field_ignored(self, tmp_path: Path):
"""A bare ``AIGC{...}`` blob with no TC260 field must not false-positive."""
from remove_ai_watermarks.metadata import aigc_label
p = tmp_path / "bare_unrelated.jpg"
Image.new("RGB", (32, 32)).save(p)
raw = p.read_bytes()
blob = b'AIGC{"unrelated":"value"}'
segment = b"\xff\xe9" + (len(blob) + 2).to_bytes(2, "big") + blob
p.write_bytes(raw[:2] + segment + raw[2:])
assert aigc_label(p) is None
def test_raw_json_without_tc260_field_ignored(self, tmp_path: Path):
"""A bare ``{"AIGC":{...}}`` object with no TC260 field must not fire."""
import json
@@ -1185,3 +1221,49 @@ class TestFfmpegMetadataStrip:
remove_ai_metadata(src, out)
assert out.exists()
assert b"Suno AI generated" not in out.read_bytes() # tag stripped, audio kept
class TestC2paCloudManifest:
"""C2PA 2.4 Durable Content Credentials: an XMP dcterms:provenance pointer to
a vendor cloud manifest store survives when the embedded manifest is stripped."""
def _cloud_png(self, tmp_path: Path, host: bytes = b"cai-manifests.adobe.com") -> Path:
xmp = (
b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/">'
b'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
b'<rdf:Description rdf:about="" xmlns:dcterms="http://purl.org/dc/terms/" '
b'dcterms:provenance="https://' + host + b'/manifests/urn-c2pa-abc123"> </rdf:Description>'
b'</rdf:RDF></x:xmpmeta><?xpacket end="w"?>'
)
p = tmp_path / "cloud.png"
img = Image.new("RGB", (16, 16))
meta = PngInfo()
meta.add_itxt("XML:com.adobe.xmp", xmp.decode("latin-1"))
img.save(p, pnginfo=meta)
return p
def test_detects_adobe_cloud_manifest(self, tmp_path: Path):
from remove_ai_watermarks.metadata import c2pa_cloud_manifest
assert c2pa_cloud_manifest(self._cloud_png(tmp_path)) == "Adobe Content Authenticity"
def test_no_provenance_pointer_is_none(self, tmp_clean_png: Path):
from remove_ai_watermarks.metadata import c2pa_cloud_manifest
assert c2pa_cloud_manifest(tmp_clean_png) is None
def test_unknown_host_is_none(self, tmp_path: Path):
from remove_ai_watermarks.metadata import c2pa_cloud_manifest
# A dcterms:provenance pointer to an unrecognized host is not attributed.
assert c2pa_cloud_manifest(self._cloud_png(tmp_path, host=b"manifests.example.com")) is None
def test_cloud_manifest_does_not_assert_ai(self, tmp_path: Path):
# Provenance only -- a cloud manifest can describe a human edit, so the
# verdict must stay 'unknown', not 'AI-generated'.
from remove_ai_watermarks.identify import identify
r = identify(self._cloud_png(tmp_path), check_visible=False, check_invisible=False)
assert r.is_ai_generated is None
assert any("Durable Content Credentials" in w for w in r.watermarks)
assert any(s.name == "c2pa_cloud" for s in r.signals)