mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
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/<model> 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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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/<model>"
|
||||
# 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<x:xmpmeta photoshop:Credit="Apple Photos Clean Up" '
|
||||
b"Iptc4xmpExt:DigitalSourceType=compositeWithTrainedAlgorithmicMedia></x:xmpmeta>\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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user