Download app.py from LPX55/DeepfakeDetection-Explainability: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/LPX55/DeepfakeDetection-Explainability/resolve/main/app.py
- Command line
-
hf download hf://spaces/LPX55/DeepfakeDetection-Explainability/app.py
-
curl -L -o app.py https://huggingface.co/spaces/LPX55/DeepfakeDetection-Explainability/resolve/main/app.py
11.6 kB
| import os | |
| import base64 | |
| import hashlib | |
| import threading | |
| from io import BytesIO | |
| from collections import OrderedDict | |
| from concurrent.futures import ThreadPoolExecutor | |
| from PIL import Image | |
| from pydantic import BaseModel | |
| from fastapi.responses import HTMLResponse | |
| import gradio as gr | |
| from gradio import Server | |
| from xai_engine.model import get_model | |
| from xai_engine.methods import run_all_methods, run_single_method, run_per_head_attention | |
| from forensics.runner import run_forensic_tool | |
| from logger import log_run_dataset | |
| # Initialize Gradio Server app | |
| app = Server(title="DeepfakeDetection-XAI") | |
| demo = app | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Result Cache — SHA256-keyed LRU, server-side RAM | |
| # ────────────────────────────────────────────────────────────────────── | |
| _result_cache: OrderedDict[str, dict] = OrderedDict() | |
| _cache_lock = threading.Lock() | |
| _CACHE_MAX = 50 # ~120MB max at 6 XAI + 5 forensics per entry | |
| def _img_hash(image: Image.Image) -> str: | |
| """SHA256 hash of image pixel content for cache keying.""" | |
| buf = BytesIO() | |
| image.save(buf, format="PNG") | |
| return hashlib.sha256(buf.getvalue()).hexdigest() | |
| def _cache_get(key: str) -> dict | None: | |
| with _cache_lock: | |
| val = _result_cache.get(key) | |
| if val is not None: | |
| _result_cache.move_to_end(key) # LRU touch | |
| return val | |
| def _cache_set(key: str, val: dict): | |
| with _cache_lock: | |
| _result_cache[key] = val | |
| if len(_result_cache) > _CACHE_MAX: | |
| _result_cache.popitem(last=False) # Evict oldest | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Helpers | |
| # ────────────────────────────────────────────────────────────────────── | |
| def b64_to_pil(b64_str: str) -> Image.Image: | |
| """Convert base64 image data URL to PIL Image.""" | |
| if not b64_str or not isinstance(b64_str, str): | |
| raise ValueError("Invalid base64 image string provided.") | |
| if "," in b64_str: | |
| _, data = b64_str.split(",", 1) | |
| else: | |
| data = b64_str | |
| image_data = base64.b64decode(data) | |
| return Image.open(BytesIO(image_data)).convert("RGB") | |
| def _run_all_forensics_defaults(image: Image.Image) -> dict: | |
| """Run all 5 classic forensics tools at default parameters. Returns dict keyed by tool id.""" | |
| tools = ["ela", "gradient", "bitplane", "minmax", "wavelet"] | |
| results = {} | |
| for t in tools: | |
| try: | |
| results[t] = run_forensic_tool(image, t) | |
| except Exception as e: | |
| print(f"[forensics] Error running {t}: {e}") | |
| return results | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Pydantic Request Schemas | |
| # ────────────────────────────────────────────────────────────────────── | |
| class PredictReq(BaseModel): | |
| image_b64: str | |
| class AnalyzeReq(BaseModel): | |
| image_b64: str | |
| alpha: float = 0.6 | |
| cmap: str = "jet" | |
| class MethodDetailReq(BaseModel): | |
| image_b64: str | |
| method_name: str = "rollout" | |
| steps: int = 20 | |
| patch_size: int = 32 | |
| stride: int = 16 | |
| target_layer_idx: int = 11 | |
| alpha: float = 0.6 | |
| cmap: str = "jet" | |
| class HeadsReq(BaseModel): | |
| image_b64: str | |
| layer_idx: int = 11 | |
| cmap: str = "jet" | |
| class ForensicReq(BaseModel): | |
| image_b64: str | |
| tool_name: str = "ela" | |
| quality: int = 75 | |
| scale: int = 50 | |
| contrast: int = 20 | |
| gradient_intensity: int = 90 | |
| blue_mode: str = "Abs" | |
| channel: str = "Luminance" | |
| bit: int = 0 | |
| minmax_radius: int = 2 | |
| class SubmitRunReq(BaseModel): | |
| image_b64: str | |
| image_hash: str | None = None | |
| ground_truth: str = "unknown" | |
| opt_in_image: bool = True | |
| # ────────────────────────────────────────────────────────────────────── | |
| # REST API Routes | |
| # ────────────────────────────────────────────────────────────────────── | |
| def predict_post(req: PredictReq) -> dict: | |
| """Fast prediction — no XAI, just verdict + confidence.""" | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| model = get_model() | |
| pred_data = model.predict(image) | |
| img_hash = _img_hash(image) | |
| return { | |
| "status": "success", | |
| "prediction": pred_data, | |
| "image_hash": img_hash, | |
| } | |
| def analyze_post(req: AnalyzeReq) -> dict: | |
| """Full analysis: XAI + forensics. Results are cached.""" | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| key = _img_hash(image) | |
| # Check cache | |
| cached = _cache_get(key) | |
| if cached: | |
| return {**cached, "cache_hit": True} | |
| model = get_model() | |
| pred_data = model.predict(image) | |
| # Run XAI methods and forensics in parallel | |
| with ThreadPoolExecutor(max_workers=2) as executor: | |
| xai_future = executor.submit(run_all_methods, model, image, alpha=req.alpha, cmap=req.cmap) | |
| forensics_future = executor.submit(_run_all_forensics_defaults, image) | |
| xai_results = xai_future.result() | |
| forensics_results = forensics_future.result() | |
| res_dicts = [ | |
| { | |
| "name": r.name, | |
| "description": r.description, | |
| "overlay_b64": r.overlay_b64, | |
| "raw_heatmap_b64": r.raw_heatmap_b64, | |
| "metadata": r.metadata, | |
| "compute_time_ms": r.compute_time_ms, | |
| } | |
| for r in xai_results | |
| ] | |
| response = { | |
| "status": "success", | |
| "prediction": pred_data, | |
| "results": res_dicts, | |
| "forensics": forensics_results, | |
| "image_hash": key, | |
| "cache_hit": False, | |
| } | |
| _cache_set(key, response) | |
| return response | |
| def method_detail_post(req: MethodDetailReq) -> dict: | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| model = get_model() | |
| res = run_single_method( | |
| model, image, req.method_name, | |
| alpha=req.alpha, cmap=req.cmap, steps=req.steps, | |
| patch_size=req.patch_size, stride=req.stride, | |
| target_layer_idx=req.target_layer_idx, | |
| ) | |
| return { | |
| "status": "success", | |
| "result": { | |
| "name": res.name, | |
| "description": res.description, | |
| "overlay_b64": res.overlay_b64, | |
| "raw_heatmap_b64": res.raw_heatmap_b64, | |
| "metadata": res.metadata, | |
| "compute_time_ms": res.compute_time_ms, | |
| } | |
| } | |
| def heads_post(req: HeadsReq) -> dict: | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| model = get_model() | |
| head_data = run_per_head_attention(model, image, layer_idx=req.layer_idx, cmap=req.cmap) | |
| head_data["status"] = "success" | |
| return head_data | |
| def forensic_post(req: ForensicReq) -> dict: | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| res = run_forensic_tool( | |
| image, req.tool_name, | |
| quality=req.quality, scale=req.scale, contrast=req.contrast, | |
| gradient_intensity=req.gradient_intensity, blue_mode=req.blue_mode, | |
| channel=req.channel, bit=req.bit, minmax_radius=req.minmax_radius, | |
| ) | |
| res["status"] = "success" | |
| return res | |
| def submit_run_post(req: SubmitRunReq) -> dict: | |
| """Submit ground truth feedback. Reads XAI/forensics from cache — zero re-inference.""" | |
| try: | |
| image = b64_to_pil(req.image_b64) | |
| except Exception as e: | |
| return {"status": "error", "message": f"Image decoding failed: {e}"} | |
| key = req.image_hash or _img_hash(image) | |
| cached = _cache_get(key) | |
| if cached: | |
| pred_data = cached["prediction"] | |
| xai_results = cached["results"] | |
| forensics_results = cached.get("forensics", {}) | |
| else: | |
| # Fallback: run analysis if cache miss (shouldn't happen in normal flow) | |
| model = get_model() | |
| pred_data = model.predict(image) | |
| xai_result_objs = run_all_methods(model, image) | |
| xai_results = [ | |
| { | |
| "name": r.name, | |
| "description": r.description, | |
| "overlay_b64": r.overlay_b64, | |
| "raw_heatmap_b64": r.raw_heatmap_b64, | |
| "metadata": r.metadata, | |
| "compute_time_ms": r.compute_time_ms, | |
| } | |
| for r in xai_result_objs | |
| ] | |
| forensics_results = _run_all_forensics_defaults(image) | |
| run_id = log_run_dataset( | |
| image, pred_data, xai_results, | |
| forensics_results=forensics_results, | |
| ground_truth=req.ground_truth, | |
| opt_in_image=req.opt_in_image, | |
| ) | |
| return { | |
| "status": "success", | |
| "run_id": run_id, | |
| "message": f"Run {run_id} successfully submitted to dataset!", | |
| } | |
| def client_config(): | |
| """Return model and space configuration for frontend initialization.""" | |
| return { | |
| "model_name": "buildborderless/CommunityForensics-DeepfakeDet-ViT", | |
| "model_architecture": "ViT-Small (384x384, 12 layers, 6 heads)", | |
| "xai_methods": [ | |
| {"id": "rollout", "name": "Attention Rollout"}, | |
| {"id": "chefer", "name": "Gradient Attention Rollout (Chefer)"}, | |
| {"id": "gradcam", "name": "GradCAM"}, | |
| {"id": "integrated", "name": "Integrated Gradients"}, | |
| {"id": "deeplift", "name": "DeepLIFT"}, | |
| {"id": "occlusion", "name": "Occlusion Sensitivity"}, | |
| ], | |
| "forensic_tools": [ | |
| {"id": "ela", "name": "Error Level Analysis (ELA)"}, | |
| {"id": "gradient", "name": "Spatial Edge Gradient"}, | |
| {"id": "bitplane", "name": "Bit Plane Extractor"}, | |
| {"id": "minmax", "name": "MinMax Deviation"}, | |
| {"id": "wavelet", "name": "Wavelet Noise Estimation"}, | |
| ] | |
| } | |
| async def homepage(): | |
| html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html") | |
| with open(html_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| app.setup() | |
| if __name__ == "__main__": | |
| app.launch(show_error=True) | |