Spaces:
Running on Zero
Running on Zero
Download app.py from KoreaPeter/DeepFake-Detection-XAI: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main/app.py
- Command line
-
hf download hf://spaces/KoreaPeter/DeepFake-Detection-XAI/app.py
-
curl -L -o app.py https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main/app.py
15.6 kB
| 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 ───────────────────────────────────────────────────────────────── | |
| 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 <img> tags away | |
| XAI_DEMO_HTML = f""" | |
| <div style="text-align:center; margin:4px 0;"> | |
| <img src="{XAI_DEMO_SRC}" style="max-width:95%; border-radius:8px;"> | |
| </div> | |
| """ | |
| # ── Hero banner ─────────────────────────────────────────────────────────────── | |
| HERO = f""" | |
| <div style=" | |
| background: linear-gradient(135deg, #13161f 0%, #3d2f0a 60%, #4a3600 100%); | |
| border-radius: 14px; padding: 36px 40px; margin-bottom: 8px; | |
| border: 1px solid #FFD21E; box-shadow: 0 4px 28px rgba(255,210,30,0.20); | |
| "> | |
| <div style="display:flex; align-items:center; gap:16px; margin-bottom:12px;"> | |
| <span style="font-size:2.4rem;">🤗</span> | |
| <div> | |
| <h1 style="margin:0; color:#ffffff; font-size:1.9rem; font-weight:700; letter-spacing:-0.5px;">DeepGuard · XAI</h1> | |
| <p style="margin:4px 0 0; color:#FFD21E; font-size:1rem;">Dual-Branch Deepfake Explainability</p> | |
| </div> | |
| </div> | |
| <p style="margin:0 0 16px; color:#e5d5a8; font-size:0.95rem; line-height:1.6;"> | |
| Our custom <strong style="color:#ffffff;">MS-EffGCViT</strong> reasons over two parallel streams — a | |
| <strong style="color:#93c5fd;">high-resolution low-level branch</strong> for local forgery artifacts and a | |
| <strong style="color:#fca5a5;">low-resolution high-level branch</strong> for global semantic structure. | |
| This tool mirrors that dual-branch design to reveal <em>where and why</em> a face is flagged as manipulated. | |
| </p> | |
| <div style="text-align:center; margin:8px 0 16px;"><img src="{DUAL_BRANCH_SRC}" style="max-width:70%; border-radius:8px; border:1px solid #FFD21E;"></div> | |
| <div style="display:flex; gap:12px; flex-wrap:wrap;"> | |
| <a href="https://github.com/HanMoonSub/DeepGuard" target="_blank" | |
| style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🔗 GitHub</a> | |
| <a href="https://huggingface.co/KoreaPeter" target="_blank" | |
| style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🤗 HuggingFace</a> | |
| <a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Image-Detection" target="_blank" | |
| style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🕵️ Image Detection</a> | |
| <a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Video-Detection" target="_blank" | |
| style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🎬 Video Detection</a> | |
| </div> | |
| </div> | |
| """ | |
| # ── 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) |