Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- Elements --- | |
| const fileInput = document.getElementById('file-input'); | |
| const uploadPanel = document.getElementById('upload-panel'); | |
| const previewPanel = document.getElementById('preview-panel'); | |
| // Preview | |
| const fileNameDisplay = document.getElementById('file-name'); | |
| const previewImage = document.getElementById('preview-image'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| const imageViewport = document.getElementById('image-viewport'); | |
| const blurBackdrop = document.getElementById('blur-backdrop'); | |
| // Buttons & Progress | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const newScanBtn = document.getElementById('new-scan-btn'); | |
| const progressContainer = document.getElementById('progress-container'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressText = document.getElementById('progress-text'); | |
| const scanningFx = document.getElementById('scanning-fx'); | |
| const resultFx = document.getElementById('result-fx'); | |
| // Telemetry Panel | |
| const logIdle = document.getElementById('log-idle'); | |
| const logActive = document.getElementById('log-active'); | |
| const logSteps = [ | |
| document.getElementById('step-meta'), | |
| document.getElementById('step-freq'), | |
| document.getElementById('step-noise'), | |
| document.getElementById('step-vit'), | |
| document.getElementById('step-agg') | |
| ]; | |
| // Info Panels | |
| const infoDisplays = document.getElementById('info-displays'); | |
| const resultsDisplay = document.getElementById('results-display'); | |
| // Results Data | |
| const resultCard = document.getElementById('result-card'); | |
| const verdictTitle = document.getElementById('final-verdict-title'); | |
| const verdictExplanation = document.getElementById('verdict-explanation'); | |
| const circleFg = document.getElementById('circle-fg'); | |
| const chartPct = document.getElementById('chart-pct'); | |
| // Stats | |
| const valDf = document.getElementById('val-df'); | |
| const barDf = document.getElementById('bar-df'); | |
| const valDct = document.getElementById('val-dct'); | |
| const barDct = document.getElementById('bar-dct'); | |
| const valNoise = document.getElementById('val-noise'); | |
| const barNoise = document.getElementById('bar-noise'); | |
| const valEdge = document.getElementById('val-edge'); | |
| const barEdge = document.getElementById('bar-edge'); | |
| let selectedFile = null; | |
| let analysisInProgress = false; | |
| // --- Drag & Drop --- | |
| uploadPanel.addEventListener('click', () => fileInput.click()); | |
| uploadPanel.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| uploadPanel.classList.add('dragover'); | |
| }); | |
| uploadPanel.addEventListener('dragleave', () => { | |
| uploadPanel.classList.remove('dragover'); | |
| }); | |
| uploadPanel.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| uploadPanel.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length > 0) setFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', e => { | |
| if (e.target.files.length > 0) setFile(e.target.files[0]); | |
| }); | |
| resetBtn.addEventListener('click', resetSystem); | |
| newScanBtn.addEventListener('click', resetSystem); | |
| analyzeBtn.addEventListener('click', runAnalysis); | |
| // --- Core Logic --- | |
| function setFile(file) { | |
| if (!file.type.startsWith('image/') && !file.name.toLowerCase().endsWith('.mp4')) { | |
| alert('Invalid format. Image or MP4 files only.'); | |
| return; | |
| } | |
| if (file.size > 20 * 1024 * 1024) { | |
| alert('File exceeds 20MB limit.'); | |
| return; | |
| } | |
| selectedFile = file; | |
| fileNameDisplay.textContent = file.name; | |
| const reader = new FileReader(); | |
| reader.onload = e => { | |
| const url = e.target.result; | |
| previewImage.src = url; | |
| blurBackdrop.style.backgroundImage = `url(${url})`; | |
| uploadPanel.classList.add('hidden'); | |
| previewPanel.classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| async function runAnalysis() { | |
| if (!selectedFile || analysisInProgress) return; | |
| analysisInProgress = true; | |
| // UI State -> Analyzing | |
| analyzeBtn.classList.add('hidden'); | |
| progressContainer.classList.remove('hidden'); | |
| scanningFx.classList.remove('hidden'); | |
| imageViewport.classList.add('analyzing'); | |
| infoDisplays.classList.add('hidden'); | |
| resultsDisplay.classList.add('hidden'); | |
| logIdle.classList.add('hidden'); | |
| logActive.classList.remove('hidden'); | |
| // Reset steps | |
| logSteps.forEach(s => { | |
| s.className = 'log-step'; | |
| s.querySelector('.step-icon').innerHTML = '<div class="dot-pending"></div>'; | |
| }); | |
| // Backend Request | |
| const formData = new FormData(); | |
| formData.append('file', selectedFile); | |
| let data = null; | |
| let fetchError = null; | |
| const fetchPromise = fetch('/predict', { method: 'POST', body: formData }) | |
| .then(r => { | |
| if (!r.ok) throw new Error(`HTTP Error ${r.status}`); | |
| return r.json(); | |
| }) | |
| .then(resData => { data = resData; }) | |
| .catch(e => { fetchError = e; }); | |
| // Fake cinematic progression | |
| const totalDuration = 4500; | |
| const stepDuration = totalDuration / logSteps.length; | |
| let p = 0; | |
| progressBar.style.width = '0%'; | |
| progressText.textContent = '0%'; | |
| const progressInterval = setInterval(() => { | |
| p += Math.random() * 2 + 0.5; | |
| if (p > 99) p = 99; | |
| progressBar.style.width = p + '%'; | |
| progressText.textContent = Math.floor(p) + '%'; | |
| }, 100); | |
| let stepIndex = 0; | |
| const stepInterval = setInterval(() => { | |
| if (stepIndex > 0) { | |
| // Mark previous as done | |
| const prev = logSteps[stepIndex-1]; | |
| prev.className = 'log-step done'; | |
| prev.querySelector('.step-icon').innerHTML = '<i data-lucide="check-circle-2" class="icon-done"></i>'; | |
| lucide.createIcons(); | |
| } | |
| if (stepIndex < logSteps.length) { | |
| const curr = logSteps[stepIndex]; | |
| curr.className = 'log-step active'; | |
| curr.querySelector('.step-icon').innerHTML = '<div class="dot-pulse"></div><div class="dot-active"></div>'; | |
| } | |
| stepIndex++; | |
| }, stepDuration); | |
| // Wait for both fetch AND cinematic time | |
| await Promise.all([ | |
| fetchPromise, | |
| new Promise(r => setTimeout(r, totalDuration)) | |
| ]); | |
| clearInterval(progressInterval); | |
| clearInterval(stepInterval); | |
| analysisInProgress = false; | |
| // Finish progress | |
| progressBar.style.width = '100%'; | |
| progressText.textContent = '100%'; | |
| // Finish final step | |
| const lastStep = logSteps[logSteps.length-1]; | |
| lastStep.className = 'log-step done'; | |
| lastStep.querySelector('.step-icon').innerHTML = '<i data-lucide="check-circle-2" class="icon-done"></i>'; | |
| lucide.createIcons(); | |
| if (fetchError) { | |
| alert("Analysis failed: " + fetchError.message); | |
| resetSystem(); | |
| return; | |
| } | |
| setTimeout(() => displayResults(data), 500); | |
| } | |
| function displayResults(data) { | |
| progressContainer.classList.add('hidden'); | |
| scanningFx.classList.add('hidden'); | |
| newScanBtn.classList.remove('hidden'); | |
| imageViewport.classList.remove('analyzing'); | |
| resultsDisplay.classList.remove('hidden'); | |
| resultsDisplay.classList.add('animate-in', 'slide-in-from-right-8', 'duration-700'); // generic tailwind-like util if css handles it | |
| // Determine state | |
| let isFake = false; | |
| let isUncertain = false; | |
| if (data.label === 'Fake') isFake = true; | |
| else if (data.label === 'Uncertain') isUncertain = true; | |
| // Styles | |
| resultCard.className = 'glass-panel result-panel h-full flex flex-col relative overflow-hidden'; | |
| if (isFake) { | |
| resultCard.classList.add('fake'); | |
| verdictTitle.innerHTML = '<i data-lucide="shield-alert"></i> SYNTHETIC'; | |
| circleFg.setAttribute('stroke', 'url(#roseGradient)'); | |
| resultFx.className = 'result-fx result-fake-bg'; | |
| } else if (isUncertain) { | |
| resultCard.classList.add('uncertain'); | |
| verdictTitle.innerHTML = '<i data-lucide="shield-alert"></i> AMBIGUOUS'; | |
| circleFg.setAttribute('stroke', 'url(#amberGradient)'); | |
| resultFx.className = 'result-fx'; | |
| } else { | |
| resultCard.classList.add('real'); | |
| verdictTitle.innerHTML = '<i data-lucide="shield-check"></i> AUTHENTIC'; | |
| circleFg.setAttribute('stroke', 'url(#emeraldGradient)'); | |
| resultFx.className = 'result-fx result-real-bg'; | |
| } | |
| lucide.createIcons(); | |
| resultFx.classList.remove('hidden'); | |
| verdictExplanation.textContent = data.explanation || "Integrity verification complete."; | |
| // Circle Chart | |
| const conf = data.confidence || 0; | |
| const offset = 264 - (264 * conf); | |
| circleFg.style.strokeDashoffset = offset; | |
| animateNumber(chartPct, 0, conf * 100, 1500); | |
| // Metric Bars | |
| const d = data.detail || {}; | |
| const n = d.neural || {}; | |
| const f = d.forensics || {}; | |
| setMetric(valDf, barDf, n['Deepfake Probability'] !== undefined ? n['Deepfake Probability'] * 100 : 0); | |
| // Inverse format for these, let's just render them as percentages relative to thresholds | |
| setMetric(valDct, barDct, f.dct_score !== undefined ? (1 - f.dct_score) * 100 : 0); | |
| setMetric(valNoise, barNoise, f.noise_score !== undefined ? f.noise_score * 100 : 0); | |
| setMetric(valEdge, barEdge, f.edge_score !== undefined ? f.edge_score * 100 : 0); | |
| } | |
| function setMetric(valEl, barEl, numVal) { | |
| numVal = Math.min(Math.max(numVal, 0), 100); | |
| valEl.textContent = numVal.toFixed(1) + '%'; | |
| let colorClass = 'bg-emerald-500'; | |
| if (numVal > 60) colorClass = 'bg-rose-500'; | |
| else if (numVal > 40) colorClass = 'bg-amber-500'; | |
| barEl.className = 'metric-fill ' + colorClass; | |
| barEl.style.width = '0%'; | |
| setTimeout(() => { barEl.style.width = numVal + '%'; }, 300); | |
| if (numVal > 60) valEl.classList.add('text-rose-400'); | |
| else valEl.classList.add('text-emerald-400'); | |
| } | |
| function resetSystem() { | |
| if (analysisInProgress) return; | |
| selectedFile = null; | |
| fileInput.value = ''; | |
| previewPanel.classList.add('hidden'); | |
| uploadPanel.classList.remove('hidden'); | |
| analyzeBtn.classList.remove('hidden'); | |
| newScanBtn.classList.add('hidden'); | |
| progressContainer.classList.add('hidden'); | |
| scanningFx.classList.add('hidden'); | |
| resultFx.classList.add('hidden'); | |
| imageViewport.classList.remove('analyzing'); | |
| circleFg.style.strokeDashoffset = 264; | |
| logIdle.classList.remove('hidden'); | |
| logActive.classList.add('hidden'); | |
| infoDisplays.classList.remove('hidden'); | |
| resultsDisplay.classList.add('hidden'); | |
| } | |
| function animateNumber(el, start, end, duration) { | |
| let startTime = null; | |
| const step = (ts) => { | |
| if (!startTime) startTime = ts; | |
| const progress = Math.min((ts - startTime) / duration, 1); | |
| el.textContent = Math.round(progress * (end - start) + start); | |
| if (progress < 1) requestAnimationFrame(step); | |
| }; | |
| requestAnimationFrame(step); | |
| } | |
| }); | |