Chethanand's picture
Upload 24 files
eb3afa1 verified
Raw
History Blame Contribute Delete
14.6 kB
/**
* AI Video Detector – Frontend Application
* Handles: drag-drop upload, video preview, API call, animated gauge, results
*/
"use strict";
// ── DOM References ────────────────────────────────────────────────────────────
const uploadZone = document.getElementById("upload-zone");
const fileInput = document.getElementById("file-input");
const previewArea = document.getElementById("preview-area");
const previewVideo = document.getElementById("preview-video");
const previewName = document.getElementById("preview-name");
const previewMeta = document.getElementById("preview-meta");
const btnRemove = document.getElementById("btn-remove");
const btnAnalyze = document.getElementById("btn-analyze");
const progressWrap = document.getElementById("progress-wrap");
const progressBar = document.getElementById("progress-bar");
const progressLbl = document.getElementById("progress-label");
const progressPct = document.getElementById("progress-pct");
const resultsPanel = document.getElementById("results-panel");
const resultCard = document.getElementById("result-card");
const verdictBadge = document.getElementById("verdict-badge");
const verdictIcon = document.getElementById("verdict-icon");
const verdictText = document.getElementById("verdict-text");
const gaugeFill = document.getElementById("gauge-fill");
const gaugePctTxt = document.getElementById("gauge-pct-text");
const mProb = document.getElementById("m-prob");
const mConf = document.getElementById("m-conf");
const mTime = document.getElementById("m-time");
const mThresh = document.getElementById("m-thresh");
const resultDesc = document.getElementById("result-desc");
const btnReset = document.getElementById("btn-reset");
const statusDot = document.getElementById("status-dot");
const statusText = document.getElementById("status-text");
const toastCont = document.getElementById("toast-container");
// Progress steps
const psUpload = document.getElementById("ps-upload");
const psExtract = document.getElementById("ps-extract");
const psCnn = document.getElementById("ps-cnn");
const psLstm = document.getElementById("ps-lstm");
const psVerdict = document.getElementById("ps-verdict");
const pSteps = [psUpload, psExtract, psCnn, psLstm, psVerdict];
// ── State ─────────────────────────────────────────────────────────────────────
let selectedFile = null;
let analysisTimer = null;
// ── Gauge constants ───────────────────────────────────────────────────────────
const GAUGE_CIRCUMFERENCE = 330; // stroke-dasharray value in SVG
// ── Server Status Check ───────────────────────────────────────────────────────
async function checkServerStatus() {
try {
const res = await fetch("/api/status", { signal: AbortSignal.timeout(4000) });
if (res.ok) {
const data = await res.json();
statusDot.classList.remove("offline");
statusText.textContent = `Model ready Β· ${data.device?.toUpperCase() ?? "CPU"}`;
statusDot.setAttribute("title", "Server online");
return true;
}
} catch (_) {}
statusDot.classList.add("offline");
statusText.textContent = "Server offline";
return false;
}
// ── Toast Notifications ───────────────────────────────────────────────────────
function toast(message, type = "info", duration = 4500) {
const icons = { error: "❌", success: "βœ…", info: "ℹ️" };
const el = document.createElement("div");
el.className = `toast ${type}`;
el.setAttribute("role", "alert");
el.innerHTML = `<span class="toast-icon" aria-hidden="true">${icons[type]}</span><span>${message}</span>`;
toastCont.appendChild(el);
setTimeout(() => {
el.classList.add("toast-exit");
el.addEventListener("animationend", () => el.remove());
}, duration);
}
// ── File Handling ─────────────────────────────────────────────────────────────
const ALLOWED_TYPES = ["video/mp4", "video/avi", "video/x-msvideo", "video/x-matroska", "video/quicktime", "video/webm", "video/x-ms-wmv"];
const MAX_SIZE_MB = 500;
function isVideoFile(file) {
if (ALLOWED_TYPES.includes(file.type)) return true;
const ext = file.name.split(".").pop().toLowerCase();
return ["mp4","avi","mkv","mov","webm","wmv"].includes(ext);
}
function formatBytes(bytes) {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function setFile(file) {
if (!isVideoFile(file)) {
toast("Unsupported file type. Please upload a video (MP4, AVI, MKV, MOV, WEBM).", "error");
return;
}
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
toast(`File too large. Maximum allowed size is ${MAX_SIZE_MB} MB.`, "error");
return;
}
selectedFile = file;
// Preview
const url = URL.createObjectURL(file);
previewVideo.src = url;
previewName.textContent = file.name;
previewMeta.textContent = formatBytes(file.size);
previewArea.classList.add("visible");
btnAnalyze.disabled = false;
hideResults();
toast(`Video selected: ${file.name}`, "success", 3000);
}
function clearFile() {
selectedFile = null;
fileInput.value = "";
previewVideo.src = "";
previewArea.classList.remove("visible");
btnAnalyze.disabled = true;
hideResults();
}
// ── Upload Zone Events ────────────────────────────────────────────────────────
uploadZone.addEventListener("click", () => fileInput.click());
uploadZone.addEventListener("keydown", e => { if (e.key === "Enter" || e.key === " ") fileInput.click(); });
fileInput.addEventListener("change", () => {
if (fileInput.files[0]) setFile(fileInput.files[0]);
});
uploadZone.addEventListener("dragenter", e => { e.preventDefault(); uploadZone.classList.add("drag-over"); });
uploadZone.addEventListener("dragover", e => { e.preventDefault(); uploadZone.classList.add("drag-over"); });
uploadZone.addEventListener("dragleave", e => {
if (!uploadZone.contains(e.relatedTarget)) uploadZone.classList.remove("drag-over");
});
uploadZone.addEventListener("drop", e => {
e.preventDefault();
uploadZone.classList.remove("drag-over");
const files = e.dataTransfer?.files;
if (files && files[0]) setFile(files[0]);
});
btnRemove.addEventListener("click", clearFile);
// ── Progress Simulation ───────────────────────────────────────────────────────
function setProgress(pct, label, activeStep) {
progressBar.style.width = `${pct}%`;
progressBar.parentElement.setAttribute("aria-valuenow", pct);
progressLbl.textContent = label;
progressPct.textContent = `${Math.round(pct)}%`;
pSteps.forEach(s => {
s.classList.remove("active", "done");
const idx = pSteps.indexOf(s);
const activeIdx = pSteps.indexOf(activeStep);
if (idx < activeIdx) s.classList.add("done");
else if (idx === activeIdx) s.classList.add("active");
});
}
function startProgressSimulation() {
clearTimeout(analysisTimer);
setProgress(5, "Uploading video…", psUpload);
progressWrap.classList.add("visible");
const steps = [
{ delay: 800, pct: 20, label: "Extracting frames from video…", step: psExtract },
{ delay: 2200, pct: 45, label: "Running CNN feature extraction…", step: psCnn },
{ delay: 4000, pct: 70, label: "Processing LSTM temporal sequence…", step: psLstm },
{ delay: 5500, pct: 90, label: "Computing classification verdict…", step: psVerdict },
];
steps.forEach(({ delay, pct, label, step }) => {
analysisTimer = setTimeout(() => setProgress(pct, label, step), delay);
});
}
function finishProgress() {
clearTimeout(analysisTimer);
setProgress(100, "Analysis complete!", psVerdict);
psVerdict.classList.remove("active");
psVerdict.classList.add("done");
}
// ── Gauge Animation ───────────────────────────────────────────────────────────
/**
* The gauge covers ~280Β° of 360Β°. stroke-dasharray=330 corresponds to the arc.
* A confidence of 0% β†’ dashoffset=330 (empty), 100% β†’ dashoffset=0 (full).
*/
function animateGauge(confidencePct, isAI) {
const fillClass = isAI ? "ai-fill" : "real-fill";
gaugeFill.setAttribute("class", `gauge-fill ${fillClass}`);
const offset = GAUGE_CIRCUMFERENCE - (confidencePct / 100) * GAUGE_CIRCUMFERENCE;
// Start at empty
gaugeFill.style.strokeDashoffset = GAUGE_CIRCUMFERENCE;
gaugePctTxt.textContent = "--";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
gaugeFill.style.strokeDashoffset = offset;
});
});
// Animate number counter
let start = 0;
const end = confidencePct;
const duration = 1200;
const startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
start = Math.round(ease * end);
gaugePctTxt.textContent = `${start}%`;
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// ── Results Rendering ─────────────────────────────────────────────────────────
function showResults(data) {
const { verdict, is_ai, probability, confidence, processing_time, threshold } = data;
// Card theme
resultCard.classList.remove("ai-result", "real-result");
resultCard.classList.add(is_ai ? "ai-result" : "real-result");
// Verdict badge
verdictBadge.classList.remove("ai-badge", "real-badge");
verdictBadge.classList.add(is_ai ? "ai-badge" : "real-badge");
verdictIcon.textContent = is_ai ? "πŸ€–" : "βœ…";
verdictText.textContent = verdict;
verdictBadge.setAttribute("aria-label", `Verdict: ${verdict}`);
// Gauge
animateGauge(Math.round(confidence), is_ai);
// Metrics
mProb.textContent = probability.toFixed(4);
mProb.className = "metric-value";
mProb.classList.add(is_ai ? "red" : "green");
mConf.textContent = `${confidence.toFixed(1)}%`;
mConf.className = "metric-value";
mConf.classList.add(is_ai ? "red" : "green");
mTime.textContent = `${processing_time}s`;
mThresh.textContent = is_ai ? "No βœ—" : "Yes βœ“";
mThresh.className = `metric-value ${is_ai ? "red" : "green"}`;
// Description
resultDesc.className = "result-desc";
resultDesc.classList.add(is_ai ? "ai" : "real");
resultDesc.textContent = is_ai
? `This video shows strong indicators of AI generation. The model found temporal artifacts and synthetic patterns across the frame sequence β€” characteristic of AI-created content such as deepfakes or generative video models. Confidence: ${confidence.toFixed(1)}%.`
: `This video exhibits natural, organic characteristics consistent with real-world footage. The temporal patterns and spatial features analysed across frames match those of authentic video capture. Confidence: ${confidence.toFixed(1)}%.`;
resultsPanel.classList.add("visible");
}
function hideResults() {
resultsPanel.classList.remove("visible");
progressWrap.classList.remove("visible");
pSteps.forEach(s => s.classList.remove("active", "done"));
}
// ── Analyze ───────────────────────────────────────────────────────────────────
btnAnalyze.addEventListener("click", async () => {
if (!selectedFile) return;
const online = await checkServerStatus();
if (!online) {
toast("Cannot connect to the server. Make sure app.py is running.", "error", 6000);
return;
}
// UI: loading state
btnAnalyze.disabled = true;
btnAnalyze.classList.add("loading");
btnAnalyze.setAttribute("aria-busy", "true");
hideResults();
startProgressSimulation();
const formData = new FormData();
formData.append("video", selectedFile, selectedFile.name);
try {
const res = await fetch("/api/predict", {
method: "POST",
body: formData,
});
finishProgress();
if (!res.ok) {
let errMsg = `Server error ${res.status}`;
try {
const errData = await res.json();
errMsg = errData.error || errMsg;
} catch (_) {}
toast(`Analysis failed: ${errMsg}`, "error", 7000);
return;
}
const data = await res.json();
// Brief pause so progress animation finishes
await new Promise(r => setTimeout(r, 500));
showResults(data);
toast(
`Analysis complete: ${data.verdict} (${data.confidence.toFixed(1)}% confidence)`,
data.is_ai ? "info" : "success",
5000
);
} catch (err) {
toast(`Network error: ${err.message}`, "error", 7000);
console.error(err);
} finally {
btnAnalyze.disabled = false;
btnAnalyze.classList.remove("loading");
btnAnalyze.setAttribute("aria-busy", "false");
}
});
// ── Reset ─────────────────────────────────────────────────────────────────────
btnReset.addEventListener("click", () => {
clearFile();
hideResults();
window.scrollTo({ top: 0, behavior: "smooth" });
toast("Ready for a new analysis.", "info", 2500);
});
// ── Init ──────────────────────────────────────────────────────────────────────
(async function init() {
await checkServerStatus();
// Recheck every 30 seconds
setInterval(checkServerStatus, 30_000);
})();