dk2430098's picture
Upload folder using huggingface_hub
e03466a verified
/**
* app.js β€” ImageForensics-Detect Frontend
* Handles file upload, API calls, result rendering, animations.
*/
// ──// Use current hostname to avoid CORS/Localhost resolution issues
// For deployment (Vercel/HuggingFace), empty string "" uses current host/port
const API_HOST = window.location.hostname;
const API_BASE = (API_HOST === "" || API_HOST === "localhost" || API_HOST === "127.0.0.1")
? "http://localhost:8000"
: "";
console.log("[Forensics] Host:", API_HOST || "local-file");
console.log("[Forensics] API Base:", API_BASE || "relative-root");
// ── State ────────────────────────────────────────────────────────
let selectedFile = null;
let stepTimer = null;
const STEPS = ["spectral", "edge", "cnn", "vit", "diffusion", "fusion"];
// ── DOM refs ─────────────────────────────────────────────────────
const uploadZone = document.getElementById("uploadZone");
const fileInput = document.getElementById("fileInput");
const selectBtn = document.getElementById("selectBtn");
const previewArea = document.getElementById("previewArea");
const previewImg = document.getElementById("previewImg");
const fileNameEl = document.getElementById("fileName");
const fileSizeEl = document.getElementById("fileSize");
const analyzeBtn = document.getElementById("analyzeBtn");
const clearBtn = document.getElementById("clearBtn");
const analyzingState = document.getElementById("analyzingState");
const resultsSection = document.getElementById("resultsSection");
const errorBanner = document.getElementById("errorBanner");
const errorMsg = document.getElementById("errorMsg");
const serverStatus = document.getElementById("serverStatus");
// ── UI Helpers ───────────────────────────────────────────────────
function showError(msg) {
errorMsg.textContent = msg;
errorBanner.style.display = "flex";
}
function hideError() {
errorBanner.style.display = "none";
}
// Fixed for hoisting: define resetUI as a window property but also a function
window.resetUI = function resetUI() {
selectedFile = null;
if (fileInput) fileInput.value = "";
if (previewArea) previewArea.style.display = "none";
if (resultsSection) resultsSection.style.display = "none";
if (analyzingState) analyzingState.style.display = "none";
if (errorBanner) errorBanner.style.display = "none";
if (uploadZone) uploadZone.scrollIntoView({ behavior: "smooth" });
};
// ── Server health check ──────────────────────────────────────────
async function checkServerHealth() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const r = await fetch(`${API_BASE}/health`, { signal: controller.signal });
clearTimeout(timeoutId);
if (r.ok) {
serverStatus.textContent = "● API Online";
serverStatus.classList.remove("offline");
} else {
throw new Error("not ok");
}
} catch (err) {
if (err.name === 'AbortError') {
// Silent timeout
} else {
console.warn("[Forensics] Health check failed:", err.message);
}
serverStatus.textContent = "● API Offline";
serverStatus.classList.add("offline");
}
}
// ── Analysis Steps Animation ──────────────────────────────────────
function animateSteps() {
let i = 0;
console.log("[Forensics] Starting analysis animations...");
STEPS.forEach(s => {
const el = document.getElementById(`step-${s}`);
if (el) {
el.className = "step-item";
const iconMatch = el.textContent.match(/^[βœ”β—‹γ€‡]\s*/);
if (iconMatch) {
el.textContent = "β—‹ " + el.textContent.substring(iconMatch[0].length);
}
}
});
stepTimer = setInterval(() => {
if (i > 0) {
const prev = document.getElementById(`step-${STEPS[i - 1]}`);
if (prev) {
prev.classList.remove("active");
prev.classList.add("done");
prev.textContent = "βœ” " + prev.textContent.slice(2);
}
}
if (i < STEPS.length) {
const cur = document.getElementById(`step-${STEPS[i]}`);
if (cur) cur.classList.add("active");
i++;
} else {
clearInterval(stepTimer);
}
}, 600);
}
function stopStepAnimation() {
if (stepTimer) clearInterval(stepTimer);
STEPS.forEach(s => {
const el = document.getElementById(`step-${s}`);
if (el) el.classList.remove("active");
});
}
// ── Viz Loading Helper ───────────────────────────────────────────
function setVizImg(id, b64) {
const el = document.getElementById(id);
if (!el) return;
if (b64) {
el.src = `data:image/jpeg;base64,${b64}`;
el.classList.add("loaded");
} else {
el.classList.remove("loaded");
el.src = "";
}
}
// ── Result Rendering ─────────────────────────────────────────────
function renderResults(data) {
const isFake = data.prediction === "AI-Generated";
const conf = data.confidence;
const probFake = data.prob_fake;
const probReal = 1 - probFake;
// ── Verdict Card ──────────────────────────────────────────────
const verdictCard = document.getElementById("verdictCard");
if (verdictCard) verdictCard.className = `verdict-card ${isFake ? "fake" : "real"}`;
const iconEl = document.getElementById("verdictIcon");
if (iconEl) iconEl.textContent = isFake ? "πŸ€–" : "πŸ“·";
const vtEl = document.getElementById("verdictText");
if (vtEl) {
vtEl.textContent = data.prediction;
vtEl.className = `verdict-text ${isFake ? "fake" : "real"}`;
}
const vsEl = document.getElementById("verdictSub");
if (vsEl) {
vsEl.textContent = `Overall confidence: ${conf.toFixed(1)}% Β· ${isFake
? "Characteristics of AI-generated content detected"
: "Characteristics of a real camera photograph detected"}`;
}
// Probability Bar
const pctFake = Math.round(probFake * 100);
const pctReal = Math.round(probReal * 100);
const pbReal = document.getElementById("probBarReal");
const pbFake = document.getElementById("probBarFake");
if (pbReal) pbReal.style.width = `${pctReal}%`;
if (pbFake) pbFake.style.width = `${pctFake}%`;
const prPct = document.getElementById("probRealPct");
const pfPct = document.getElementById("probFakePct");
if (prPct) prPct.textContent = `${pctReal}%`;
if (pfPct) pfPct.textContent = `${pctFake}%`;
// Confidence Ring
const ring = document.getElementById("ringFill");
const confVal = document.getElementById("confValue");
if (ring && confVal) {
const circumf = 2 * Math.PI * 50;
ring.style.strokeDashoffset = circumf * (1 - conf / 100);
ring.className = `ring-fill ${isFake ? "fake" : "real"}`;
confVal.textContent = `${Math.round(conf)}%`;
confVal.style.color = isFake ? "var(--fake)" : "var(--real)";
}
// ── Branch Cards ──────────────────────────────────────────────
const fused = data.fused_weights || {};
Object.entries(data.branches).forEach(([name, info]) => {
const probFakeB = info.prob_fake;
const conf_b = info.confidence;
const weight = fused[name] || 0;
const isFakeB = info.label === "AI-Generated";
const color = isFakeB ? "var(--fake)" : "var(--real)";
const card = document.getElementById(`bc-${name}`);
if (card) card.style.borderLeft = `3px solid ${color}`;
const bar = document.getElementById(`bar-${name}`);
if (bar) {
setTimeout(() => { bar.style.width = `${probFakeB * 100}%`; }, 100);
bar.style.background = color;
}
const probEl = document.getElementById(`prob-${name}`);
const confEl = document.getElementById(`conf-${name}`);
const weightEl = document.getElementById(`weight-${name}`);
if (probEl) {
probEl.textContent = `${(probFakeB * 100).toFixed(1)}% fake`;
probEl.style.color = color;
}
if (confEl) confEl.textContent = `conf: ${(conf_b * 100).toFixed(0)}%`;
if (weightEl) {
weightEl.textContent = `weight: ${(weight * 100).toFixed(1)}%`;
weightEl.style.color = weight > 0.25 ? "var(--accent)" : "var(--text3)";
}
});
// ── Visualizations ────────────────────────────────────────────
const gradcamImg = document.getElementById("img-gradcam");
const gradcamUnavail = document.getElementById("gradcam-unavail");
const gradcamLabel = document.getElementById("gradcam-label");
const gradcamSub = document.getElementById("gradcam-sub");
if (gradcamImg) {
if (data.gradcam_available && data.gradcam_b64) {
gradcamImg.src = `data:image/jpeg;base64,${data.gradcam_b64}`;
gradcamImg.classList.add("loaded");
if (gradcamUnavail) gradcamUnavail.style.display = "none";
if (gradcamLabel) gradcamLabel.textContent = "Saliency / Grad-CAM Heatmap";
if (gradcamSub) gradcamSub.textContent = "Suspicious regions highlighted (JET colormap)";
} else {
gradcamImg.classList.remove("loaded");
if (gradcamUnavail) gradcamUnavail.style.display = "flex";
if (gradcamLabel) gradcamLabel.textContent = "Grad-CAM Heatmap";
if (gradcamSub) gradcamSub.textContent = "Unavailable β€” CNN weights not loaded";
}
}
setVizImg("img-spectrum", data.spectrum_b64);
setVizImg("img-spectrum-ann", data.spectrum_annotated_b64);
setVizImg("img-noise", data.noise_map_b64);
setVizImg("img-edge", data.edge_map_b64);
const lcBanner = document.getElementById("lowCertaintyBanner");
if (lcBanner) lcBanner.style.display = data.low_certainty ? "block" : "none";
}
// ── Main Analysis Logic ──────────────────────────────────────────
async function runAnalysis() {
console.log("[Forensics] runAnalysis() started for file:", selectedFile ? selectedFile.name : "none");
if (!selectedFile) { showError("Please select an image first."); return; }
previewArea.style.display = "none";
resultsSection.style.display = "none";
errorBanner.style.display = "none";
analyzingState.style.display = "block";
animateSteps();
const formData = new FormData();
formData.append("file", selectedFile);
try {
console.log("[Forensics] Fetching /predict...");
const response = await fetch(`${API_BASE}/predict`, {
method: "POST",
body: formData,
});
console.log("[Forensics] /predict status:", response.status);
stopStepAnimation();
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || `Server error ${response.status}`);
}
const data = await response.json();
analyzingState.style.display = "none";
renderResults(data);
resultsSection.style.display = "block";
resultsSection.scrollIntoView({ behavior: "smooth", block: "start" });
} catch (e) {
console.error("[Forensics] Analysis error:", e);
stopStepAnimation();
analyzingState.style.display = "none";
previewArea.style.display = "flex";
showError(`Analysis failed: ${e.message}. Make sure the API server is running.`);
}
}
// ── File Handling ────────────────────────────────────────────────
function handleFile(file) {
if (!file.type.startsWith("image/")) {
showError("Please upload a valid image file (JPG, PNG, WebP, BMP).");
return;
}
if (file.size > 15 * 1024 * 1024) {
showError("File too large. Maximum allowed size is 15 MB.");
return;
}
selectedFile = file;
const url = URL.createObjectURL(file);
previewImg.src = url;
fileNameEl.textContent = file.name;
fileSizeEl.textContent = `${(file.size / 1024).toFixed(1)} KB Β· ${file.type}`;
hideError();
previewArea.style.display = "flex";
}
// ── Event Listeners ──────────────────────────────────────────────
if (uploadZone) {
uploadZone.addEventListener("dragover", e => {
e.preventDefault();
uploadZone.classList.add("drag-over");
});
uploadZone.addEventListener("dragleave", () => uploadZone.classList.remove("drag-over"));
uploadZone.addEventListener("drop", e => {
e.preventDefault();
uploadZone.classList.remove("drag-over");
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
uploadZone.addEventListener("click", e => {
if (e.target === selectBtn || selectBtn.contains(e.target)) return;
fileInput.click();
});
}
if (selectBtn) selectBtn.addEventListener("click", e => { e.stopPropagation(); fileInput.click(); });
if (fileInput) fileInput.addEventListener("change", () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); });
if (clearBtn) clearBtn.addEventListener("click", window.resetUI);
if (analyzeBtn) analyzeBtn.addEventListener("click", runAnalysis);
// Initialize
checkServerHealth();
setInterval(checkServerHealth, 15000);