| 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 |
|
|
| |
| app = Server(title="DeepfakeDetection-XAI") |
| demo = app |
|
|
| |
| |
| |
| _result_cache: OrderedDict[str, dict] = OrderedDict() |
| _cache_lock = threading.Lock() |
| _CACHE_MAX = 50 |
|
|
|
|
| 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) |
| 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) |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| @app.post("/api/predict") |
| 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, |
| } |
|
|
|
|
| @app.post("/api/analyze") |
| 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) |
|
|
| |
| cached = _cache_get(key) |
| if cached: |
| return {**cached, "cache_hit": True} |
|
|
| model = get_model() |
| pred_data = model.predict(image) |
|
|
| |
| 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 |
|
|
|
|
| @app.post("/api/method_detail") |
| 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, |
| } |
| } |
|
|
|
|
| @app.post("/api/heads") |
| 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 |
|
|
|
|
| @app.post("/api/forensic") |
| 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 |
|
|
|
|
| @app.post("/api/submit_run") |
| 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: |
| |
| 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!", |
| } |
|
|
|
|
| @app.get("/api/config") |
| 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"}, |
| ] |
| } |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| 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) |
|
|