aeye-backend / provenance_gate.py
wonjun12's picture
Deploy A-EYE app (Expo web) + hybrid backend (model 49 verdict + 63 heatmap)
5dab1e8 verified
Raw
History Blame Contribute Delete
4.08 kB
"""Self-contained provenance (C2PA/metadata) gate — works on raw upload bytes.
Ported from the detector repo's scripts/provenance_check.py. No external service
(:8061) and no special c2pa library: pure PIL + byte-signature scan. When a
generator/provenance signal is present it is HIGH precision -> force AI verdict.
Absence proves nothing (screenshots strip it) -> defer to the pixel model.
"""
from __future__ import annotations
import io
import json
from PIL import ExifTags, Image
SIGNATURES = {
"C2PA/JUMBF": [b"c2pa", b"jumbf", b"urn:c2pa", b"claim_generator", b"caBX", b"contentauth", b"c2pa.assertions"],
"OpenAI": [b"openai", b"dall-e", b"dall\xc2\xb7e", b"dalle", b"gpt-image", b"chatgpt"],
"Adobe/Firefly": [b"firefly", b"cai:"],
"StableDiffusion/Comfy": [b"stable diffusion", b"stable-diffusion", b"sd-webui", b"automatic1111",
b"comfyui", b"negative prompt"],
"Midjourney": [b"midjourney", b"niji"],
"Google/SynthID": [b"synthid", b"made with google", b"gemini", b"google ai", b"imagen 3", b"imagen 4"],
"Other-gen": [b"novelai", b"leonardo.ai", b"ideogram", b"playground", b"seedream", b"flux.1", b"grok-image"],
"AIGC-generic": [b"trainedalgorithmicmedia", b"compositesynthetic", b"ai-generated", b"genai"],
}
_EXIF_TAGS = {v: k for k, v in ExifTags.TAGS.items()}
def _scan(raw: bytes) -> set[str]:
low = raw.lower()
return {label for label, sigs in SIGNATURES.items() if any(s in low for s in sigs)}
def check_bytes(raw: bytes) -> dict:
"""Returns {'signals': [...], 'details': {...}} ; signals non-empty => AI."""
details: dict = {}
sig = _scan(raw)
try:
img = Image.open(io.BytesIO(raw))
details["format"] = img.format
info = {k: v for k, v in (img.info or {}).items()
if isinstance(v, (str, bytes)) and k.lower() != "icc_profile"}
if info:
details["text_chunks"] = {
k: (v if isinstance(v, str) else v.decode("latin-1", "ignore"))[:300]
for k, v in info.items()
}
if "xmp" in img.info:
xmp = img.info["xmp"]
sig |= _scan(xmp if isinstance(xmp, bytes) else xmp.encode("latin-1", "ignore"))
exif = img.getexif()
if exif:
wanted = ("Make", "Model", "Software", "Artist", "XPComment", "ImageDescription")
ex = {name: str(exif[_EXIF_TAGS[name]])[:200]
for name in wanted if name in _EXIF_TAGS and _EXIF_TAGS[name] in exif}
if ex:
details["exif"] = ex
except Exception as exc: # noqa: BLE001
details["pil_error"] = str(exc)
blob = json.dumps(details, ensure_ascii=False).lower()
for label, sigs in SIGNATURES.items():
if any(s.decode("latin-1", "ignore") in blob for s in sigs):
sig.add(label)
return {"signals": sorted(sig), "details": details}
def check_filename(name: str | None) -> list[str]:
"""Weak mobile fallback for generator-exported filenames.
Some phone gallery providers hand Expo a transcoded/cache copy that strips
C2PA/JUMBF chunks. The filename is not cryptographic provenance, but app
exports like "ChatGPT Image ..." are still useful as a high-confidence hint
for the demo path when raw provenance was removed before upload.
"""
low = (name or "").lower()
found: list[str] = []
if "chatgpt image" in low or "gpt-image" in low or "dall-e" in low or "dalle" in low:
found.extend(["OpenAI", "Filename:AI-export"])
if "midjourney" in low or "niji" in low:
found.extend(["Midjourney", "Filename:AI-export"])
if "stable diffusion" in low or "comfyui" in low or "sd-webui" in low:
found.extend(["StableDiffusion/Comfy", "Filename:AI-export"])
# Gemini app saves as "Gemini_Generated_Image_xxxxx.png"; Imagen exports similarly.
if "gemini" in low or "imagen" in low or "google_ai" in low or "made with google" in low:
found.extend(["Google/SynthID", "Filename:AI-export"])
return sorted(set(found))