HaseebArif11's picture
Update app.py
998e927 verified
Raw
History Blame Contribute Delete
25.3 kB
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"""
<div class="sig-row" title="{tooltip}">
<span class="sig-name">{label}</span>
<div class="sig-track">
<div class="sig-fill" style="width:{val}%;background:{color};box-shadow:0 0 6px {color}55"></div>
</div>
<span class="sig-val" style="color:{color}">{val}</span>
</div>"""
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"""<svg width="88" height="88" viewBox="0 0 88 88">
<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="7"/>
<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="{color}" stroke-width="7"
stroke-dasharray="{dash:.1f} {circ:.1f}" stroke-dashoffset="{circ/4:.1f}"
stroke-linecap="round" style="filter:drop-shadow(0 0 4px {color})"/>
<text x="{cx}" y="{cy+5}" text-anchor="middle" font-family="JetBrains Mono,monospace"
font-size="13" font-weight="700" fill="{color}">{pct:.0f}%</text>
</svg>"""
fake_gauge = gauge_svg(fake_prob * 100, "#EF4444")
real_gauge = gauge_svg(real_prob * 100, "#10B981")
result_html = f"""
<div class="result-wrap">
<div class="verdict-banner" style="background:{verdict_bg};border:1px solid {verdict_border}">
<div class="verdict-ring" style="border-color:{verdict_color};box-shadow:0 0 22px {ring_shadow}">
<span style="font-size:24px">{verdict_icon}</span>
</div>
<div class="verdict-body">
<div class="v-eyebrow">FORENSIC VERDICT</div>
<div class="v-main" style="color:{verdict_color}">{verdict_text}</div>
<div class="v-sub">{interp}</div>
</div>
<div class="verdict-badge" style="background:{verdict_color}22;border:1px solid {verdict_color}55;color:{verdict_color}">
<span class="badge-conf">{confidence:.1f}%</span>
<span class="badge-tier" style="color:{certainty_color}">{certainty}</span>
</div>
</div>
<div class="metrics-row">
<div class="gauge-card">
<div class="gauge-label">SYNTHETIC PROB.</div>
{fake_gauge}
</div>
<div class="gauge-card">
<div class="gauge-label">AUTHENTIC PROB.</div>
{real_gauge}
</div>
<div class="bars-card">
<div class="prob-row">
<span class="prob-name fake-col">SYNTHETIC</span>
<div class="prob-track">
<div class="prob-fill" style="width:{fake_fill_w};background:linear-gradient(90deg,#7F1D1D,#EF4444);box-shadow:0 0 6px rgba(239,68,68,.4)"></div>
</div>
<span class="prob-val fake-col">{fake_prob*100:.1f}%</span>
</div>
<div class="prob-row">
<span class="prob-name real-col">AUTHENTIC</span>
<div class="prob-track">
<div class="prob-fill" style="width:{real_fill_w};background:linear-gradient(90deg,#064E3B,#10B981);box-shadow:0 0 6px rgba(16,185,129,.4)"></div>
</div>
<span class="prob-val real-col">{real_prob*100:.1f}%</span>
</div>
<div class="infer-row">
<span class="infer-label">INFERENCE TIME</span>
<span class="infer-val">{inference_ms} ms</span>
</div>
</div>
</div>
<div class="section-card">
<div class="card-header">
<span class="card-title">FORENSIC SIGNAL ANALYSIS</span>
<span class="card-hint">Higher = more suspicious</span>
</div>
<div class="sig-grid">{sig_html}</div>
</div>
<div class="section-card">
<div class="card-header">
<span class="card-title">IMAGE METADATA</span>
</div>
<div class="meta-grid">
<div class="meta-item"><span class="meta-k">DIMENSIONS</span><span class="meta-v">{img_info['width']} × {img_info['height']} px</span></div>
<div class="meta-item"><span class="meta-k">MEGAPIXELS</span><span class="meta-v">{img_info['megapixels']} MP</span></div>
<div class="meta-item"><span class="meta-k">ASPECT RATIO</span><span class="meta-v">{img_info['aspect']}</span></div>
<div class="meta-item"><span class="meta-k">COLOR MODE</span><span class="meta-v">{img_info['mode']}</span></div>
<div class="meta-item"><span class="meta-k">BRIGHTNESS</span><span class="meta-v">{img_info['brightness']}/255</span></div>
<div class="meta-item"><span class="meta-k">DEVICE</span><span class="meta-v">{'GPU' if device.type == 'cuda' else 'CPU'}</span></div>
</div>
</div>
<div class="scan-footer">
<span>MODEL · EfficientNet-B3</span>
<span>BACKBONE · ImageNet</span>
<span>FRAMEWORK · PyTorch</span>
<span>VERSION · 1.0.0</span>
</div>
</div>
"""
return result_html
def _await_html():
return """<div class="await-state">
<div class="await-icon">⬡</div>
<div class="await-text">AWAITING SCAN</div>
<div class="await-sub">Upload an image and run forensic scan</div>
</div>"""
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 = """
<div class="glow-orb orb-v"></div>
<div class="glow-orb orb-c"></div>
<div class="glow-orb orb-m"></div>
<div class="app-header">
<div class="app-eyebrow">// forensic image analysis · v1.0</div>
<div class="app-title">DeepFake Detector
<span class="app-title-sub">FORENSIC · NEURAL · ANALYSIS</span>
</div>
<div class="app-subtitle">
EfficientNet-B3 model trained to classify AI-generated vs. authentic images.
Includes real-time forensic signal extraction and image metadata analysis.
</div>
<div class="stats-bar">
<div class="stat-item"><span class="stat-num">B3</span><span class="stat-lbl">EfficientNet</span></div>
<div class="stat-item"><span class="stat-num">300px</span><span class="stat-lbl">Input Res.</span></div>
<div class="stat-item"><span class="stat-num">5</span><span class="stat-lbl">Signal Channels</span></div>
<div class="stat-item"><span class="stat-num">2</span><span class="stat-lbl">Classes</span></div>
</div>
</div>
"""
info_cards_html = """
<div class="info-cards">
<div class="info-card">
<div class="info-card-title">MODEL ARCHITECTURE</div>
<div class="info-card-val">EfficientNet-B3</div>
<div class="info-card-desc">Compound-scaled CNN with dual dropout classifier head</div>
</div>
<div class="info-card">
<div class="info-card-title">FORENSIC SIGNALS</div>
<div class="info-card-val">5 Channels</div>
<div class="info-card-desc">Noise, color, sharpness, entropy, face symmetry</div>
</div>
<div class="info-card">
<div class="info-card-title">DETECTION SCOPE</div>
<div class="info-card-val">GAN · Diffusion</div>
<div class="info-card-desc">Trained on real/fake datasets + AI-generated augmentation</div>
</div>
<div class="info-card">
<div class="info-card-title">CONFIDENCE THRESHOLD</div>
<div class="info-card-val">&lt; 75% = Uncertain</div>
<div class="info-card-desc">Results below 75% confidence are flagged as inconclusive</div>
</div>
</div>
"""
disclaimer_html = """
<div class="disclaimer">
<strong>DISCLAIMER ·</strong> 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.
</div>
"""
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('<div class="sec-label">Input · Upload Image</div>')
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('<div class="sec-label">Output · Analysis Result</div>')
result_output = gr.HTML(
value="""<div class="await-state">
<div class="await-icon">⬡</div>
<div class="await-text">AWAITING SCAN</div>
<div class="await-sub">Upload an image and run forensic scan</div>
</div>""",
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")