Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import torch | |
| import numpy as np | |
| import cv2 | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from PIL import Image | |
| from torchvision import transforms | |
| from transformers import (AutoImageProcessor, AutoModelForImageClassification, | |
| ViTMAEForPreTraining) | |
| from facenet_pytorch import MTCNN | |
| # ββ Setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| to_pil = transforms.ToPILImage() | |
| IMG_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3,1,1) | |
| IMG_STD = torch.tensor([0.229, 0.224, 0.225]).view(3,1,1) | |
| # ββ Load models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("β³ Loading models β¦") | |
| clf_processor = AutoImageProcessor.from_pretrained("Wvolf/ViT_Deepfake_Detection") | |
| clf_model = AutoModelForImageClassification.from_pretrained( | |
| "Wvolf/ViT_Deepfake_Detection").to(DEVICE) | |
| clf_model.eval() | |
| mae_processor = AutoImageProcessor.from_pretrained("facebook/vit-mae-base") | |
| mae_model = ViTMAEForPreTraining.from_pretrained("facebook/vit-mae-base").to(DEVICE) | |
| mae_model.eval() | |
| mtcnn = MTCNN(image_size=224, margin=20, device=DEVICE, keep_all=False) | |
| face_cascade = cv2.CascadeClassifier( | |
| cv2.data.haarcascades + "haarcascade_frontalface_default.xml") | |
| print("β All models ready!") | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_face_image(pil_img): | |
| face = mtcnn(pil_img) | |
| if face is None: | |
| return pil_img.resize((224, 224)) | |
| return to_pil(((face.clamp(-1,1)+1)/2.0).cpu()).resize((224,224)) | |
| def extract_face_video(bgr): | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(60,60)) | |
| if len(faces) == 0: | |
| return Image.fromarray(cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)).resize((224,224)) | |
| x,y,w,h = max(faces, key=lambda f: f[2]*f[3]) | |
| crop = bgr[max(0,y-20):y+h+20, max(0,x-20):x+w+20] | |
| return Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)).resize((224,224)) | |
| def classify(face_pil): | |
| inp = clf_processor(images=face_pil, return_tensors="pt").to(DEVICE) | |
| with torch.inference_mode(): | |
| probs = torch.softmax(clf_model(**inp).logits, dim=1).squeeze().tolist() | |
| labels = clf_model.config.id2label | |
| fake_idx = next((i for i,v in labels.items() if "fake" in v.lower()), 1) | |
| return probs[fake_idx], probs[1-fake_idx] | |
| def reconstruct(face_pil): | |
| inp = mae_processor(images=face_pil, return_tensors="pt").to(DEVICE) | |
| with torch.inference_mode(): | |
| out = mae_model(**inp) | |
| recon = mae_model.unpatchify(out.logits).squeeze(0).cpu() | |
| orig = inp.pixel_values.squeeze(0).cpu() | |
| recon_dn = (recon*IMG_STD+IMG_MEAN).clamp(0,1) | |
| orig_dn = (orig *IMG_STD+IMG_MEAN).clamp(0,1) | |
| return to_pil(recon_dn), round(torch.mean((orig_dn-recon_dn)**2).item(), 6) | |
| # ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def image_chart(fake_prob, mse): | |
| fig = plt.figure(figsize=(12, 4.2), facecolor="#07080f") | |
| gs = fig.add_gridspec(1, 3, wspace=0.4, left=0.04, right=0.97, | |
| top=0.88, bottom=0.14) | |
| is_fake = fake_prob > 0.5 | |
| c_accent = "#ff3b5c" if is_fake else "#00e5a0" | |
| c_dim = "#151525" | |
| # Donut | |
| ax1 = fig.add_subplot(gs[0]) | |
| ax1.set_facecolor("#07080f") | |
| ax1.pie([fake_prob, 1-fake_prob], colors=[c_accent, c_dim], startangle=90, | |
| wedgeprops=dict(width=0.52, edgecolor="#07080f", linewidth=4)) | |
| ax1.text(0, 0.13, f"{fake_prob*100:.1f}%", ha="center", va="center", | |
| fontsize=24, fontweight="bold", color=c_accent, fontfamily="monospace") | |
| ax1.text(0, -0.22, "FAKE" if is_fake else "REAL", | |
| ha="center", va="center", fontsize=13, fontweight="bold", color=c_accent, | |
| bbox=dict(boxstyle="round,pad=0.35", fc=c_accent+"28", ec=c_accent, lw=1.3)) | |
| ax1.set_title("Classification", color="#9999bb", fontsize=11, fontweight="bold", pad=10) | |
| # Probability bars | |
| ax2 = fig.add_subplot(gs[1]) | |
| ax2.set_facecolor("#07080f") | |
| ax2.set_xlim(0,1); ax2.set_ylim(-0.6, 1.6); ax2.axis("off") | |
| for val, lbl, ypos, col in [(fake_prob,"FAKE",1.1,"#ff3b5c"),(1-fake_prob,"REAL",0.1,"#00e5a0")]: | |
| ax2.add_patch(plt.Rectangle((0, ypos-0.18), 1.0, 0.32, fc="#151525", ec="#222238", lw=1)) | |
| ax2.add_patch(plt.Rectangle((0, ypos-0.18), val, 0.32, fc=col+"bb", ec="none")) | |
| ax2.text(-0.03, ypos-0.01, lbl, ha="right", va="center", | |
| fontsize=10, fontweight="bold", color=col) | |
| ax2.text(val+0.02, ypos-0.01, f"{val:.3f}", ha="left", va="center", | |
| fontsize=10, color="white", fontfamily="monospace") | |
| ax2.set_title("Probabilities", color="#9999bb", fontsize=11, fontweight="bold") | |
| ax2.text(0.5, -0.4, "0.0 ββββββββββββββββ 1.0", | |
| ha="center", va="center", fontsize=8, color="#444466") | |
| # MSE gauge | |
| ax3 = fig.add_subplot(gs[2]) | |
| ax3.set_facecolor("#07080f"); ax3.axis("off") | |
| ax3.set_xlim(0,1); ax3.set_ylim(0,1) | |
| MAX_MSE = 0.06 | |
| fill = min(mse/MAX_MSE, 1.0) | |
| mse_color = "#ff3b5c" if mse > 0.02 else "#00e5a0" | |
| ax3.add_patch(plt.FancyBboxPatch((0.05,0.38), 0.90, 0.18, | |
| boxstyle="round,pad=0.02", fc="#151525", ec="#222238", lw=1.2)) | |
| ax3.add_patch(plt.FancyBboxPatch((0.05,0.38), 0.90*fill, 0.18, | |
| boxstyle="round,pad=0.02", fc=mse_color, ec="none")) | |
| tx = 0.05 + 0.90*(0.02/MAX_MSE) | |
| ax3.plot([tx,tx],[0.34,0.60], color="#ffa94d", lw=2, ls="--", zorder=5) | |
| ax3.text(tx, 0.64, "threshold", ha="center", color="#ffa94d", fontsize=8.5, fontstyle="italic") | |
| ax3.text(0.5, 0.82, f"{mse:.5f}", ha="center", va="center", | |
| fontsize=22, fontweight="bold", color=mse_color, fontfamily="monospace") | |
| ax3.text(0.5, 0.94, "Reconstruction MSE", ha="center", | |
| color="#9999bb", fontsize=11, fontweight="bold") | |
| status = "HIGH βΆ Likely Fake" if mse > 0.02 else "LOW βΆ Likely Real" | |
| ax3.text(0.5, 0.20, status, ha="center", color=mse_color, fontsize=10, fontweight="bold", | |
| bbox=dict(boxstyle="round,pad=0.3", fc=mse_color+"22", ec=mse_color, lw=1)) | |
| return fig | |
| def video_chart(fake_probs, mse_scores): | |
| fig, axes = plt.subplots(2, 1, figsize=(12, 6), facecolor="#07080f", | |
| gridspec_kw={"hspace":0.55,"top":0.93,"bottom":0.10, | |
| "left":0.07,"right":0.97}) | |
| x = list(range(len(fake_probs))) | |
| for ax in axes: | |
| ax.set_facecolor("#0d0e1c") | |
| ax.tick_params(colors="#666688", labelsize=9) | |
| for sp in ax.spines.values(): sp.set_color("#1e1e38") | |
| ax.grid(alpha=0.10, color="#5555aa", lw=0.6, linestyle="--") | |
| ax1, ax2 = axes | |
| ax1.plot(x, fake_probs, color="#ff3b5c", lw=2.5, marker="o", ms=5, | |
| markeredgecolor="#ff8899", markeredgewidth=1, zorder=4) | |
| ax1.axhline(0.5, color="#ffa94d", ls="--", lw=1.8, label="Threshold 0.5", zorder=3) | |
| ax1.fill_between(x, fake_probs, 0.5, where=[f>0.5 for f in fake_probs], | |
| alpha=0.22, color="#ff3b5c", zorder=2, label="Fake zone") | |
| ax1.fill_between(x, fake_probs, 0.5, where=[f<=0.5 for f in fake_probs], | |
| alpha=0.14, color="#00e5a0", zorder=2, label="Real zone") | |
| ax1.set_ylim(-0.05, 1.08) | |
| ax1.set_ylabel("Fake Probability", color="#8888bb", fontsize=10) | |
| ax1.set_title("Frame-by-Frame Fake Probability", color="#ccccee", | |
| fontsize=12, fontweight="bold", pad=9) | |
| ax1.legend(facecolor="#131325", labelcolor="#aaaacc", fontsize=9, | |
| edgecolor="#2a2a48", loc="upper right") | |
| ax1.yaxis.label.set_color("#8888bb") | |
| mean_mse = float(np.mean(mse_scores)) | |
| ax2.plot(x, mse_scores, color="#4b9eff", lw=2.5, marker="s", ms=5, | |
| markeredgecolor="#88ccff", markeredgewidth=1, zorder=4) | |
| ax2.axhline(mean_mse, color="#00e5a0", ls="--", lw=1.8, | |
| label=f"Mean {mean_mse:.4f}", zorder=3) | |
| ax2.fill_between(x, mse_scores, mean_mse, | |
| where=[m>mean_mse for m in mse_scores], | |
| alpha=0.20, color="#ff3b5c", zorder=2, label="Above mean") | |
| ax2.fill_between(x, mse_scores, mean_mse, | |
| where=[m<=mean_mse for m in mse_scores], | |
| alpha=0.12, color="#4b9eff", zorder=2) | |
| ax2.set_xlabel("Frame Index", color="#8888bb", fontsize=10) | |
| ax2.set_ylabel("Reconstruction MSE", color="#8888bb", fontsize=10) | |
| ax2.set_title("Reconstruction Error per Frame", color="#ccccee", | |
| fontsize=12, fontweight="bold", pad=9) | |
| ax2.legend(facecolor="#131325", labelcolor="#aaaacc", fontsize=9, | |
| edgecolor="#2a2a48", loc="upper right") | |
| ax2.xaxis.label.set_color("#8888bb") | |
| ax2.yaxis.label.set_color("#8888bb") | |
| return fig | |
| # ββ Predictions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def predict_image(image): | |
| if image is None: | |
| return "β οΈ Please upload an image first.", None, None, None, 0.0, 0.0 | |
| pil = Image.fromarray(image).convert("RGB") | |
| face = extract_face_image(pil) | |
| fake_p, real_p = classify(face) | |
| recon, mse = reconstruct(face) | |
| is_fake = fake_p > 0.5 | |
| icon = "π΄ DEEPFAKE DETECTED" if is_fake else "π’ AUTHENTIC β Real Face" | |
| conf = ("β οΈ Very high confidence β likely manipulated" if fake_p > 0.85 else | |
| "β‘ High confidence β probably fake" if fake_p > 0.65 else | |
| "π Borderline β manual review recommended" if fake_p > 0.5 else | |
| "β Low fake score β appears genuine" if fake_p > 0.25 else | |
| "β Very low fake score β highly authentic") | |
| verdict = ( | |
| f"{icon}\n" | |
| f"{'β'*40}\n" | |
| f" Fake probability β {fake_p:.4f}\n" | |
| f" Real probability β {real_p:.4f}\n" | |
| f" Reconstruction MSE β {mse:.6f}\n" | |
| f"{'β'*40}\n" | |
| f" {conf}" | |
| ) | |
| return verdict, face, recon, image_chart(fake_p, mse), round(fake_p,4), mse | |
| def predict_video(video_path): | |
| if video_path is None: | |
| return "β οΈ Please upload a video first.", None, 0.0, 0.0 | |
| cap = cv2.VideoCapture(video_path) | |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| n_samples = min(25, max(1, total)) | |
| sample_idx = set(np.linspace(0, total-1, n_samples, dtype=int)) | |
| fake_probs, mse_scores = [], [] | |
| idx = 0 | |
| while cap.isOpened(): | |
| ret, frame = cap.read() | |
| if not ret: break | |
| if idx in sample_idx: | |
| try: | |
| face = extract_face_video(frame) | |
| fp, _ = classify(face) | |
| _, ms = reconstruct(face) | |
| fake_probs.append(round(fp,4)) | |
| mse_scores.append(round(ms,6)) | |
| except Exception: pass | |
| idx += 1 | |
| cap.release() | |
| if not fake_probs: | |
| return "β οΈ No faces detected in video.", None, 0.0, 0.0 | |
| avg_f = float(np.mean(fake_probs)) | |
| avg_m = float(np.mean(mse_scores)) | |
| n_fake = sum(1 for f in fake_probs if f > 0.5) | |
| pct = n_fake / len(fake_probs) * 100 | |
| is_fake = avg_f > 0.5 | |
| icon = "π΄ DEEPFAKE DETECTED" if is_fake else "π’ AUTHENTIC β Real Video" | |
| conf = ("β οΈ Very high confidence β likely manipulated" if avg_f > 0.85 else | |
| "β‘ High confidence β probably fake" if avg_f > 0.65 else | |
| "π Borderline β manual review recommended" if avg_f > 0.5 else | |
| "β Low fake score β appears genuine") | |
| verdict = ( | |
| f"{icon}\n" | |
| f"{'β'*40}\n" | |
| f" Avg fake probability β {avg_f:.4f}\n" | |
| f" Avg reconstruction MSE β {avg_m:.6f}\n" | |
| f" Fake frames detected β {n_fake}/{len(fake_probs)} ({pct:.1f}%)\n" | |
| f" Total frames analyzed β {len(fake_probs)} / {total}\n" | |
| f"{'β'*40}\n" | |
| f" {conf}" | |
| ) | |
| return verdict, video_chart(fake_probs, mse_scores), round(avg_f,4), round(avg_m,6) | |
| # ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); | |
| *, *::before, *::after { box-sizing: border-box; } | |
| body, .gradio-container { | |
| background: radial-gradient(ellipse at 20% 20%, #0e0a1f 0%, #07080f 60%, #050810 100%) !important; | |
| font-family: 'Inter', sans-serif !important; min-height: 100vh; | |
| } | |
| .app-header { | |
| background: linear-gradient(135deg, #130a2e 0%, #0a1530 40%, #0a1a1a 100%); | |
| border: 1px solid rgba(120,80,255,0.25); border-radius: 20px; | |
| padding: 36px 40px 28px; margin-bottom: 28px; text-align: center; | |
| position: relative; overflow: hidden; | |
| box-shadow: 0 0 60px rgba(100,60,255,0.12), inset 0 1px 0 rgba(255,255,255,0.06); | |
| } | |
| .app-header::before { | |
| content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; | |
| background: linear-gradient(90deg, transparent, #7b4fff, #ff3b5c, #7b4fff, transparent); | |
| } | |
| .app-header h1 { | |
| font-size: 2.4rem !important; font-weight: 800 !important; | |
| background: linear-gradient(90deg, #ff3b5c 0%, #ff9f43 40%, #7b4fff 80%, #ff3b5c 100%) !important; | |
| background-size: 200% !important; | |
| -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; letter-spacing: -0.5px !important; margin-bottom: 8px !important; | |
| } | |
| .app-header .subtitle { color: #7777aa !important; font-size: 14px !important; margin-bottom: 18px !important; } | |
| .badge-row { display:flex; justify-content:center; gap:10px; flex-wrap:wrap; } | |
| .badge { padding: 5px 14px; border-radius: 20px; font-size: 12px; font-weight: 600; letter-spacing: 0.3px; } | |
| .badge-purple { background:#7b4fff18; color:#a57fff; border:1px solid #7b4fff44; } | |
| .badge-red { background:#ff3b5c18; color:#ff7090; border:1px solid #ff3b5c44; } | |
| .badge-green { background:#00e5a018; color:#00e5a0; border:1px solid #00e5a044; } | |
| .badge-blue { background:#4b9eff18; color:#7bb8ff; border:1px solid #4b9eff44; } | |
| .gr-block, .block, .panel, [class*="block"] { | |
| background: #0d0e1c !important; border: 1px solid #1a1b30 !important; | |
| border-radius: 16px !important; box-shadow: 0 4px 24px rgba(0,0,0,0.4) !important; | |
| } | |
| button.primary, .gr-button-primary { | |
| background: linear-gradient(135deg, #ff3b5c 0%, #cc2244 100%) !important; | |
| border: none !important; border-radius: 12px !important; font-weight: 700 !important; | |
| font-size: 15px !important; letter-spacing: 0.5px !important; height: 52px !important; | |
| box-shadow: 0 4px 20px #ff3b5c44 !important; transition: all 0.25s ease !important; color: white !important; | |
| } | |
| button.primary:hover { | |
| transform: translateY(-2px) !important; box-shadow: 0 8px 32px #ff3b5c66 !important; | |
| background: linear-gradient(135deg, #ff5572 0%, #ee3355 100%) !important; | |
| } | |
| textarea, input[type="text"] { | |
| background: #080912 !important; color: #d0d0f0 !important; | |
| border: 1px solid #1e1f38 !important; border-radius: 10px !important; | |
| font-family: 'JetBrains Mono', monospace !important; font-size: 13px !important; line-height: 1.8 !important; | |
| } | |
| input[type="number"] { | |
| background: #080912 !important; color: #00e5a0 !important; | |
| border: 1px solid #1e1f38 !important; border-radius: 10px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 18px !important; font-weight: 700 !important; text-align: center !important; | |
| } | |
| label > span, .label-wrap span { | |
| color: #555577 !important; font-size: 11px !important; | |
| text-transform: uppercase !important; letter-spacing: 0.8px !important; font-weight: 600 !important; | |
| } | |
| .tab-nav { border-bottom: 1px solid #1a1b30 !important; } | |
| .tab-nav button { color: #555577 !important; font-weight: 600 !important; font-size: 14px !important; | |
| padding: 12px 24px !important; border-radius: 10px 10px 0 0 !important; } | |
| .tab-nav button.selected { | |
| color: #ffffff !important; background: linear-gradient(180deg, #1a1030 0%, #0d0e1c 100%) !important; | |
| border-bottom: 2px solid #ff3b5c !important; | |
| } | |
| .section-divider { border: none; border-top: 1px solid #1a1b30; margin: 8px 0; } | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: #07080f; } | |
| ::-webkit-scrollbar-thumb { background: #2a2a44; border-radius: 3px; } | |
| """ | |
| # ββ UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(css=CSS, title="Deepfake Detector") as demo: | |
| gr.HTML(""" | |
| <div class="app-header"> | |
| <h1>π΅οΈ Deepfake Face Detector</h1> | |
| <p class="subtitle">AI-powered detection using Vision Transformer classification and Masked Autoencoder reconstruction</p> | |
| <div class="badge-row"> | |
| <span class="badge badge-purple">π§ ViT Model</span> | |
| <span class="badge badge-red">β‘ 98.7% Accuracy</span> | |
| <span class="badge badge-blue">π MAE Autoencoder</span> | |
| <span class="badge badge-green">ποΈ MTCNN Face Detection</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("πΌοΈ Image Detection"): | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1, min_width=320): | |
| img_in = gr.Image(label="Upload Image", height=320) | |
| img_btn = gr.Button("π Analyze Image", variant="primary") | |
| with gr.Column(scale=1, min_width=320): | |
| img_verdict = gr.Textbox(label="Detection Result", lines=10, | |
| interactive=False, placeholder="Upload an image and click Analyzeβ¦") | |
| with gr.Row(): | |
| img_fake_score = gr.Number(label="Fake Probability", precision=4) | |
| img_mse_score = gr.Number(label="Reconstruction MSE", precision=6) | |
| gr.HTML('<hr class="section-divider"/>') | |
| with gr.Row(): | |
| img_face = gr.Image(label="π Detected Face β Model Input", height=260) | |
| img_recon = gr.Image(label="π Reconstructed Face β MAE Output", height=260) | |
| img_chart = gr.Plot(label="π Score Analysis Dashboard") | |
| img_btn.click(fn=predict_image, inputs=[img_in], | |
| outputs=[img_verdict, img_face, img_recon, img_chart, img_fake_score, img_mse_score]) | |
| with gr.Tab("π¬ Video Detection"): | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1, min_width=320): | |
| vid_in = gr.Video(label="Upload Video", height=320) | |
| vid_btn = gr.Button("π Analyze Video", variant="primary") | |
| with gr.Column(scale=1, min_width=320): | |
| vid_verdict = gr.Textbox(label="Detection Result", lines=10, | |
| interactive=False, placeholder="Upload a video and click Analyzeβ¦") | |
| with gr.Row(): | |
| vid_fake_score = gr.Number(label="Avg Fake Probability", precision=4) | |
| vid_mse_score = gr.Number(label="Avg Reconstruction MSE", precision=6) | |
| gr.HTML('<hr class="section-divider"/>') | |
| vid_chart = gr.Plot(label="π Frame-by-Frame Analysis Dashboard") | |
| vid_btn.click(fn=predict_video, inputs=[vid_in], | |
| outputs=[vid_verdict, vid_chart, vid_fake_score, vid_mse_score]) | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) |