Spaces:
Running on Zero
Running on Zero
Download app.py from KoreaPeter/DeepFake-Video-Detection: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/KoreaPeter/DeepFake-Video-Detection/resolve/main/app.py
- Command line
-
hf download hf://spaces/KoreaPeter/DeepFake-Video-Detection/app.py
-
curl -L -o app.py https://huggingface.co/spaces/KoreaPeter/DeepFake-Video-Detection/resolve/main/app.py
13.8 kB
| 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""" | |
| <tr style="background:{bg}; border-bottom:1px solid #1e3a5f;"> | |
| <td style="padding:8px 14px; color:#90caf9; font-weight:600;">Frame {i+1}</td> | |
| <td style="padding:8px 14px;"> | |
| <div style="background:#0d2137; border-radius:4px; height:14px; width:180px; overflow:hidden;"> | |
| <div style="background:{text}; width:{bar_w}%; height:100%; border-radius:4px;"></div> | |
| </div> | |
| </td> | |
| <td style="padding:8px 14px; color:{text}; font-family:monospace;">{pct}</td> | |
| <td style="padding:8px 14px; color:{text}; font-weight:600;">{badge}</td> | |
| </tr>""" | |
| 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""" | |
| <tr style="background:#0a3060; border-top:2px solid #1565c0;"> | |
| <td style="padding:10px 14px; color:#90caf9; font-weight:700;">βΆ Final Score</td> | |
| <td style="padding:10px 14px;"> | |
| <div style="background:#0d2137; border-radius:4px; height:14px; width:180px; overflow:hidden;"> | |
| <div style="background:{fc}; width:{int(fp*100)}%; height:100%; border-radius:4px;"></div> | |
| </div> | |
| </td> | |
| <td style="padding:10px 14px; color:{fc}; font-family:monospace; font-weight:700;">{pct}</td> | |
| <td style="padding:10px 14px; color:{fc}; font-weight:700;">Aggregated</td> | |
| </tr>""" | |
| return f""" | |
| <div style="overflow-x:auto; margin-top:8px;"> | |
| <table style="width:100%; border-collapse:collapse; font-size:14px; | |
| background:#061626; border-radius:8px; overflow:hidden;"> | |
| <thead> | |
| <tr style="background:#0d47a1; color:#e3f2fd;"> | |
| <th style="padding:10px 14px; text-align:left;">Frame</th> | |
| <th style="padding:10px 14px; text-align:left;">FAKE Probability</th> | |
| <th style="padding:10px 14px; text-align:left;">Score</th> | |
| <th style="padding:10px 14px; text-align:left;">Verdict</th> | |
| </tr> | |
| </thead> | |
| <tbody>{rows}{final_row}</tbody> | |
| </table> | |
| </div>""" | |
| # ββ Inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 = """ | |
| <div style=" | |
| background: linear-gradient(135deg, #0d47a1 0%, #1565c0 40%, #0a3060 100%); | |
| border-radius: 12px; padding: 36px 40px; margin-bottom: 8px; | |
| border: 1px solid #1e3a5f; box-shadow: 0 4px 24px rgba(13,71,161,0.4); | |
| "> | |
| <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:#e3f2fd; font-size:1.9rem; font-weight:700; letter-spacing:-0.5px;">DeepGuard</h1> | |
| <p style="margin:4px 0 0; color:#90caf9; font-size:1rem;">Video Deepfake Detection</p> | |
| </div> | |
| </div> | |
| <p style="margin:0 0 16px; color:#bbdefb; font-size:0.95rem; line-height:1.6;"> | |
| Frame-by-frame deepfake analysis powered by <strong style="color:#e3f2fd;">MS-EffGCViT</strong> | |
| Β· 6 domain-specific models Β· Asian Β· Celebrity Β· Western faces | |
| </p> | |
| <div style="display:flex; gap:12px; flex-wrap:wrap;"> | |
| <a href="https://github.com/HanMoonSub/DeepGuard" target="_blank" | |
| style="background:#1e3a5f; color:#64b5f6; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #2979ff;">π GitHub</a> | |
| <a href="https://huggingface.co/KoreaPeter" target="_blank" | |
| style="background:#1e3a5f; color:#64b5f6; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #2979ff;">π€ HuggingFace</a> | |
| <a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Image-Detection" target="_blank" | |
| style="background:#1e3a5f; color:#64b5f6; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #2979ff;">π΅οΈ Image Detection</a> | |
| <a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI" target="_blank" | |
| style="background:#1e3a5f; color:#64b5f6; padding:7px 16px; border-radius:6px; | |
| text-decoration:none; font-size:0.85rem; border:1px solid #2979ff;">π¬ DeepFake XAI</a> | |
| </div> | |
| </div> | |
| """ | |
| # ββ 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) |