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.
"""
# ── 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)