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("""

🕵️ Deepfake Face Detector

AI-powered detection using Vision Transformer classification and Masked Autoencoder reconstruction

🧠 ViT Model ⚡ 98.7% Accuracy 🔄 MAE Autoencoder 👁️ MTCNN Face Detection
""") 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('
') 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('
') 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)