import spaces # ← must be first, before any CUDA-related import
import gradio as gr
import torch
from transformers import pipeline
# ── Model registry ───────────────────────────────────────────────────────────
MODELS = {
("fast", "kodf"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-kodf",
("fast", "celeb-df-v2"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-celeb-df-v2",
("fast", "ff++"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-ff-plus-plus",
("pro", "kodf"): "KoreaPeter/ms-eff-gcvit-deepfake-b5-kodf",
("pro", "celeb-df-v2"): "KoreaPeter/ms-eff-gcvit-deepfake-b5-celeb-df-v2",
("pro", "ff++"): "KoreaPeter/ms-eff-gcvit-deepfake-b5-ff-plus-plus",
}
_cache: dict = {}
def _get_pipeline(variant: str, dataset: str):
key = (variant, dataset)
if key not in _cache:
# DEVICE를 함수 호출 시점에 평가 — ZeroGPU가 GPU 할당한 이후여야 함
device = "cuda" if torch.cuda.is_available() else "cpu"
_cache[key] = pipeline(
"video-classification",
model=MODELS[key],
trust_remote_code=True,
device=device,
)
return _cache[key]
# ── Frame table (HTML) ────────────────────────────────────────────────────────
def _build_frame_table(frame_scores: list, final_fake_prob: float) -> str:
rows = ""
for i, s in enumerate(frame_scores):
fp = round(float(s), 4)
pct = f"{fp * 100:.1f}%"
if fp >= 0.7:
bg, badge, text = "#3b0f0f", "🔴 FAKE", "#ff6b6b"
elif fp >= 0.4:
bg, badge, text = "#2e2a00", "🟡 UNCERTAIN", "#ffd700"
else:
bg, badge, text = "#0b1f2e", "🟢 REAL", "#4fc3f7"
bar_w = int(fp * 100)
rows += f"""
| Frame {i+1} |
|
{pct} |
{badge} |
"""
fp = round(final_fake_prob, 4)
pct = f"{fp * 100:.1f}%"
fc = "#ff6b6b" if fp >= 0.7 else ("#ffd700" if fp >= 0.4 else "#4fc3f7")
final_row = f"""
| ▶ Final Score |
|
{pct} |
Aggregated |
"""
return f"""
| Frame |
FAKE Probability |
Score |
Verdict |
{rows}{final_row}
"""
# ── Inference ─────────────────────────────────────────────────────────────────
@spaces.GPU
def predict(
video_path: str,
variant: str,
dataset: str,
num_frames: int,
margin_ratio: float,
conf_thres: float,
min_face_ratio: float,
tta_hflip: float,
agg_mode: str,
):
if video_path is None:
return None, "⚠️ Please upload a video.", ""
try:
pipe = _get_pipeline(variant, dataset)
results = pipe(
video_path,
num_frames=num_frames,
margin_ratio=margin_ratio,
conf_thres=conf_thres,
min_face_ratio=min_face_ratio,
tta_hflip=tta_hflip,
agg_mode=agg_mode,
return_frame_scores=True,
)
except Exception as e:
return None, f"❌ Pipeline error: {e}", ""
if results[0]["label"] == "no_face_detected":
return (
{"REAL": 0.0, "FAKE": 0.0},
"😶 No face detected.\nPlease use a video with a clearly visible face.",
"",
)
scores = {r["label"].upper(): round(r["score"], 4)
for r in results if r.get("label") in ("fake", "real")}
fake_prob = scores.get("FAKE", 0.0)
if fake_prob >= 0.7:
verdict = f"🔴 FAKE ({fake_prob * 100:.1f}% confidence)"
elif fake_prob >= 0.4:
verdict = f"🟡 UNCERTAIN (FAKE {fake_prob * 100:.1f}%)"
else:
verdict = f"🟢 REAL ({(1 - fake_prob) * 100:.1f}% confidence)"
frame_scores = next(
(r["frame_scores"] for r in results if "frame_scores" in r), None
)
table_html = _build_frame_table(frame_scores, fake_prob) if frame_scores else ""
return scores, verdict, table_html
# ── Hero banner HTML ──────────────────────────────────────────────────────────
HERO = """
🎬
DeepGuard
Video Deepfake Detection
Frame-by-frame deepfake analysis powered by MS-EffGCViT
· 6 domain-specific models · Asian · Celebrity · Western faces
"""
# ── Theme & CSS ───────────────────────────────────────────────────────────────
THEME = gr.themes.Base(
primary_hue="blue",
neutral_hue="slate",
font=gr.themes.GoogleFont("Inter"),
).set(
body_background_fill="#061626",
body_text_color="#e3f2fd",
block_background_fill="#0d2137",
block_border_color="#1e3a5f",
input_background_fill="#0a1929",
button_primary_background_fill="#0d47a1",
button_primary_text_color="#e3f2fd",
button_primary_border_color="#2979ff",
button_primary_background_fill_hover="#1565c0",
button_primary_text_color_hover="#ffffff",
)
CSS = """
.result-box textarea { font-size:1.1rem !important; font-weight:bold !important; text-align:center; }
footer { display:none !important; }
"""
# ── Build UI ──────────────────────────────────────────────────────────────────
with gr.Blocks(title="DeepGuard Video") as demo:
gr.HTML(HERO)
with gr.Row():
with gr.Column(scale=1, min_width=300):
video_input = gr.Video(label="Upload Video")
with gr.Accordion("🎞️ Example Videos — (click to expand)", open=False):
gr.Examples(
examples=["western_fake.mp4", "western_real.mp4"],
inputs=video_input,
label="order = FAKE, REAL",
)
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,
)
run_btn = gr.Button("🔍 Analyze", variant="primary", size="lg")
with gr.Column(scale=1, min_width=300):
verdict_output = gr.Textbox(
label="Result",
interactive=False,
lines=2,
elem_classes=["result-box"],
)
score_output = gr.Label(label="Class Probabilities", num_top_classes=2)
gr.Markdown("""
---
**Model Guide**
| | ⚡ Fast (b0) | 🔥 Pro (b5) |
|---|---|---|
| **Params** | 8.7M | 50.3M |
| **Resolution** | 224×224 | 384×384 |
| **Speed** | Faster | Slower |
| **Accuracy** | High | Higher |
**Domain Guide**
| 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("⚙️ Advanced Settings", open=True):
gr.Markdown("Adjust inference parameters. Defaults are recommended for most cases.")
with gr.Row():
with gr.Column():
num_frames_slider = gr.Slider(
minimum=5, maximum=20, value=5, step=1,
label="Num Frames",
info="Number of frames sampled from the video",
)
margin_ratio_slider = gr.Slider(
minimum=0.0, maximum=0.5, value=0.2, step=0.05,
label="Margin Ratio",
info="Margin around the detected face bounding box",
)
tta_hflip_slider = gr.Slider(
minimum=0.0, maximum=1.0, value=0.0, step=0.1,
label="TTA Horizontal Flip",
info="⚠️ > 0.0 doubles inference time per frame",
)
with gr.Column():
agg_radio = gr.Radio(
choices=["conf", "mean", "vote"],
value="conf",
label="Aggregation Mode",
info="conf: heuristic (recommended) · mean: average · vote: majority",
)
conf_thres_slider = gr.Slider(
minimum=0.1, maximum=0.9, value=0.5, step=0.05,
label="Face Detection Confidence",
info="YOLO face detection confidence threshold",
)
min_face_ratio_slider = gr.Slider(
minimum=0.001, maximum=0.1, value=0.01, step=0.001,
label="Min Face Ratio",
info="Minimum face-to-frame area ratio to process",
)
with gr.Accordion("📊 Frame-level Results", open=True):
frame_table = gr.HTML("")
run_btn.click(
fn=predict,
inputs=[
video_input, variant_radio, dataset_radio,
num_frames_slider, margin_ratio_slider, conf_thres_slider,
min_face_ratio_slider, tta_hflip_slider, agg_radio,
],
outputs=[score_output, verdict_output, frame_table],
)
if __name__ == "__main__":
demo.launch(theme=THEME, css=CSS)