ethix's picture
feat: performance overhaul, client-side report, result cache, streaming cards, UX polish
05f720a
Raw History Blame Contribute Delete
54.4 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DeepfakeDet-ViT — XAI & Digital Forensics Suite</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<!-- html2canvas -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
colors: {
brand: {
50: '#f0fdfa',
500: '#14b8a6',
600: '#0d9488',
900: '#134e4a',
}
}
}
}
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.glass-panel {
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(51, 65, 85, 0.5);
}
.custom-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: #0f172a; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton-shimmer {
background: linear-gradient(90deg, rgba(30,41,59,0.5) 25%, rgba(51,65,85,0.8) 50%, rgba(30,41,59,0.5) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
</style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col selection:bg-teal-500 selection:text-white">
<!-- Header Banner -->
<header class="border-b border-slate-800 bg-slate-900/80 sticky top-0 z-40 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-teal-500 to-emerald-400 flex items-center justify-center shadow-lg shadow-teal-500/20">
<i class="fa-solid fa-microscope text-slate-950 text-lg"></i>
</div>
<div>
<h1 class="font-bold text-lg text-slate-100 tracking-tight flex items-center gap-2">
DeepfakeDet-ViT <span class="text-xs px-2 py-0.5 rounded-full bg-teal-500/10 text-teal-400 font-mono border border-teal-500/20">XAI & Forensics Suite</span>
</h1>
<p class="text-xs text-slate-400">ViT-Small Deepfake Classifier (384×384) — Attribution & Digital Artifact Analysis</p>
</div>
</div>
<div class="flex items-center space-x-3">
<a href="https://huggingface.co/buildborderless/CommunityForensics-DeepfakeDet-ViT" target="_blank" class="text-xs px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition flex items-center gap-1.5 border border-slate-700">
<i class="fa-brands fa-huggingface"></i> HF Model Hub
</a>
</div>
</div>
</header>
<!-- Navigation Tabs -->
<div class="bg-slate-900/50 border-b border-slate-800/80 sticky top-[61px] z-30 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-4 flex space-x-2 overflow-x-auto custom-scrollbar">
<button onclick="switchTab('quick')" id="tab-quick" class="tab-btn py-3 px-4 text-sm font-medium border-b-2 border-teal-400 text-teal-400 flex items-center gap-2 transition">
<i class="fa-solid fa-bolt text-xs"></i> ⚡ Run Analysis
</button>
<button onclick="switchTab('explorer')" id="tab-explorer" class="tab-btn py-3 px-4 text-sm font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-2 transition">
<i class="fa-solid fa-sliders text-xs"></i> 🔍 Method Explorer
</button>
<button onclick="switchTab('heads')" id="tab-heads" class="tab-btn py-3 px-4 text-sm font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-2 transition">
<i class="fa-solid fa-brain text-xs"></i> 🧠 Per-Head Inspector
</button>
<button onclick="switchTab('forensics')" id="tab-forensics" class="tab-btn py-3 px-4 text-sm font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-2 transition">
<i class="fa-solid fa-user-secret text-xs"></i> 🕵️ Classic Forensics
</button>
<button onclick="switchTab('guide')" id="tab-guide" class="tab-btn py-3 px-4 text-sm font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-2 transition">
<i class="fa-solid fa-book-open text-xs"></i> 📖 Guide & Reference
</button>
</div>
</div>
<!-- Main Container -->
<main class="max-w-7xl mx-auto px-4 py-6 flex-1 w-full">
<!-- Global Image Upload Section -->
<div class="glass-panel rounded-2xl p-5 mb-6 shadow-xl">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-center">
<!-- Left: Upload Box -->
<div class="lg:col-span-5">
<div id="drop-zone" onclick="document.getElementById('file-input').click()" class="border-2 border-dashed border-slate-700 hover:border-teal-500/60 bg-slate-900/60 hover:bg-slate-900 transition rounded-xl p-6 text-center cursor-pointer relative group">
<input type="file" id="file-input" accept="image/*" class="hidden" onchange="handleFileSelect(event)" />
<div id="upload-prompt" class="py-2">
<i class="fa-solid fa-cloud-arrow-up text-3xl text-teal-400/80 group-hover:scale-110 transition mb-2"></i>
<p class="text-sm font-medium text-slate-200">Drag & Drop Image, Paste, or <span class="text-teal-400 underline">Browse</span></p>
<p class="text-xs text-slate-500 mt-1">Supports JPG, PNG, WEBP (Auto-cropped to 384×384)</p>
</div>
<div id="preview-container" class="hidden relative inline-block">
<img id="image-preview" src="" alt="Input Preview" class="max-h-48 rounded-lg shadow-lg border border-slate-700 object-contain mx-auto" />
<button onclick="event.stopPropagation(); clearImage()" class="absolute -top-2 -right-2 bg-rose-500 text-white w-6 h-6 rounded-full text-xs flex items-center justify-center hover:bg-rose-600 shadow z-10">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</div>
<!-- Right: Control Panel & Status -->
<div class="lg:col-span-7 flex flex-col justify-between h-full space-y-4">
<div class="flex items-center justify-between border-b border-slate-800 pb-3">
<div>
<span class="text-xs font-mono text-slate-400 uppercase tracking-wider">Analysis Status</span>
<div id="prediction-badge" class="mt-1 text-sm font-bold text-slate-500 flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-slate-600"></span> Awaiting Input Image
</div>
</div>
<div id="confidence-box" class="hidden text-right">
<span class="text-xs text-slate-400">Confidence Score</span>
<p id="confidence-val" class="font-mono text-lg font-bold text-teal-400">0.00%</p>
</div>
</div>
<!-- Buttons -->
<div class="flex flex-wrap items-center gap-3">
<button id="btn-analyze" onclick="runAnalysis()" disabled class="flex-1 bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 font-bold px-6 py-3 rounded-xl shadow-lg shadow-teal-500/20 transition flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa-solid fa-bolt"></i> Run Full XAI Suite
</button>
<button id="btn-report" onclick="generateReport()" disabled class="bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold px-4 py-3 rounded-xl border border-slate-700 transition flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa-solid fa-file-arrow-down"></i> Report PNG
</button>
</div>
</div>
</div>
</div>
<!-- Loading Bar / Spinner -->
<div id="loading-spinner" class="hidden glass-panel rounded-2xl p-6 mb-6 text-center border-t border-teal-500">
<div class="inline-block animate-spin w-8 h-8 border-4 border-teal-500 border-t-transparent rounded-full mb-3"></div>
<p id="loading-text" class="text-sm font-medium text-slate-300">Computing ViT Attribution Maps in parallel...</p>
</div>
<!-- TAB 1: ⚡ Run Analysis -->
<div id="section-quick" class="tab-section">
<div class="glass-panel rounded-2xl p-6 shadow-xl mb-6">
<h2 class="text-base font-bold text-slate-100 mb-4 flex items-center justify-between">
<span><i class="fa-solid fa-layer-group text-teal-400 mr-2"></i> ViT Attribution Overlays</span>
<span class="text-xs font-normal text-slate-400">Click any map to inspect in fullscreen modal</span>
</h2>
<!-- Grid of 6 attribution overlays -->
<div id="results-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Placeholder cards (initial state) -->
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">Attention Rollout</p>
</div>
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">Chefer Gradient-Rollout</p>
</div>
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">GradCAM</p>
</div>
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">Integrated Gradients</p>
</div>
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">DeepLIFT</p>
</div>
<div class="bg-slate-900/40 border border-slate-800/80 rounded-xl p-4 text-center py-12 text-slate-600">
<i class="fa-regular fa-image text-3xl mb-2"></i>
<p class="text-xs">Occlusion Sensitivity</p>
</div>
</div>
</div>
<!-- Dataset Feedback Submission Panel -->
<div id="submit-panel" class="hidden glass-panel rounded-2xl p-6 shadow-xl border border-teal-500/20">
<h3 class="text-sm font-bold text-slate-200 mb-2 flex items-center gap-2">
<i class="fa-solid fa-database text-teal-400"></i> Contribute Ground Truth to Research Dataset
</h3>
<p class="text-xs text-slate-400 mb-4">
Help improve future forensic models by verifying prediction correctness. Your feedback and the composite XAI heatmaps will be uploaded to <code class="text-teal-400 bg-slate-900 px-1.5 py-0.5 rounded">buildborderless/deepfake-explainability</code>.
</p>
<div class="flex flex-wrap items-center gap-6">
<div>
<span class="text-xs text-slate-400 block mb-1">Ground Truth Label:</span>
<div class="flex items-center space-x-3">
<label class="inline-flex items-center text-xs text-slate-300 cursor-pointer">
<input type="radio" name="ground_truth" value="real" class="form-radio text-teal-500 bg-slate-900 border-slate-700" />
<span class="ml-1.5">Real Image</span>
</label>
<label class="inline-flex items-center text-xs text-slate-300 cursor-pointer">
<input type="radio" name="ground_truth" value="fake" class="form-radio text-rose-500 bg-slate-900 border-slate-700" />
<span class="ml-1.5">Deepfake</span>
</label>
<label class="inline-flex items-center text-xs text-slate-300 cursor-pointer">
<input type="radio" name="ground_truth" value="unknown" checked class="form-radio text-slate-500 bg-slate-900 border-slate-700" />
<span class="ml-1.5">Unsure / Unknown</span>
</label>
</div>
</div>
<label class="inline-flex items-center text-xs text-slate-300 cursor-pointer">
<input type="checkbox" id="opt-in-image" checked class="form-checkbox text-teal-500 bg-slate-900 border-slate-700 rounded" />
<span class="ml-2">Include raw image in dataset</span>
</label>
<button onclick="submitDatasetRun()" class="bg-teal-600 hover:bg-teal-500 text-slate-950 font-bold text-xs px-4 py-2 rounded-lg transition ml-auto flex items-center gap-1.5">
<i class="fa-solid fa-paper-plane"></i> Submit Entry
</button>
</div>
<p id="submit-status" class="text-xs font-mono text-teal-400 mt-2 hidden"></p>
</div>
</div>
<!-- TAB 2: 🔍 Method Explorer -->
<div id="section-explorer" class="tab-section hidden">
<div class="glass-panel rounded-2xl p-6 shadow-xl mb-6">
<h2 class="text-base font-bold text-slate-100 mb-4 flex items-center gap-2">
<i class="fa-solid fa-sliders text-teal-400"></i> Interactive Method Parameter Explorer
</h2>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Parameter Controls -->
<div class="lg:col-span-4 space-y-4 bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<div>
<label class="text-xs font-medium text-slate-300 block mb-1">Select XAI Method</label>
<select id="exp-method" onchange="debouncedRunExplorer()" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-teal-500">
<option value="rollout">Attention Rollout</option>
<option value="chefer">Gradient-Weighted Rollout (Chefer)</option>
<option value="gradcam">GradCAM</option>
<option value="integrated">Integrated Gradients</option>
<option value="deeplift">DeepLIFT</option>
<option value="occlusion">Occlusion Sensitivity</option>
</select>
</div>
<div>
<label class="text-xs font-medium text-slate-300 block mb-1">Colormap</label>
<select id="exp-cmap" onchange="debouncedRunExplorer()" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
<option value="jet">Jet (Standard Forensic)</option>
<option value="inferno">Inferno (High Contrast)</option>
<option value="viridis">Viridis (Perceptual)</option>
<option value="hot">Hot (Thermal)</option>
</select>
</div>
<div>
<label class="text-xs font-medium text-slate-300 block mb-1">Overlay Transparency (Alpha: <span id="val-alpha">0.6</span>)</label>
<input type="range" id="exp-alpha" min="0.1" max="1.0" step="0.05" value="0.6" oninput="document.getElementById('val-alpha').innerText=this.value; debouncedRunExplorer()" class="w-full text-teal-500" />
</div>
<!-- Conditional Controls -->
<div id="ctrl-integrated" class="hidden space-y-3 pt-2 border-t border-slate-800">
<label class="text-xs font-medium text-slate-300 block mb-1">IG Steps: <span id="val-steps">20</span></label>
<input type="range" id="exp-steps" min="10" max="60" step="5" value="20" oninput="document.getElementById('val-steps').innerText=this.value; debouncedRunExplorer()" class="w-full" />
</div>
<div id="ctrl-occlusion" class="hidden space-y-3 pt-2 border-t border-slate-800">
<label class="text-xs font-medium text-slate-300 block mb-1">Patch Size: <span id="val-patch">32</span> px</label>
<input type="range" id="exp-patch" min="16" max="64" step="8" value="32" oninput="document.getElementById('val-patch').innerText=this.value; debouncedRunExplorer()" class="w-full" />
</div>
<div id="ctrl-gradcam" class="hidden space-y-3 pt-2 border-t border-slate-800">
<label class="text-xs font-medium text-slate-300 block mb-1">Target Transformer Layer: <span id="val-layer">12</span></label>
<input type="range" id="exp-layer" min="1" max="12" step="1" value="12" oninput="document.getElementById('val-layer').innerText=this.value; debouncedRunExplorer()" class="w-full" />
</div>
</div>
<!-- Display Box -->
<div class="lg:col-span-8 grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800 text-center">
<span class="text-xs text-slate-400 block mb-2 font-mono">Saliency Overlay</span>
<img id="exp-img-overlay" src="" class="max-h-72 rounded-lg mx-auto shadow border border-slate-700 object-contain" />
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800 text-center">
<span class="text-xs text-slate-400 block mb-2 font-mono">Raw Heatmap</span>
<img id="exp-img-raw" src="" class="max-h-72 rounded-lg mx-auto shadow border border-slate-700 object-contain" />
</div>
</div>
</div>
</div>
</div>
<!-- TAB 3: 🧠 Per-Head Inspector -->
<div id="section-heads" class="tab-section hidden">
<div class="glass-panel rounded-2xl p-6 shadow-xl mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-bold text-slate-100 flex items-center gap-2">
<i class="fa-solid fa-brain text-teal-400"></i> Attention Head Disaggregation (6 Heads per Layer)
</h2>
<div class="flex items-center space-x-3">
<span class="text-xs text-slate-400">Layer: <strong id="head-layer-val" class="text-teal-400">12</strong> / 12</span>
<input type="range" id="head-layer-input" min="1" max="12" value="12" oninput="document.getElementById('head-layer-val').innerText=this.value; debouncedRunHeads()" class="w-32" />
</div>
</div>
<div id="heads-grid" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
<!-- Placeholder cards -->
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 1</div>
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 2</div>
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 3</div>
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 4</div>
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 5</div>
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-3 text-center py-8 text-slate-600 text-xs">Head 6</div>
</div>
</div>
</div>
<!-- TAB 4: 🕵️ Classic Forensics -->
<div id="section-forensics" class="tab-section hidden">
<div class="glass-panel rounded-2xl p-6 shadow-xl mb-6">
<h2 class="text-base font-bold text-slate-100 mb-4 flex items-center gap-2">
<i class="fa-solid fa-user-secret text-teal-400"></i> Classic Digital Image Forensics Suite
</h2>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Controls -->
<div class="lg:col-span-4 space-y-4 bg-slate-900/60 p-4 rounded-xl border border-slate-800 flex flex-col">
<div>
<label class="text-xs font-medium text-slate-300 block mb-1">Forensic Analysis Tool</label>
<select id="for-tool" onchange="debouncedRunForensics(true)" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
<option value="ela">Error Level Analysis (ELA)</option>
<option value="gradient">Spatial Edge Gradient</option>
<option value="bitplane">Bit Plane Extractor</option>
<option value="minmax">MinMax Local Deviation</option>
<option value="wavelet">Wavelet Noise Estimation</option>
</select>
</div>
<!-- ELA Options -->
<div id="ctrl-ela" class="space-y-3 pt-2 border-t border-slate-800">
<label class="text-xs font-medium text-slate-300 block mb-1">JPEG Quality: <span id="val-quality">75</span>%</label>
<input type="range" id="for-quality" min="30" max="95" value="75" oninput="document.getElementById('val-quality').innerText=this.value; debouncedRunForensics(true)" class="w-full" />
</div>
<!-- Bitplane Options -->
<div id="ctrl-bitplane" class="hidden space-y-3 pt-2 border-t border-slate-800">
<label class="text-xs font-medium text-slate-300 block mb-1">Bit Index (0=LSB, 7=MSB)</label>
<input type="range" id="for-bit" min="0" max="7" value="0" oninput="debouncedRunForensics(true)" class="w-full" />
</div>
<div class="mt-auto pt-4 border-t border-slate-800/50">
<h4 id="for-desc-title" class="font-bold text-sm text-teal-400 mb-1">Error Level Analysis (ELA)</h4>
<p id="for-desc-text" class="text-xs text-slate-400 mb-2 leading-relaxed">Re-saves the image at lower JPEG quality and computes the pixel-level difference. Regions that were edited or composited appear brighter because they were saved at a different compression level than the surrounding content.</p>
<p class="text-xs text-slate-300"><span class="font-semibold text-slate-200">Look for:</span> <span id="for-desc-look">Uniform bright glow across the face region = likely composite. Inconsistent brightness between face and background = editing boundary.</span></p>
</div>
<p id="for-info" class="text-[10px] font-mono text-teal-400/90 pt-2"></p>
</div>
<!-- Display Box -->
<div class="lg:col-span-8 bg-slate-900/60 p-4 rounded-xl border border-slate-800 text-center flex flex-col justify-center items-center min-h-[400px]">
<img id="for-img-output" src="" class="max-h-[450px] rounded-lg shadow border border-slate-700 object-contain hidden" />
<p id="for-placeholder" class="text-xs text-slate-500">Upload an image and run analysis</p>
</div>
</div>
</div>
</div>
<!-- TAB 5: 📖 Guide & Reference -->
<div id="section-guide" class="tab-section hidden">
<div class="glass-panel rounded-2xl p-6 shadow-xl space-y-6">
<div>
<h2 class="text-lg font-bold text-slate-100 mb-2 border-b border-slate-800 pb-2">1. How to Read XAI Heatmaps</h2>
<p class="text-sm text-slate-300 leading-relaxed mb-2">
Explainable AI (XAI) methods attribute the final prediction score back to the original image pixels.
<strong>Warm colors (red/yellow)</strong> indicate high attribution, meaning those pixels strongly influenced the model's prediction.
<strong>Cool colors (blue/purple)</strong> indicate low attribution or minimal influence.
Each method approaches this problem differently, effectively answering a different question about the model's internal behavior.
</p>
</div>
<div>
<h2 class="text-lg font-bold text-slate-100 mb-4 border-b border-slate-800 pb-2">2. XAI Methods</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-xs text-slate-300">
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Attention Rollout</h3>
<p class="text-slate-400">Averages attention weights across all heads and propagates them through all 12 layers. It highlights where the model focused its spatial attention globally, regardless of the final class prediction.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Chefer Gradient-Rollout</h3>
<p class="text-slate-400">Multiplies attention weights by their backward gradients. This makes the map class-discriminative, showing exactly which regions pushed the probability toward the final verdict (REAL or FAKE).</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">GradCAM</h3>
<p class="text-slate-400">Uses the gradients of the target concept flowing into the final attention layer. It provides the cleanest, most localized answer to "where did the model decide?", often highlighting the face center.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Integrated Gradients</h3>
<p class="text-slate-400">Computes the integral of gradients along a straight path from a black baseline image to the input. It meticulously captures pixel-level contributions, highlighting fine details that influenced the score.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">DeepLIFT</h3>
<p class="text-slate-400">Decomposes the differences in non-linear activations relative to a baseline reference. It is excellent at finding isolated "hotspots" or blending artifacts along face boundaries.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Occlusion Sensitivity</h3>
<p class="text-slate-400">The most faithful, model-agnostic method. It slides a black patch over the image and measures the drop in prediction score. Bright regions directly mean: "if you cover this, the model changes its mind."</p>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-bold text-slate-100 mb-4 border-b border-slate-800 pb-2">3. Classic Forensics Tools</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs text-slate-300">
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Error Level Analysis (ELA)</h3>
<p class="text-slate-400">Reveals differing compression levels in an image. Spliced or manipulated areas often stand out as noticeably brighter or darker than the surrounding native pixels when re-compressed.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Spatial Edge Gradient</h3>
<p class="text-slate-400">Calculates high-frequency transitions to expose unnatural edge profiles. Face swaps and AI generations frequently leave subtle halos, double edges, or unnatural sharpness at blending seams.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">Bit Plane Extractor</h3>
<p class="text-slate-400">Strips away significant visual data to analyze the least significant bits (LSB). AI synthesis often leaves structured, blocky, or geometrically regular noise patterns in the lowest bit planes.</p>
</div>
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-800">
<h3 class="font-bold text-teal-400 mb-1">MinMax & Wavelet Estimators</h3>
<p class="text-slate-400">Statistical and frequency domain techniques to find regions where pixel noise variance or intensity drastically deviates from the local neighborhood, typical of pasted imagery.</p>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-bold text-slate-100 mb-2 border-b border-slate-800 pb-2">4. Model Architecture</h2>
<p class="text-sm text-slate-300 leading-relaxed">
Model: <code class="text-teal-400 font-mono bg-slate-900 px-1 rounded">buildborderless/CommunityForensics-DeepfakeDet-ViT</code><br/>
Architecture: Vision Transformer (ViT-Small). Inputs are resized to 384×384. It contains 12 transformer blocks (layers) and 6 attention heads per layer. The final layer passes through a binary sigmoid classifier. The model was trained on 2.7M samples across 4,803 distinct generative models and manipulation techniques.
</p>
</div>
</div>
</div>
</main>
<!-- Fullscreen Modal Viewer -->
<div id="modal-viewer" class="hidden fixed inset-0 z-50 bg-slate-950/90 backdrop-blur-md flex items-center justify-center p-4" onclick="closeModal()">
<div class="relative max-w-4xl w-full bg-slate-900 border border-slate-700 rounded-2xl overflow-hidden p-4 shadow-2xl" onclick="event.stopPropagation()">
<div class="flex items-center justify-between mb-3 border-b border-slate-800 pb-2">
<h3 id="modal-title" class="text-sm font-bold text-slate-200">Attribution Overlay</h3>
<button onclick="closeModal()" class="text-slate-400 hover:text-slate-100 text-lg transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<img id="modal-img" src="" class="max-h-[75vh] mx-auto rounded-lg object-contain bg-slate-900/50" />
</div>
</div>
<!-- JavaScript App Logic -->
<script>
// State Variables
let currentImageB64 = null;
let currentImageHash = null;
let analysisComplete = false;
let currentAnalysisData = null;
let cachedForensics = null;
let collectedResults = {};
const METHOD_META = {
rollout: { name: 'Attention Rollout', insight: 'Bright areas = where the model concentrated attention across all 12 layers. Unexpectedly bright regions on backgrounds or hair often indicate GAN artifacts the model caught.' },
chefer: { name: 'Chefer Gradient-Rollout', insight: 'Class-discriminative — shows specifically what pushed the score toward FAKE (warm) vs REAL (cool). Based on Chefer et al. (CVPR 2021).' },
gradcam: { name: 'GradCAM', insight: 'Red/yellow = strongest activations driving the final verdict. The cleanest spatial answer to "where did the model decide?"' },
integrated: { name: 'Integrated Gradients', insight: 'Pixel-level contributions relative to a black baseline. Warm = pushed toward FAKE; Cool = pushed toward REAL.' },
deeplift: { name: 'DeepLIFT', insight: 'Decomposes non-linear activation differences. Watch for isolated face-boundary hotspots that indicate blending artifacts.' },
occlusion: { name: 'Occlusion Sensitivity', insight: 'Most faithful — shows what actually changes the score when hidden. Bright = "if you cover this, the model changes its mind."' },
};
const FORENSIC_META = {
ela: {
name: 'Error Level Analysis (ELA)',
desc: 'Re-saves the image at lower JPEG quality and computes the pixel-level difference. Regions that were edited or composited appear brighter because they were saved at a different compression level than the surrounding content.',
lookFor: 'Uniform bright glow across the face region = likely composite. Inconsistent brightness between face and background = editing boundary.'
},
gradient: {
name: 'Spatial Edge Gradient',
desc: 'Computes directional edge gradients (Sobel/Scharr filters) to highlight high-frequency transitions. Natural images have smooth, consistent edge profiles; deepfakes often leave unnatural edge artifacts at blending boundaries.',
lookFor: 'Halos or sharp outlines around the face perimeter. Double edges where a face was pasted onto a different background.'
},
bitplane: {
name: 'Bit Plane Extractor',
desc: 'Extracts individual bit planes from the image data, from the Least Significant Bit (LSB, bit 0) to the Most Significant Bit (MSB, bit 7). Lower bit planes reveal hidden noise patterns and subtle data anomalies invisible to the naked eye.',
lookFor: 'Structured geometric patterns in the LSB (bit 0-1) that differ between the face and background regions. AI-generated images often show unusually regular or blocky LSB textures.'
},
minmax: {
name: 'MinMax Local Deviation',
desc: 'Measures the deviation between each pixel and its local neighborhood minimum/maximum values. This highlights regions where pixel intensity is statistically unusual compared to surrounding context.',
lookFor: 'Bright spots indicating patches that are "too smooth" or "too noisy" relative to neighbors — a telltale sign of AI-inserted content or localized editing.'
},
wavelet: {
name: 'Wavelet Noise Estimation',
desc: 'Applies wavelet decomposition to estimate the high-frequency noise variance across different regions of the image. Real camera photos have consistent sensor noise fingerprints; manipulated regions often have a different noise signature.',
lookFor: 'Significant noise variance differences between the face region and the rest of the image. Swapped faces typically carry noise from a different camera/source.'
},
};
// Debounce Utility
function debounce(fn, ms = 300) {
let timer;
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
}
const debouncedRunExplorer = debounce(runExplorerMethod, 300);
const debouncedRunHeads = debounce(runPerHeadInspector, 300);
const debouncedRunForensics = debounce(runForensicAnalysis, 300);
// Initialization & Event Listeners
document.addEventListener('DOMContentLoaded', () => {
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-teal-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-teal-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-teal-500');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (ev) => loadNewImage(ev.target.result);
reader.readAsDataURL(file);
}
});
document.addEventListener('paste', (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = (ev) => loadNewImage(ev.target.result);
reader.readAsDataURL(file);
break;
}
}
});
});
function loadNewImage(b64) {
currentImageB64 = b64;
document.getElementById('image-preview').src = currentImageB64;
document.getElementById('upload-prompt').classList.add('hidden');
document.getElementById('preview-container').classList.remove('hidden');
document.getElementById('btn-analyze').disabled = false;
analysisComplete = false;
}
function handleFileSelect(evt) {
const file = evt.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => loadNewImage(e.target.result);
reader.readAsDataURL(file);
}
function clearImage() {
currentImageB64 = null;
analysisComplete = false;
document.getElementById('file-input').value = '';
document.getElementById('upload-prompt').classList.remove('hidden');
document.getElementById('preview-container').classList.add('hidden');
document.getElementById('btn-analyze').disabled = true;
document.getElementById('btn-report').disabled = true;
document.getElementById('submit-panel').classList.add('hidden');
document.getElementById('confidence-box').classList.add('hidden');
const badge = document.getElementById('prediction-badge');
badge.className = 'mt-1 text-sm font-bold text-slate-500 flex items-center gap-2';
badge.innerHTML = '<span class="w-2.5 h-2.5 rounded-full bg-slate-600"></span> Awaiting Input Image';
}
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('border-teal-400', 'text-teal-400');
btn.classList.add('border-transparent', 'text-slate-400');
});
document.querySelectorAll('.tab-section').forEach(sec => sec.classList.add('hidden'));
document.getElementById('tab-' + tabId).classList.add('border-teal-400', 'text-teal-400');
document.getElementById('tab-' + tabId).classList.remove('border-transparent', 'text-slate-400');
document.getElementById('section-' + tabId).classList.remove('hidden');
if (analysisComplete) {
if (tabId === 'explorer') debouncedRunExplorer();
if (tabId === 'heads') debouncedRunHeads();
if (tabId === 'forensics') {
updateForensicDesc();
runForensicAnalysis(false);
}
}
}
async function apiCall(endpoint, payload) {
const resp = await fetch('/api/' + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return await resp.json();
}
async function runAnalysis() {
if (!currentImageB64) return;
document.getElementById('loading-spinner').classList.remove('hidden');
document.getElementById('btn-analyze').disabled = true;
document.getElementById('btn-report').disabled = true;
analysisComplete = false;
currentImageHash = null;
cachedForensics = null;
collectedResults = {};
currentAnalysisData = null;
// a) Clear results, show 6 skeleton cards
const grid = document.getElementById('results-grid');
grid.innerHTML = '';
const methods = ['rollout', 'chefer', 'gradcam', 'integrated', 'deeplift', 'occlusion'];
methods.forEach(m => {
const card = document.createElement('div');
card.id = `card-${m}`;
card.className = 'glass-panel rounded-xl p-3 flex flex-col h-full';
card.innerHTML = `
<div class="w-full h-44 rounded-lg skeleton-shimmer mb-3"></div>
<div class="h-4 w-3/4 rounded skeleton-shimmer mb-2"></div>
<div class="h-3 w-1/4 rounded skeleton-shimmer mb-4"></div>
<div class="h-8 w-full rounded skeleton-shimmer mt-auto"></div>
`;
grid.appendChild(card);
});
try {
// b) Call /api/predict
const predictRes = await apiCall('predict', { image_b64: currentImageB64 });
if (predictRes.status === 'success') {
const pred = predictRes.prediction;
currentImageHash = predictRes.image_hash;
currentAnalysisData = pred;
const isFake = pred.prediction === 'FAKE';
const badge = document.getElementById('prediction-badge');
badge.className = `mt-1 text-sm font-bold flex items-center gap-2 ${isFake ? 'text-rose-400' : 'text-emerald-400'}`;
badge.innerHTML = `<span class="w-2.5 h-2.5 rounded-full ${isFake ? 'bg-rose-500' : 'bg-emerald-500'}"></span> VERDICT: ${pred.prediction}`;
document.getElementById('confidence-val').innerText = pred.confidence_pct.toFixed(2) + '%';
document.getElementById('confidence-box').classList.remove('hidden');
}
// c & e) Fire parallel requests
const analyzePromise = apiCall('analyze', { image_b64: currentImageB64, alpha: 0.6, cmap: 'jet' }).then(res => {
if (res.status === 'success') {
cachedForensics = res.forensics;
if(!currentImageHash) currentImageHash = res.image_hash;
}
});
const methodPromises = methods.map(m => {
return apiCall('method_detail', {
image_b64: currentImageB64, method_name: m, steps: 20, patch_size: 32, target_layer_idx: 11, alpha: 0.6, cmap: 'jet'
}).then(res => {
if (res.status === 'success') {
// d) Replace skeleton card
collectedResults[m] = res.result;
renderResultCard(m, res.result);
}
});
});
await Promise.all([...methodPromises, analyzePromise]);
// f) When all complete
analysisComplete = true;
document.getElementById('submit-panel').classList.remove('hidden');
document.getElementById('btn-report').disabled = false;
} catch(err) {
console.error(err);
alert('API Request Failed: ' + err);
} finally {
document.getElementById('loading-spinner').classList.add('hidden');
document.getElementById('btn-analyze').disabled = false;
}
}
function toggleHeatmap(m, e) {
e.stopPropagation();
const state = collectedResults[m];
if(!state) return;
state.showingRaw = !state.showingRaw;
const imgEl = document.getElementById(`img-${m}`);
imgEl.src = state.showingRaw ? state.raw_heatmap_b64 : state.overlay_b64;
}
function renderResultCard(m, result) {
const meta = METHOD_META[m];
result.showingRaw = false;
const card = document.getElementById(`card-${m}`);
if(!card) return;
card.className = 'glass-panel rounded-xl p-3 hover:border-teal-500/50 transition cursor-pointer group flex flex-col h-full';
card.onclick = () => openModal(meta.name, result.showingRaw ? result.raw_heatmap_b64 : result.overlay_b64);
card.innerHTML = `
<div class="relative overflow-hidden rounded-lg mb-2 h-44 bg-slate-900/50 flex-shrink-0">
<img id="img-${m}" src="${result.overlay_b64}" class="w-full h-full object-contain group-hover:scale-105 transition duration-300" />
<span class="absolute bottom-2 right-2 bg-slate-950/80 text-teal-400 text-[10px] font-mono px-2 py-0.5 rounded border border-slate-700">${Math.round(result.compute_time_ms)} ms</span>
<button onclick="toggleHeatmap('${m}', event)" class="absolute top-2 right-2 bg-slate-900/80 text-slate-300 hover:text-white w-7 h-7 rounded-full text-xs flex items-center justify-center border border-slate-600 shadow transition z-10" title="Toggle Raw Heatmap">
<i class="fa-solid fa-arrows-rotate"></i>
</button>
</div>
<h4 class="text-xs font-bold text-slate-200 mb-1">${meta.name}</h4>
<p class="text-[10px] text-slate-400 line-clamp-3 leading-relaxed mt-auto">${meta.insight}</p>
`;
}
async function generateReport() {
if (!currentImageB64 || Object.keys(collectedResults).length < 6) return;
const btn = document.getElementById('btn-report');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Generating...';
try {
const reportDiv = document.createElement('div');
reportDiv.id = 'report-card';
reportDiv.style.position = 'absolute';
reportDiv.style.left = '-9999px';
reportDiv.style.top = '-9999px';
reportDiv.style.width = '800px';
reportDiv.style.backgroundColor = '#0f172a';
reportDiv.style.color = '#f1f5f9';
reportDiv.style.padding = '32px';
reportDiv.style.fontFamily = 'Inter, sans-serif';
let verdictColor = currentAnalysisData.prediction === 'FAKE' ? '#fb7185' : '#34d399';
let html = `
<div style="border-bottom: 1px solid #334155; padding-bottom: 16px; margin-bottom: 24px;">
<h1 style="font-size: 24px; font-weight: bold; margin: 0 0 8px 0; display: flex; align-items: center; gap: 8px;">
<span style="color: #2dd4bf;">●</span> ViT Deepfake Forensic Analysis Report
</h1>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: #94a3b8;">
<span>Model: buildborderless/CommunityForensics-DeepfakeDet-ViT</span>
<span>${new Date().toLocaleString()}</span>
</div>
</div>
<div style="background: rgba(30, 41, 59, 0.5); border: 1px solid #334155; border-radius: 12px; padding: 20px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center;">
<div>
<span style="font-size: 12px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">Verdict</span>
<div style="font-size: 20px; font-weight: bold; color: ${verdictColor}; margin-top: 4px;">
${currentAnalysisData.prediction}
</div>
</div>
<div style="text-align: right;">
<span style="font-size: 12px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">Confidence</span>
<div style="font-size: 20px; font-weight: bold; color: #2dd4bf; margin-top: 4px; font-family: monospace;">
${currentAnalysisData.confidence_pct.toFixed(2)}%
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;">
`;
const methods = ['rollout', 'chefer', 'gradcam', 'integrated', 'deeplift', 'occlusion'];
methods.forEach(m => {
const res = collectedResults[m];
const meta = METHOD_META[m];
html += `
<div style="background: #1e293b; border-radius: 8px; padding: 12px;">
<img src="${res.overlay_b64}" style="width: 100%; aspect-ratio: 1/1; object-fit: contain; background: #0f172a; border-radius: 6px; margin-bottom: 12px;" />
<div style="font-size: 14px; font-weight: 600; color: #f1f5f9;">${meta.name}</div>
<div style="font-size: 12px; color: #94a3b8; font-family: monospace;">${Math.round(res.compute_time_ms)} ms</div>
</div>
`;
});
html += `
</div>
<div style="border-top: 1px solid #334155; padding-top: 16px; font-size: 12px; color: #64748b; text-align: center;">
Generated by LPX55/DeepfakeDetection-Explainability
</div>
`;
reportDiv.innerHTML = html;
document.body.appendChild(reportDiv);
const canvas = await html2canvas(reportDiv, { scale: 2, backgroundColor: '#0f172a' });
const imgData = canvas.toDataURL('image/png');
document.body.removeChild(reportDiv);
const a = document.createElement('a');
a.href = imgData;
a.download = `forensic_report_${Date.now()}.png`;
a.click();
} catch (err) {
console.error(err);
alert('Report generation failed: ' + err);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-file-arrow-down"></i> Report PNG';
}
}
async function runExplorerMethod() {
if (!currentImageB64) return;
const method = document.getElementById('exp-method').value;
const cmap = document.getElementById('exp-cmap').value;
const alpha = parseFloat(document.getElementById('exp-alpha').value);
const steps = parseInt(document.getElementById('exp-steps').value);
const patch_size = parseInt(document.getElementById('exp-patch').value);
const target_layer_idx = parseInt(document.getElementById('exp-layer').value) - 1;
const ctrlIG = document.getElementById('ctrl-integrated');
if (ctrlIG) ctrlIG.classList.toggle('hidden', method !== 'integrated');
const ctrlOcc = document.getElementById('ctrl-occlusion');
if (ctrlOcc) ctrlOcc.classList.toggle('hidden', method !== 'occlusion');
const ctrlGC = document.getElementById('ctrl-gradcam');
if (ctrlGC) ctrlGC.classList.toggle('hidden', method !== 'gradcam');
const res = await apiCall('method_detail', {
image_b64: currentImageB64,
method_name: method,
steps: steps,
patch_size: patch_size,
target_layer_idx: target_layer_idx,
alpha: alpha,
cmap: cmap
});
if (res.status === 'success' && res.result) {
document.getElementById('exp-img-overlay').src = res.result.overlay_b64;
document.getElementById('exp-img-raw').src = res.result.raw_heatmap_b64;
}
}
async function runPerHeadInspector() {
if (!currentImageB64) return;
const displayLayer = parseInt(document.getElementById('head-layer-input').value);
const layer_idx = displayLayer - 1;
const res = await apiCall('heads', {
image_b64: currentImageB64,
layer_idx: layer_idx,
cmap: 'jet'
});
if (res.status === 'success' && res.heads) {
const grid = document.getElementById('heads-grid');
grid.innerHTML = '';
res.heads.forEach((head) => {
const card = document.createElement('div');
card.className = 'glass-panel rounded-xl p-2 text-center hover:border-teal-500/50 transition cursor-pointer';
card.onclick = () => openModal(`Layer ${displayLayer} — Head ${head.head}`, head.overlay_b64);
card.innerHTML = `
<img src="${head.overlay_b64}" class="w-full h-24 object-cover rounded-lg mb-1" />
<span class="text-[10px] font-mono text-slate-300">Head ${head.head}</span>
`;
grid.appendChild(card);
});
}
}
function updateForensicDesc() {
const tool = document.getElementById('for-tool').value;
const meta = FORENSIC_META[tool];
if(meta) {
document.getElementById('for-desc-title').innerText = meta.name;
document.getElementById('for-desc-text').innerText = meta.desc;
document.getElementById('for-desc-look').innerText = meta.lookFor;
}
}
async function runForensicAnalysis(force = true) {
if (!currentImageB64) return;
const tool = document.getElementById('for-tool').value;
const quality = parseInt(document.getElementById('for-quality').value);
const bit = parseInt(document.getElementById('for-bit').value);
updateForensicDesc();
const ctrlELA = document.getElementById('ctrl-ela');
if (ctrlELA) ctrlELA.classList.toggle('hidden', tool !== 'ela');
const ctrlBit = document.getElementById('ctrl-bitplane');
if (ctrlBit) ctrlBit.classList.toggle('hidden', tool !== 'bitplane');
if (!force && cachedForensics && cachedForensics[tool]) {
const cached = cachedForensics[tool];
displayForensicResult(cached.result_b64, cached.compute_time_ms, tool);
return;
}
const res = await apiCall('forensic', {
image_b64: currentImageB64,
tool_name: tool,
quality: quality,
bit: bit
});
if (res.status === 'success' && res.result_b64) {
displayForensicResult(res.result_b64, res.compute_time_ms, res.tool);
}
}
function displayForensicResult(b64, time, toolName) {
const img = document.getElementById('for-img-output');
img.src = b64;
img.classList.remove('hidden');
document.getElementById('for-placeholder').classList.add('hidden');
document.getElementById('for-info').innerText = `${toolName} analysis complete (${Math.round(time)} ms).`;
}
async function submitDatasetRun() {
if (!currentImageB64 || !currentImageHash) return;
const gtEl = document.querySelector('input[name="ground_truth"]:checked');
const gt = gtEl ? gtEl.value : 'unknown';
const optIn = document.getElementById('opt-in-image').checked;
const res = await apiCall('submit_run', {
image_b64: currentImageB64,
image_hash: currentImageHash,
ground_truth: gt,
opt_in_image: optIn
});
if (res.status === 'success') {
const statusP = document.getElementById('submit-status');
statusP.innerText = `Run ${res.run_id} logged to dataset buildborderless/deepfake-explainability!`;
statusP.classList.remove('hidden');
}
}
function openModal(title, b64) {
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-img').src = b64;
document.getElementById('modal-viewer').classList.remove('hidden');
}
function closeModal() {
document.getElementById('modal-viewer').classList.add('hidden');
}
</script>
</body>
</html>