Download metadata_extractor.py from BADRzs/deepguard: direct link, hf CLI and curl.
- Browser
- Download file 8.52 kB
-
https://huggingface.co/spaces/BADRzs/deepguard/resolve/main/metadata_extractor.py
- Command line
-
hf download hf://spaces/BADRzs/deepguard/metadata_extractor.py
-
curl -L -o metadata_extractor.py https://huggingface.co/spaces/BADRzs/deepguard/resolve/main/metadata_extractor.py
8.52 kB
| """ | |
| Stage 2.5 - Metadata Forensic Analysis. | |
| Hardened version: downweighted to minor corroborating signal only. | |
| Missing metadata is NOT evidence of manipulation. | |
| """ | |
| import io | |
| from typing import Dict, Any, List, Optional | |
| from PIL import Image | |
| import piexif | |
| def extract_metadata(image_bytes: bytes) -> Dict[str, Any]: | |
| """ | |
| Extract EXIF metadata and analyze for forensic signals. | |
| IMPORTANT: Metadata is a MINOR CORROBORATING SIGNAL ONLY. | |
| - Absence of EXIF is common (social media, screenshots, editing) | |
| - Missing metadata does NOT push verdict toward FAKE | |
| - Software signatures are informative but not definitive | |
| """ | |
| try: | |
| image = Image.open(io.BytesIO(image_bytes)) | |
| image.load() # Force load to access EXIF | |
| except Exception as e: | |
| return _empty_metadata(f"Failed to open image: {e}") | |
| # Extract raw EXIF | |
| exif_data = _extract_exif(image) | |
| # Analyze signals | |
| has_exif = len(exif_data) > 0 | |
| camera_model = _get_camera_model(exif_data) | |
| software = _get_software(exif_data) | |
| gps_info = _get_gps(exif_data) | |
| dimensions = f"{image.width}x{image.height}" | |
| is_standard_square = _is_standard_square(image.width, image.height) | |
| # Build suspicious flags (INFORMATIVE ONLY - weight: minor) | |
| suspicious_flags = [] | |
| if not has_exif: | |
| suspicious_flags.append({ | |
| "flag": "NO_EXIF", | |
| "description": "No EXIF metadata found", | |
| "weight": "minor", | |
| "note": "Common for social media uploads, screenshots, and edited images. NOT evidence of AI generation." | |
| }) | |
| else: | |
| # Check for AI software signatures | |
| ai_software = _check_ai_software(software) | |
| if ai_software: | |
| suspicious_flags.append({ | |
| "flag": "AI_SOFTWARE", | |
| "description": f"Software signature suggests AI tool: {ai_software}", | |
| "weight": "moderate", | |
| "note": "Software tags can be stripped or spoofed. Corroborating only." | |
| }) | |
| # Check for editing software | |
| editing_software = _check_editing_software(software) | |
| if editing_software: | |
| suspicious_flags.append({ | |
| "flag": "EDITING_SOFTWARE", | |
| "description": f"Edited with: {editing_software}", | |
| "weight": "minor", | |
| "note": "Legitimate editing is common. Not evidence of fakery." | |
| }) | |
| # Standard generator dimensions | |
| if is_standard_square: | |
| suspicious_flags.append({ | |
| "flag": "STANDARD_DIMENSIONS", | |
| "description": f"Dimensions {dimensions} match common AI generator defaults", | |
| "weight": "minor", | |
| "note": "Many real photos are also square or standard sizes. Weak signal alone." | |
| }) | |
| # Missing camera hardware info (when EXIF exists but no camera data) | |
| if not camera_model and has_exif: | |
| suspicious_flags.append({ | |
| "flag": "NO_CAMERA_INFO", | |
| "description": "EXIF present but no camera make/model recorded", | |
| "weight": "minor", | |
| "note": "Some cameras/devices don't write full EXIF. Common in edited images." | |
| }) | |
| # Summary | |
| if not has_exif: | |
| summary = "No EXIF metadata available. This is normal for images from social media, messaging apps, or screenshots. Metadata absence is not evidence of manipulation." | |
| elif ai_software: | |
| summary = f"Metadata indicates creation with {ai_software}. This is a moderate corroborating signal for AI generation." | |
| elif editing_software: | |
| summary = f"Image edited with {editing_software}. Legitimate editing is common; this does not indicate fakery." | |
| elif is_standard_square: | |
| summary = f"Image has standard dimensions ({dimensions}) but camera metadata is present. Weak signal." | |
| else: | |
| summary = f"Camera metadata present: {camera_model or 'unknown device'}. Consistent with authentic capture." | |
| return { | |
| "has_exif": has_exif, | |
| "camera_model": camera_model, | |
| "software": software, | |
| "gps_present": gps_info is not None, | |
| "dimensions": dimensions, | |
| "is_standard_square": is_standard_square, | |
| "suspicious_flags": suspicious_flags, | |
| "summary": summary, | |
| "weight": "minor", # EXPLICIT: metadata is minor corroborating signal only | |
| "metadata_absent": not has_exif, # Explicit flag for downstream use | |
| } | |
| def _empty_metadata(reason: str) -> Dict[str, Any]: | |
| return { | |
| "has_exif": False, | |
| "camera_model": None, | |
| "software": None, | |
| "gps_present": False, | |
| "dimensions": "unknown", | |
| "is_standard_square": False, | |
| "suspicious_flags": [{ | |
| "flag": "EXTRACTION_FAILED", | |
| "description": reason, | |
| "weight": "none", | |
| "note": "Metadata extraction failed; no signal available." | |
| }], | |
| "summary": f"Metadata extraction failed: {reason}", | |
| "weight": "minor", | |
| "metadata_absent": True, | |
| } | |
| def _extract_exif(image: Image.Image) -> Dict: | |
| """Extract EXIF data using piexif for robust parsing.""" | |
| exif = {} | |
| try: | |
| if "exif" in image.info: | |
| exif_dict = piexif.load(image.info["exif"]) | |
| for ifd_name in ["0th", "Exif", "GPS", "1st", "Interop"]: | |
| if ifd_name in exif_dict: | |
| for tag, value in exif_dict[ifd_name].items(): | |
| try: | |
| exif[tag] = value | |
| except: | |
| pass | |
| except Exception: | |
| pass | |
| return exif | |
| def _get_camera_model(exif: Dict) -> Optional[str]: | |
| """Extract camera make/model from EXIF.""" | |
| make = exif.get(271) or exif.get(272) # Make/Model | |
| model = exif.get(272) or exif.get(271) | |
| if make and model: | |
| return f"{make} {model}".strip() | |
| return make or model | |
| def _get_software(exif: Dict) -> Optional[str]: | |
| """Extract software tag from EXIF.""" | |
| # Tag 305 = Software | |
| software = exif.get(305) | |
| if isinstance(software, bytes): | |
| software = software.decode('utf-8', errors='ignore') | |
| return software | |
| def _get_gps(exif: Dict) -> Optional[Dict]: | |
| """Extract GPS info if present.""" | |
| gps_tags = [k for k in exif.keys() if 'gps' in str(k).lower() or k in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]] | |
| if gps_tags: | |
| return {"tags_present": len(gps_tags)} | |
| return None | |
| def _is_standard_square(width: int, height: int) -> bool: | |
| """Check if dimensions match common AI generator defaults.""" | |
| standard_sizes = { | |
| (512, 512), (768, 768), (1024, 1024), (2048, 2048), | |
| (512, 768), (768, 512), (768, 1024), (1024, 768), | |
| (896, 896), (1152, 1152), | |
| } | |
| return (width, height) in standard_sizes or (height, width) in standard_sizes | |
| def _check_ai_software(software: Optional[str]) -> Optional[str]: | |
| """Check for AI generation software signatures.""" | |
| if not software: | |
| return None | |
| software_lower = software.lower() | |
| ai_signatures = { | |
| "midjourney": "Midjourney", | |
| "stable diffusion": "Stable Diffusion", | |
| "stablediffusion": "Stable Diffusion", | |
| "dall-e": "DALL-E", | |
| "dalle": "DALL-E", | |
| "novelai": "NovelAI", | |
| "comfyui": "ComfyUI", | |
| "automatic1111": "Automatic1111", | |
| "invokeai": "InvokeAI", | |
| "krita-ai": "Krita AI", | |
| "photoshop ai": "Photoshop AI", | |
| "firefly": "Adobe Firefly", | |
| "flux": "Flux", | |
| } | |
| for key, name in ai_signatures.items(): | |
| if key in software_lower: | |
| return name | |
| return None | |
| def _check_editing_software(software: Optional[str]) -> Optional[str]: | |
| """Check for traditional editing software.""" | |
| if not software: | |
| return None | |
| software_lower = software.lower() | |
| editing_signatures = { | |
| "photoshop": "Adobe Photoshop", | |
| "lightroom": "Adobe Lightroom", | |
| "gimp": "GIMP", | |
| "affinity": "Affinity Photo", | |
| "paint.net": "Paint.NET", | |
| "snapseed": "Snapseed", | |
| "vsco": "VSCO", | |
| "lightroom": "Lightroom", | |
| } | |
| for key, name in editing_signatures.items(): | |
| if key in software_lower: | |
| return name | |
| return None |