From c11181b9b5800979ebad8cd98c9082072091756f Mon Sep 17 00:00:00 2001 From: Victor Kuznetsov Date: Thu, 23 Jul 2026 16:15:48 -0700 Subject: [PATCH] Detect and attribute Bria AI, fal.ai, and Apple Photos Clean Up Corpus-mined vendor gaps (42k-file metadata scan, 2026-07-23): - Bria AI signs C2PA as "Bria Artificial Intelligence" with source type empty (no trainedAlgorithmicMedia), so identify was completely blind; registered with asserts_ai like Dreamina. - fal.ai ("fal - Features & Labels Inc.", fal-ai/ generators) was detected via the source type but never attributed; registered, also asserts_ai as a pure generative platform. - Apple Photos Clean Up (Apple Intelligence object removal) was detected as a generic made-with-AI tag; now attributed as an AI edit via the photoshop:Credit marker, and the credit value joins AI_GENERATOR_TOKENS so removal strips it in parity. Each new test was verified red without its fix. --- src/remove_ai_watermarks/identify.py | 13 ++++++++- src/remove_ai_watermarks/noai/constants.py | 21 +++++++++++++++ tests/test_identify.py | 31 ++++++++++++++++++++++ tests/test_metadata.py | 17 ++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/remove_ai_watermarks/identify.py b/src/remove_ai_watermarks/identify.py index e0bd2fe..e6e0739 100644 --- a/src/remove_ai_watermarks/identify.py +++ b/src/remove_ai_watermarks/identify.py @@ -312,6 +312,9 @@ _AI_VENDOR_TOKENS: tuple[tuple[str, str], ...] = ( ("elevenlabs", "ElevenLabs"), ("eleven labs", "ElevenLabs"), ("black forest", "Black Forest Labs"), + ("fal-ai", "fal.ai"), + ("bria", "Bria"), + ("apple photos clean up", "Apple"), ) @@ -673,7 +676,15 @@ def identify(image_path: Path, *, check_visible: bool = True, check_invisible: b watermarks.append("IPTC digitalSourceType (Made with AI)") caveats.append(_IPTC_ONLY_CAVEAT) if platform is None: - platform = "Made-with-AI tag (e.g. Meta AI); platform not specified" + # Apple Photos Clean Up (Apple Intelligence object removal) marks + # the edit with photoshop:Credit / IPTC "Apple Photos Clean Up" + # next to compositeWithTrainedAlgorithmicMedia -- corpus-measured + # 2026-07-23 (35 files); it was detected but never attributed. + platform = ( + "Apple Photos (Clean Up AI edit)" + if b"Apple Photos Clean Up" in head + else "Made-with-AI tag (e.g. Meta AI); platform not specified" + ) # ── IPTC 2025.1 AI-disclosure fields (Iptc4xmpExt:AISystemUsed etc.) ─ iptc_ai = any(m in head for m in IPTC_AI_FIELD_MARKERS) diff --git a/src/remove_ai_watermarks/noai/constants.py b/src/remove_ai_watermarks/noai/constants.py index 660d1b1..6ed2a55 100644 --- a/src/remove_ai_watermarks/noai/constants.py +++ b/src/remove_ai_watermarks/noai/constants.py @@ -184,6 +184,20 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = ( # manifest alone marks AI generation. Verified against the mined retained # corpus, 2026-06-20. ElevenLabs does not use SynthID. C2paAiVendor(b"Eleven Labs", "ElevenLabs", "ElevenLabs", "ElevenLabs"), + # fal.ai (generative inference platform, issuer "fal - Features & Labels + # Inc." / common name "fal.ai", claim generators like "fal-ai/seedvr", + # "fal-ai/gpt-image-2"). Corpus-measured 2026-07-23 on the retained + # uploads (17 files): the files carry trainedAlgorithmicMedia, so the + # verdict already fired, but the platform stayed unattributed. fal.ai is + # a pure generative platform, so ``asserts_ai`` also covers its output + # that omits the source-type. + C2paAiVendor(b"fal-ai", "fal.ai", "fal.ai", "fal.ai", asserts_ai=True), + # Bria AI (bria.ai, generative platform) signs as "Bria Artificial + # Intelligence" with a "Bria Ai" claim generator and source type + # ``empty`` (NOT trainedAlgorithmicMedia), so a real signed file was + # completely missed by identify -- corpus-found 2026-07-23. A pure-AI + # vendor with distinctive strings, so ``asserts_ai`` is safe here. + C2paAiVendor(b"Bria", "Bria Artificial Intelligence", "Bria AI", "Bria", asserts_ai=True), # Truepic is a C2PA signing authority, not an AI generator: no platform label, # never asserts is_ai (the verdict comes from the digital-source-type). C2paAiVendor(b"Truepic", "Truepic", None, None), @@ -307,6 +321,13 @@ AI_GENERATOR_TOKENS: frozenset[str] = frozenset( "novelai", "reve.com", "aphrodite ai", + # Corpus-mined 2026-07-23: + # - Apple Photos Clean Up (Apple Intelligence object removal): XMP + # photoshop:Credit / IPTC credit value; composite source-type + # covered detection, this token covers removal parity (35 files). + # - fal-ai: generative-platform generator string (17 files). + "apple photos clean up", + "fal-ai", } ) diff --git a/tests/test_identify.py b/tests/test_identify.py index 13e3d0c..78c8fd0 100644 --- a/tests/test_identify.py +++ b/tests/test_identify.py @@ -171,6 +171,23 @@ class TestIdentifyNonPng: assert r.platform == "ElevenLabs" assert not any("SynthID" in w for w in r.watermarks) # ElevenLabs does not use SynthID + def test_fal_ai_attributed(self, tmp_path: Path): + # fal.ai signs as "fal - Features & Labels Inc." with a "fal-ai/" + # claim generator; corpus-measured 2026-07-23 (17 files). + path = self._c2pa_jpeg(tmp_path, b"fal - Features & Labels Inc. fal-ai/seedvr trainedAlgorithmicMedia") + r = identify(path, check_visible=False, check_invisible=False) + assert r.is_ai_generated is True + assert r.platform == "fal.ai" + + def test_bria_attributed_without_source_type(self, tmp_path: Path): + # Bria signs as "Bria Artificial Intelligence" with source type ``empty`` + # (NO trainedAlgorithmicMedia) -- a pure-generator asserts_ai vendor, so + # the issuer/generator strings alone must flag AI. Corpus-found 2026-07-23. + path = self._c2pa_jpeg(tmp_path, b"Bria Artificial Intelligence Bria Ai c2pa.created c2pa.edited") + r = identify(path, check_visible=False, check_invisible=False) + assert r.is_ai_generated is True + assert r.platform == "Bria AI" + def test_stability_ai_issuer_attributed_no_synthid(self, tmp_path: Path): path = self._c2pa_jpeg(tmp_path, b"Stability AI ... trainedAlgorithmicMedia") r = identify(path, check_visible=False) @@ -285,6 +302,20 @@ class TestIdentifyRealSamples: assert r.is_ai_generated is True assert any("IPTC" in w for w in r.watermarks) + def test_apple_clean_up_attributed(self, tmp_path: Path): + # Apple Photos Clean Up (Apple Intelligence object removal) marks the + # AI edit via photoshop:Credit next to compositeWithTrainedAlgorithmicMedia + # -- it must be attributed, not reported as a generic made-with-AI tag. + # Corpus-measured 2026-07-23 (35 files). + p = tmp_path / "apple_cleanup.jpg" + p.write_bytes( + b'\xff\xd8\xff\xe1\xff\xd9" + ) + r = identify(p, check_visible=False, check_invisible=False) + assert r.is_ai_generated is True + assert r.platform == "Apple Photos (Clean Up AI edit)" + 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) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 43db90f..684e15a 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -777,6 +777,23 @@ class TestExifGenerator: remove_ai_metadata(src, out) assert exif_generator(out) is None + def test_apple_clean_up_removal_parity(self, tmp_path: Path): + # The "Apple Photos Clean Up" credit is an AI-edit VALUE under a + # non-AI key, so removal must drop it by value too (corpus 2026-07-23). + from PIL.PngImagePlugin import PngInfo + + from remove_ai_watermarks.metadata import remove_ai_metadata + + info = PngInfo() + info.add_text("Software", "Apple Photos Clean Up") + src = tmp_path / "apple.png" + Image.new("RGB", (64, 64)).save(src, pnginfo=info) + assert exif_generator(src) is not None + + out = tmp_path / "clean.png" + remove_ai_metadata(src, out) + assert exif_generator(out) is None + def test_imagedescription_tag_ai_tool_detected(self, tmp_path: Path): # ...and the EXIF ImageDescription field. exif = piexif.dump(