import spaces import gradio as gr import torch import tempfile import os from PIL import Image from transformers import pipeline import deepguard # noqa: F401 # registers ms_eff_gcvit_* into timm from explainability import ( HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer, EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer, ) # ── Registries ──────────────────────────────────────────────────────────────── MODEL_NAME = {"fast": "ms_eff_gcvit_b0", "pro": "ms_eff_gcvit_b5"} EXPLAINER_REGISTRY = { # low-level branch — local forgery artifacts "hirescam": HiResCAMExplainer, "gradcamelementwise": GradCAMElementWiseExplainer, "layercam": LayerCAMExplainer, # high-level branch — global semantic structure "eigengradcam": EigenGradCAMExplainer, "gradcamplusplus": GradCAMPlusPlusExplainer, "xgradcam": XGradCAMExplainer, } METHODS_BY_BRANCH = { "low": [ ("⭐ LayerCAM", "layercam"), ("HiResCAM", "hirescam"), ("GradCAMElementWise", "gradcamelementwise"), ], "high": [ ("⭐ EigenGradCAM", "eigengradcam"), ("GradCAM++", "gradcamplusplus"), ("XGradCAM", "xgradcam"), ], } RECOMMENDED = {"low": "layercam", "high": "eigengradcam"} CATEGORY_FAKE = 1 # XAI always visualizes forgery evidence # HF Hub repo naming: ms-eff-gcvit-deepfake-{b0|b5}-{kodf|celeb-df-v2|ff-plus-plus} _VARIANT_SUFFIX = {"fast": "b0", "pro": "b5"} _REPO_SUFFIX = {"kodf": "kodf", "celeb_df_v2": "celeb-df-v2", "ff++": "ff-plus-plus"} def _repo(variant: str, dataset: str) -> str: return f"KoreaPeter/ms-eff-gcvit-deepfake-{_VARIANT_SUFFIX[variant]}-{_REPO_SUFFIX[dataset]}" _pipe_cache: dict = {} _cache: dict = {} def _get_pipe(variant: str, dataset: str): """HF pipeline 로드 — download 카운트 + model card의 Spaces 노출을 트리거.""" key = (variant, dataset) if key not in _pipe_cache: device = "cuda" if torch.cuda.is_available() else "cpu" _pipe_cache[key] = pipeline( "image-classification", model=_repo(variant, dataset), trust_remote_code=True, device=device, ) return _pipe_cache[key] def _get_explainer(variant: str, dataset: str, method: str, branch: str): key = (variant, dataset, method, branch) if key not in _cache: pipe = _get_pipe(variant, dataset) _cache[key] = EXPLAINER_REGISTRY[method]( model_name=MODEL_NAME[variant], dataset=dataset, branch_level=branch, model=pipe.model.model, # pipeline이 로드한 timm 모델 주입 (가중치 포함) ) return _cache[key] # ── Inference ───────────────────────────────────────────────────────────────── @spaces.GPU def explain( image: Image.Image, variant: str, dataset: str, branch: str, method: str, display_mode: str, aug_smooth: bool, eigen_smooth: bool, image_weight: float, threshold: float, ): if image is None: raise gr.Error("Please upload an image first.") with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: image.save(f.name) tmp_path = f.name try: explainer = _get_explainer(variant, dataset, method, branch) # keep cached model on the active (ZeroGPU) device if torch.cuda.is_available(): explainer.model.to("cuda:0") explainer.device = "cuda:0" common = dict( category=CATEGORY_FAKE, threshold=threshold, aug_smooth=aug_smooth, eigen_smooth=eigen_smooth, ) if display_mode == "heatmap": result = explainer.display_heatmap_on_image(tmp_path, image_weight=image_weight, **common) elif display_mode == "bbox": result = explainer.display_bbox_on_image(tmp_path, **common) else: # heatmap_bbox result = explainer.display_heatmap_bbox_on_image(tmp_path, image_weight=image_weight, **common) except Exception as e: msg = str(e) if "face" in msg.lower() or "NoneType" in msg or "index" in msg.lower(): raise gr.Error("😶 No face detected. Please use an image with a clearly visible face.") raise gr.Error(f"Explainer error: {e}") finally: os.unlink(tmp_path) return result # ── Assets (HF CDN absolute URL — keeps GIF animation) ───────────────────────── _HF_BASE = "https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main" DUAL_BRANCH_SRC = f"{_HF_BASE}/dual_branch.gif" XAI_DEMO_SRC = f"{_HF_BASE}/xai_demo.gif" # img lives in gr.HTML — gr.Markdown sanitizes tags away XAI_DEMO_HTML = f"""
""" # ── Hero banner ─────────────────────────────────────────────────────────────── HERO = f"""
🤗

DeepGuard · XAI

Dual-Branch Deepfake Explainability

Our custom MS-EffGCViT reasons over two parallel streams — a high-resolution low-level branch for local forgery artifacts and a low-resolution high-level branch for global semantic structure. This tool mirrors that dual-branch design to reveal where and why a face is flagged as manipulated.

🔗 GitHub 🤗 HuggingFace 🕵️ Image Detection 🎬 Video Detection
""" # ── Theme & CSS ─────────────────────────────────────────────────────────────── THEME = gr.themes.Base( primary_hue="yellow", neutral_hue="gray", font=gr.themes.GoogleFont("Inter"), ).set( body_background_fill="#0b0f19", body_text_color="#e5e7eb", block_background_fill="#1a1f2e", block_border_color="#2d3748", input_background_fill="#0b0f19", button_primary_background_fill="#FFD21E", button_primary_text_color="#1a1a1a", button_primary_border_color="#FFD21E", button_primary_background_fill_hover="#FFB700", button_primary_text_color_hover="#1a1a1a", ) CSS = """ /* branch/method groups get a colored frame driven by the selected branch */ #branch-group, #method-group { border: 2px solid #3b82f6 !important; /* default = low (blue) */ border-radius: 10px !important; padding: 10px 12px !important; transition: border-color 0.2s ease; } #branch-group.branch-high, #method-group.branch-high { border-color: #ef4444 !important; /* high = red */ } footer { display:none !important; } """ # ── Build UI ────────────────────────────────────────────────────────────────── with gr.Blocks(title="DeepGuard XAI") as demo: gr.HTML(HERO) with gr.Row(): with gr.Column(scale=1, min_width=320): image_input = gr.Image(type="pil", label="Upload Image", height=400) with gr.Accordion("🖼️ Example Images — (click to expand)", open=False): gr.Examples( examples=["example.JPG"], inputs=image_input, label="", ) gr.Markdown("### ⚡ Model Variant") variant_radio = gr.Radio( choices=["fast", "pro"], value="pro", label=None, info="Fast (b0): 8.7M params | Pro (b5): 50.3M params", ) gr.Markdown("### 🌍 Training Domain") dataset_radio = gr.Radio( choices=[ ("🇰🇷 KoDF — East Asian faces", "kodf"), ("🌟 Celeb-DF-v2 — Celebrity faces", "celeb_df_v2"), ("🌍 FF++ — Western faces", "ff++"), ], value="ff++", label=None, ) gr.Markdown("### 🧠 Branch Level") branch_radio = gr.Radio( choices=[("🔵 Low — local artifacts", "low"), ("🔴 High — global semantics", "high")], value="high", label=None, elem_id="branch-group", ) gr.Markdown("### 📐 CAM Method") method_radio = gr.Radio( choices=METHODS_BY_BRANCH["high"], value=RECOMMENDED["high"], label=None, elem_id="method-group", ) run_btn = gr.Button("🔬 Visualize", variant="primary", size="lg") with gr.Column(scale=1, min_width=320): result_output = gr.Image(label="Forgery Localization", type="numpy", height=400) gr.Markdown(""" **Branch Guide** | Branch | Feature Map | Focus | Best For | |---|---|---|---| | 🔵 **Low** | High resolution | Local forgery artifacts | Skin texture, boundary blending, compression traces | | 🔴 **High** | Low resolution | Global semantic structure | Lighting inconsistency, facial geometry, shadow artifacts | """) gr.Markdown("### 🎨 Display Guide") gr.HTML(XAI_DEMO_HTML) with gr.Accordion("⚡ Model Guide", open=False): gr.Markdown(""" | | ⚡ Fast (b0) | 🔥 Pro (b5) | |---|---|---| | **Params** | 8.7M | 50.3M | | **Resolution** | 224×224 | 384×384 | | **Speed** | Faster | Slower | | **Accuracy** | High | Higher | """) with gr.Accordion("🌍 Domain Guide", open=False): gr.Markdown(""" | Domain | Training Data | Best For | |---|---|---| | KoDF | 62K real / 175K fake (Korean) | East Asian faces | | Celeb-DF-v2 | 890 real / 5.6K fake (Celebrities) | Celebrity faces | | FF++ | 1K real / 6K fake (4 manipulation types) | Western faces | """) with gr.Accordion("📐 CAM Method Guide", open=False): gr.Markdown(""" | Branch | Method | Core Idea | |---|---|---| | 🔵 Low | **HiResCAM** | GradCAM with element-wise activation×gradient; provably faithful | | 🔵 Low | **GradCAMElementWise** | Element-wise activation×gradient + ReLU before summing | | 🔵 Low | **⭐ LayerCAM** | Positive-gradient spatial weighting; strong in lower layers | | 🔴 High | **⭐ EigenGradCAM** | 1st principal component of activations×grad; cleaner GradCAM | | 🔴 High | **GradCAM++** | GradCAM using second-order gradients | | 🔴 High | **XGradCAM** | GradCAM with gradients scaled by normalized activations | """) with gr.Accordion("🖼️ Display Setting", open=False): display_radio = gr.Radio( choices=[ ("Heatmap", "heatmap"), ("BBox", "bbox"), ("Heatmap + BBox", "heatmap_bbox"), ], value="heatmap_bbox", label="Display Mode", ) with gr.Row(): image_weight_slider = gr.Slider( minimum=0.0, maximum=1.0, value=0.7, step=0.05, label="Image Weight", info="0.0 heatmap only ←→ 1.0 original only (heatmap modes)", ) threshold_slider = gr.Slider( minimum=0.5, maximum=1.0, value=0.9, step=0.05, label="Threshold", info="CAM binarization cutoff — 0.9 keeps top-10% regions", ) with gr.Accordion("🌀 Visualize Smoothing Setting", open=False): gr.Markdown("Optional smoothing passes over the CAM. Off is recommended for most cases.") with gr.Row(): aug_smooth_chk = gr.Checkbox( value=False, label="Aug Smooth", info="TTA (horizontal flips) before averaging → smoother maps. Slower.", ) eigen_smooth_chk = gr.Checkbox( value=False, label="Eigen Smooth", info="PCA noise reduction → keeps dominant forgery pattern only.", ) # branch → method choices sync (recommended method becomes default) def _sync_methods(branch): choices = METHODS_BY_BRANCH[branch] return gr.update(choices=choices, value=RECOMMENDED[branch]) branch_radio.change(fn=_sync_methods, inputs=branch_radio, outputs=method_radio) # branch → recolor the branch/method frames (blue=low, red=high) branch_radio.change( fn=None, inputs=branch_radio, outputs=None, js="""(b) => { const isHigh = (b === 'high'); ['branch-group', 'method-group'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.toggle('branch-high', isHigh); }); }""", ) inputs = [ image_input, variant_radio, dataset_radio, branch_radio, method_radio, display_radio, aug_smooth_chk, eigen_smooth_chk, image_weight_slider, threshold_slider, ] run_btn.click(fn=explain, inputs=inputs, outputs=result_output) if __name__ == "__main__": demo.launch(theme=THEME, css=CSS)