| |
| |
| |
| |
|
|
| |
| |
| 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"); |
|
|
| |
| let selectedFile = null; |
| let stepTimer = null; |
| const STEPS = ["spectral", "edge", "cnn", "vit", "diffusion", "fusion"]; |
|
|
| |
| 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"); |
|
|
| |
| function showError(msg) { |
| errorMsg.textContent = msg; |
| errorBanner.style.display = "flex"; |
| } |
|
|
| function hideError() { |
| errorBanner.style.display = "none"; |
| } |
|
|
| |
| 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" }); |
| }; |
|
|
| |
| 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') { |
| |
| } else { |
| console.warn("[Forensics] Health check failed:", err.message); |
| } |
| serverStatus.textContent = "β API Offline"; |
| serverStatus.classList.add("offline"); |
| } |
| } |
|
|
| |
| 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"); |
| }); |
| } |
|
|
| |
| 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 = ""; |
| } |
| } |
|
|
| |
| function renderResults(data) { |
| const isFake = data.prediction === "AI-Generated"; |
| const conf = data.confidence; |
| const probFake = data.prob_fake; |
| const probReal = 1 - probFake; |
|
|
| |
| 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"}`; |
| } |
|
|
| |
| 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}%`; |
|
|
| |
| 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)"; |
| } |
|
|
| |
| 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)"; |
| } |
| }); |
|
|
| |
| 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"; |
| } |
|
|
| |
| 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.`); |
| } |
| } |
|
|
| |
| 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"; |
| } |
|
|
| |
| 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); |
|
|
| |
| checkServerHealth(); |
| setInterval(checkServerHealth, 15000); |
|
|