import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image, ImageFilter, ImageEnhance, ImageStat import gradio as gr import numpy as np import time import io import base64 # ── GLOBAL DETERMINISM SETTINGS ────────────────────────────────────────────── torch.manual_seed(42) np.random.seed(42) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False torch.use_deterministic_algorithms(True, warn_only=True) if torch.cuda.is_available(): torch.cuda.manual_seed_all(42) MODEL_PATH = "best_model_rebuilt.pth" CLASS_NAMES = ["fake", "real"] def build_efficientnet_b3(num_classes=2): model = models.efficientnet_b3(weights=None) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(p=0.3, inplace=True), nn.Linear(in_features, 512), nn.ReLU(), nn.Dropout(p=0.3), nn.Linear(512, num_classes) ) return model device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_efficientnet_b3(num_classes=2) state = torch.load(MODEL_PATH, map_location=device) if isinstance(state, dict) and "model_state_dict" in state: model.load_state_dict(state["model_state_dict"]) else: model.load_state_dict(state) model = model.to(device) # ── FORCE EVAL MODE + EXPLICITLY DISABLE ALL DROPOUT ───────────────────────── model.eval() for module in model.modules(): if isinstance(module, nn.Dropout): module.eval() transform = transforms.Compose([ transforms.Resize((300, 300)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def analyze_image_properties(image): img_array = np.array(image) gray = np.mean(img_array, axis=2) noise_level = float(np.std(np.diff(gray.flatten()[:10000]))) noise_score = min(100, noise_level * 4) r_std = float(np.std(img_array[:, :, 0])) g_std = float(np.std(img_array[:, :, 1])) b_std = float(np.std(img_array[:, :, 2])) color_variance = (r_std + g_std + b_std) / 3.0 color_score = min(100, color_variance / 2.8) blurred = np.array(image.filter(ImageFilter.FIND_EDGES)) sharpness = float(np.var(blurred)) sharp_score = min(100, sharpness / 25.0) hist = np.histogram(gray, bins=64)[0] hist_norm = hist / hist.sum() hist_norm = hist_norm[hist_norm > 0] entropy = float(-np.sum(hist_norm * np.log2(hist_norm))) # low entropy = suspicious (AI images are smoother) entropy_score = max(0, min(100, 100 - (entropy * 10.0))) w = image.width left = np.array(image.crop((0, 0, w // 2, image.height)).resize((64, 64))) right = np.array(image.crop((w // 2, 0, w, image.height)).resize((64, 64))) right_flipped = right[:, ::-1, :] symmetry_diff = float(np.mean(np.abs(left.astype(float) - right_flipped.astype(float)))) symmetry_score = max(0, min(100, 100 - symmetry_diff * 1.2)) return { "noise": round(noise_score, 1), "color": round(color_score, 1), "sharpness": round(sharp_score, 1), "entropy": round(entropy_score, 1), "symmetry": round(symmetry_score, 1), } def get_image_info(image): w, h = image.size mode = image.mode arr = np.array(image) mean_bright = float(np.mean(arr)) return { "width": w, "height": h, "mode": mode, "brightness": round(mean_bright, 1), "aspect": f"{round(w / h, 2)}:1", "megapixels": round((w * h) / 1_000_000, 2), } def predict_image(image): if image is None: return _await_html() # ── PER-CALL DETERMINISM: reset seeds before every inference ───────────── torch.manual_seed(42) np.random.seed(42) if torch.cuda.is_available(): torch.cuda.manual_seed_all(42) # ── SAFETY: re-enforce eval mode on every call ─────────────────────────── model.eval() for module in model.modules(): if isinstance(module, nn.Dropout): module.eval() t0 = time.time() image = image.convert("RGB") input_tensor = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(input_tensor) probabilities = torch.softmax(outputs, dim=1)[0] fake_prob = probabilities[0].item() real_prob = probabilities[1].item() prediction_index = int(torch.argmax(probabilities).item()) prediction_label = CLASS_NAMES[prediction_index] inference_ms = round((time.time() - t0) * 1000, 1) confidence = max(fake_prob, real_prob) * 100 # ── CERTAINTY TIERS ────────────────────────────────────────────────────── if confidence < 75: certainty = "UNCERTAIN" certainty_color = "#94A3B8" is_uncertain = True elif confidence < 85: certainty = "MODERATE" certainty_color = "#FBBF24" is_uncertain = False else: certainty = "HIGH" certainty_color = "#F87171" if prediction_label == "fake" else "#34D399" is_uncertain = False signals = analyze_image_properties(image) img_info = get_image_info(image) # ── VERDICT STYLING ────────────────────────────────────────────────────── if is_uncertain: verdict_color = "#94A3B8" verdict_bg = "rgba(148,163,184,0.08)" verdict_border = "rgba(148,163,184,0.35)" ring_shadow = "rgba(148,163,184,0.3)" verdict_text = "UNCERTAIN" verdict_icon = "?" elif prediction_label == "fake": verdict_color = "#EF4444" verdict_bg = "rgba(239,68,68,0.08)" verdict_border = "rgba(239,68,68,0.35)" ring_shadow = "rgba(239,68,68,0.3)" verdict_text = "AI-GENERATED" verdict_icon = "⚠" else: verdict_color = "#10B981" verdict_bg = "rgba(16,185,129,0.08)" verdict_border = "rgba(16,185,129,0.35)" ring_shadow = "rgba(16,185,129,0.3)" verdict_text = "AUTHENTIC" verdict_icon = "✓" # ── INTERPRETATION SENTENCE ────────────────────────────────────────────── if is_uncertain: interp = f"Result is inconclusive — model confidence ({confidence:.1f}%) is below the 75% threshold. Manual review recommended." elif prediction_label == "fake" and confidence >= 85: interp = "Strong indicators of AI synthesis detected across multiple signal channels." elif prediction_label == "fake": interp = "Several patterns consistent with generative model artifacts were detected." elif confidence >= 85: interp = "No significant synthetic artifacts detected. Image appears camera-captured." else: interp = "Mostly authentic characteristics with minor ambiguous regions." fake_fill_w = f"{fake_prob * 100:.1f}%" real_fill_w = f"{real_prob * 100:.1f}%" def sig_bar(label, val, tooltip): color = "#EF4444" if val > 70 else "#FBBF24" if val > 40 else "#10B981" return f"""
{label}
{val}
""" sig_html = ( sig_bar("NOISE PATTERN", signals["noise"], "Irregular noise may indicate GAN artifacts") + sig_bar("COLOR DIST.", signals["color"], "Color distribution variance across channels") + sig_bar("SHARPNESS", signals["sharpness"], "Unnatural sharpness can indicate synthesis") + sig_bar("PIXEL ENTROPY", signals["entropy"], "Low entropy (smooth pixels) suggests AI generation") + sig_bar("FACE SYMMETRY", signals["symmetry"], "AI faces tend to be unusually symmetric") ) def gauge_svg(pct, color): r = 36; cx = 44; cy = 44 circ = 2 * 3.14159 * r dash = circ * pct / 100 return f""" {pct:.0f}% """ fake_gauge = gauge_svg(fake_prob * 100, "#EF4444") real_gauge = gauge_svg(real_prob * 100, "#10B981") result_html = f"""
{verdict_icon}
FORENSIC VERDICT
{verdict_text}
{interp}
{confidence:.1f}% {certainty}
SYNTHETIC PROB.
{fake_gauge}
AUTHENTIC PROB.
{real_gauge}
SYNTHETIC
{fake_prob*100:.1f}%
AUTHENTIC
{real_prob*100:.1f}%
INFERENCE TIME {inference_ms} ms
FORENSIC SIGNAL ANALYSIS Higher = more suspicious
{sig_html}
IMAGE METADATA
DIMENSIONS{img_info['width']} × {img_info['height']} px
MEGAPIXELS{img_info['megapixels']} MP
ASPECT RATIO{img_info['aspect']}
COLOR MODE{img_info['mode']}
BRIGHTNESS{img_info['brightness']}/255
DEVICE{'GPU' if device.type == 'cuda' else 'CPU'}
""" return result_html def _await_html(): return """
AWAITING SCAN
Upload an image and run forensic scan
""" custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'); *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} body,.gradio-container{background:#080A14 !important;font-family:'Space Grotesk',sans-serif !important;color:#E2E8F0 !important;min-height:100vh;} .gradio-container{max-width:1200px !important;width:100% !important;margin:0 auto !important;padding:0 24px 60px !important;} .contain{max-width:1200px !important} .app-header{text-align:center;padding:44px 0 32px;position:relative} .app-eyebrow{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:4px;color:#06B6D4;margin-bottom:12px} .app-title{font-size:clamp(32px,5vw,54px);font-weight:700;letter-spacing:-2px;line-height:1;background:linear-gradient(135deg,#E2E8F0 0%,#A78BFA 55%,#06B6D4 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:10px} .app-title-sub{display:block;font-size:clamp(14px,2vw,18px);font-weight:300;letter-spacing:6px;color:#475569;margin-top:6px} .app-subtitle{font-size:14px;color:#475569;max-width:560px;margin:12px auto 0;line-height:1.7} .stats-bar{display:flex;justify-content:center;gap:32px;margin:24px 0 0;flex-wrap:wrap} .stat-item{display:flex;flex-direction:column;align-items:center;gap:2px} .stat-num{font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:700;color:#A78BFA} .stat-lbl{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;color:#334155} .glow-orb{position:fixed;border-radius:50%;filter:blur(100px);pointer-events:none;z-index:-1;opacity:.09} .orb-v{width:500px;height:500px;background:#7C3AED;top:-120px;left:-120px} .orb-c{width:360px;height:360px;background:#06B6D4;bottom:-60px;right:-60px} .orb-m{width:280px;height:280px;background:#EC4899;top:40%;right:20%} .main-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start} @media(max-width:780px){.main-grid{grid-template-columns:1fr}} .sec-label{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:3px;color:#334155;margin-bottom:6px} .gradio-container [data-testid="image"]{border:1.5px dashed rgba(124,58,237,.4) !important;border-radius:16px !important;background:rgba(10,12,24,.7) !important;transition:all .25s !important;min-height:340px !important;} .gradio-container [data-testid="image"]:hover{border-color:rgba(124,58,237,.75) !important;background:rgba(124,58,237,.05) !important;} #scan-btn{width:100% !important;padding:15px !important;border-radius:12px !important;border:none !important;background:linear-gradient(135deg,#7C3AED 0%,#4F46E5 50%,#0891B2 100%) !important;color:#fff !important;font-family:'Space Grotesk',sans-serif !important;font-size:14px !important;font-weight:600 !important;letter-spacing:2px !important;text-transform:uppercase !important;cursor:pointer !important;box-shadow:0 0 28px rgba(124,58,237,.35) !important;transition:all .2s !important;} #scan-btn:hover{transform:translateY(-2px) !important;box-shadow:0 0 44px rgba(124,58,237,.55) !important} #scan-btn:active{transform:translateY(0) !important} .gradio-container button.secondary{background:rgba(15,20,40,.8) !important;border:1px solid rgba(124,58,237,.2) !important;color:#475569 !important;border-radius:10px !important;font-family:'Space Grotesk',sans-serif !important;transition:all .2s !important;} .gradio-container button.secondary:hover{border-color:rgba(124,58,237,.5) !important;color:#E2E8F0 !important} .info-cards{display:grid;grid-template-columns:1fr 1fr;gap:10px} .info-card{background:rgba(15,18,32,.7);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:12px 14px} .info-card-title{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;color:#334155;margin-bottom:6px} .info-card-val{font-family:'JetBrains Mono',monospace;font-size:13px;color:#7C3AED;font-weight:600} .info-card-desc{font-size:11px;color:#475569;margin-top:3px;line-height:1.5} .gradio-container .prose,.gradio-html{background:transparent !important;border:none !important;padding:0 !important} .await-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 20px;gap:12px;border:1px dashed rgba(124,58,237,.2);border-radius:16px;background:rgba(10,12,24,.5)} .await-icon{font-size:36px;opacity:.2} .await-text{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:4px;color:#334155} .await-sub{font-size:12px;color:#1E293B;letter-spacing:.5px} .result-wrap{display:flex;flex-direction:column;gap:12px} .verdict-banner{border-radius:14px;padding:18px 20px;display:flex;align-items:center;gap:16px;position:relative;overflow:hidden} .verdict-ring{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:2px solid;background:rgba(0,0,0,.2)} .verdict-body{flex:1} .v-eyebrow{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:3px;color:#475569;margin-bottom:3px} .v-main{font-size:22px;font-weight:700;letter-spacing:-.5px;margin-bottom:4px} .v-sub{font-size:12px;color:#64748B;line-height:1.5;max-width:280px} .verdict-badge{border-radius:10px;padding:8px 14px;display:flex;flex-direction:column;align-items:center;flex-shrink:0} .badge-conf{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:700;line-height:1} .badge-tier{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;margin-top:2px} .metrics-row{display:flex;gap:10px} .gauge-card{background:rgba(10,11,20,.7);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;align-items:center;gap:4px;flex:0 0 auto} .gauge-label{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:2px;color:#334155;text-align:center} .bars-card{flex:1;background:rgba(10,11,20,.7);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;justify-content:center} .prob-row{display:flex;align-items:center;gap:10px} .prob-name{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;width:72px;flex-shrink:0} .prob-track{flex:1;height:5px;background:rgba(255,255,255,.06);border-radius:999px;overflow:hidden} .prob-fill{height:100%;border-radius:999px;transition:width .8s cubic-bezier(.4,0,.2,1)} .prob-val{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:600;width:42px;text-align:right;flex-shrink:0} .fake-col{color:#F87171}.real-col{color:#34D399} .infer-row{display:flex;justify-content:space-between;padding-top:8px;border-top:1px solid rgba(255,255,255,.05);margin-top:2px} .infer-label{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;color:#334155} .infer-val{font-family:'JetBrains Mono',monospace;font-size:10px;color:#7C3AED} .section-card{background:rgba(10,11,20,.7);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:14px 16px} .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} .card-title{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:3px;color:#475569} .card-hint{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:1px;color:#1E293B} .sig-grid{display:flex;flex-direction:column;gap:9px} .sig-row{display:flex;align-items:center;gap:10px;cursor:help} .sig-name{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:1.5px;color:#475569;width:90px;flex-shrink:0} .sig-track{flex:1;height:4px;background:rgba(255,255,255,.05);border-radius:999px;overflow:hidden} .sig-fill{height:100%;border-radius:999px;transition:width .6s ease} .sig-val{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600;width:30px;text-align:right;flex-shrink:0} .meta-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px} .meta-item{display:flex;flex-direction:column;gap:2px} .meta-k{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:1.5px;color:#334155} .meta-v{font-family:'JetBrains Mono',monospace;font-size:11px;color:#94A3B8;font-weight:500} .scan-footer{display:flex;gap:16px;flex-wrap:wrap;padding:10px 0 0} .scan-footer span{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:1.5px;color:#1E293B} .disclaimer{margin-top:20px;padding:14px 18px;background:rgba(15,18,32,.5);border:1px solid rgba(255,255,255,.04);border-radius:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#1E293B;line-height:1.8} .disclaimer strong{color:#334155} .gradio-container label{font-family:'JetBrains Mono',sans-serif !important;color:#334155 !important;font-size:10px !important;letter-spacing:2px !important;text-transform:uppercase !important;} footer{display:none !important} """ header_html = """
// forensic image analysis · v1.0
DeepFake Detector FORENSIC · NEURAL · ANALYSIS
EfficientNet-B3 model trained to classify AI-generated vs. authentic images. Includes real-time forensic signal extraction and image metadata analysis.
B3EfficientNet
300pxInput Res.
5Signal Channels
2Classes
""" info_cards_html = """
MODEL ARCHITECTURE
EfficientNet-B3
Compound-scaled CNN with dual dropout classifier head
FORENSIC SIGNALS
5 Channels
Noise, color, sharpness, entropy, face symmetry
DETECTION SCOPE
GAN · Diffusion
Trained on real/fake datasets + AI-generated augmentation
CONFIDENCE THRESHOLD
< 75% = Uncertain
Results below 75% confidence are flagged as inconclusive
""" disclaimer_html = """
DISCLAIMER · This tool is a detection aid based on statistical patterns and is not a forensic-grade instrument. Results may vary across image types, compression levels, and generation methods. Forensic signals are heuristic-based approximations. Do not use as sole evidence in any formal process.
""" with gr.Blocks(title="DeepFake Detector") as demo: gr.HTML(header_html) with gr.Row(elem_classes=["main-grid"]): with gr.Column(scale=1): gr.HTML('
Input · Upload Image
') image_input = gr.Image(type="pil", label="", show_label=False, height=340) scan_btn = gr.Button("⬡ RUN FORENSIC SCAN", variant="primary", elem_id="scan-btn") gr.HTML(info_cards_html) with gr.Column(scale=1): gr.HTML('
Output · Analysis Result
') result_output = gr.HTML( value="""
AWAITING SCAN
Upload an image and run forensic scan
""", show_label=False, ) gr.HTML(disclaimer_html) scan_btn.click(fn=predict_image, inputs=[image_input], outputs=[result_output]) if __name__ == "__main__": demo.launch(css=custom_css, server_name="0.0.0.0")