Download frontend/static/js/app.js from ImgAuth/app: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/ImgAuth/app/resolve/main/frontend/static/js/app.js
- Command line
-
hf download hf://spaces/ImgAuth/app/frontend/static/js/app.js
-
curl -L -o app.js https://huggingface.co/spaces/ImgAuth/app/resolve/main/frontend/static/js/app.js
17.8 kB
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ImgAuth AI β Frontend Logic | |
| β Binary verdict: Likely AI-Generated vs Likely Authentic | |
| β Bug fix: result section scrolls into view after analysis | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| // ββ Theme Toggle ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| (function () { | |
| const html = document.documentElement; | |
| const btn = document.querySelector('[data-theme-toggle]'); | |
| let theme = localStorage.getItem('imgauth-theme') || 'dark'; | |
| html.setAttribute('data-theme', theme); | |
| if (btn) btn.textContent = theme === 'dark' ? 'π' : 'βοΈ'; | |
| if (btn) { | |
| btn.addEventListener('click', () => { | |
| theme = theme === 'dark' ? 'light' : 'dark'; | |
| html.setAttribute('data-theme', theme); | |
| btn.textContent = theme === 'dark' ? 'π' : 'βοΈ'; | |
| localStorage.setItem('imgauth-theme', theme); | |
| }); | |
| } | |
| })(); | |
| // ββ Element Refs ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const fileInput = document.getElementById('fileInput'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const previewZone = document.getElementById('previewZone'); | |
| const previewImg = document.getElementById('previewImg'); | |
| const previewFilename = document.getElementById('previewFilename'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const resultSection = document.getElementById('resultSection'); | |
| const newScanBtn = document.getElementById('newScanBtn'); | |
| let selectedFile = null; | |
| // ββ File Handling βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fileInput.addEventListener('change', e => handleFile(e.target.files[0])); | |
| dropZone.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| dropZone.classList.add('drag-over'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); | |
| dropZone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('drag-over'); | |
| const f = e.dataTransfer.files[0]; | |
| if (f) handleFile(f); | |
| }); | |
| function handleFile(file) { | |
| if (!file || !file.type.startsWith('image/')) { | |
| showToast('Please select a valid image file (PNG, JPG, WEBP).', 'error'); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| showToast('File too large. Maximum size is 10 MB.', 'error'); | |
| return; | |
| } | |
| selectedFile = file; | |
| const reader = new FileReader(); | |
| reader.onload = ev => { | |
| previewImg.src = ev.target.result; | |
| previewFilename.textContent = file.name; | |
| dropZone.style.display = 'none'; | |
| previewZone.style.display = 'block'; | |
| resultSection.style.display = 'none'; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| clearBtn.addEventListener('click', resetToUpload); | |
| // ββ Analyze βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| analyzeBtn.addEventListener('click', async () => { | |
| if (!selectedFile) return; | |
| showLoading(); | |
| try { | |
| const form = new FormData(); | |
| form.append('file', selectedFile); | |
| const res = await fetch('/api/detect', { method: 'POST', body: form }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.detail || 'Server error'); | |
| hideLoading(); | |
| showResult(data); | |
| } catch (err) { | |
| hideLoading(); | |
| showToast('Error: ' + err.message, 'error'); | |
| } | |
| }); | |
| newScanBtn.addEventListener('click', resetToUpload); | |
| function resetToUpload() { | |
| resultSection.style.display = 'none'; | |
| previewZone.style.display = 'none'; | |
| dropZone.style.display = 'block'; | |
| selectedFile = null; | |
| fileInput.value = ''; | |
| // Scroll back up to the upload section | |
| document.getElementById('uploadSection').scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| // ββ Loading Steps βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let stepTimer = null; | |
| const STEP_IDS = ['step1','step2','step3','step4','step5','step6']; | |
| function showLoading() { | |
| // Reset all steps | |
| STEP_IDS.forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.className = 'step'; | |
| }); | |
| loadingOverlay.style.display = 'flex'; | |
| let i = 0; | |
| stepTimer = setInterval(() => { | |
| if (i < STEP_IDS.length) { | |
| if (i > 0) { | |
| const prev = document.getElementById(STEP_IDS[i - 1]); | |
| if (prev) prev.className = 'step done'; | |
| } | |
| const cur = document.getElementById(STEP_IDS[i]); | |
| if (cur) cur.className = 'step active'; | |
| i++; | |
| } | |
| }, 700); | |
| } | |
| function hideLoading() { | |
| clearInterval(stepTimer); | |
| loadingOverlay.style.display = 'none'; | |
| } | |
| // ββ Show Result βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showResult(data) { | |
| const aiScore = typeof data.ai_score === 'number' ? data.ai_score : 50; | |
| const realScore = typeof data.real_score === 'number' ? data.real_score : 50; | |
| // ββ Populate result image | |
| document.getElementById('resultImg').src = previewImg.src; | |
| // ββ Binary verdict (no "Uncertain" category) | |
| const isAI = aiScore > 50; | |
| const badge = document.getElementById('resultBadge'); | |
| const verdict = document.getElementById('resultVerdict'); | |
| if (isAI) { | |
| badge.textContent = 'π΄ AI-Generated'; | |
| badge.className = 'verdict-badge is-ai'; | |
| verdict.textContent = 'Likely AI-Generated'; | |
| verdict.className = 'verdict-heading is-ai'; | |
| } else { | |
| badge.textContent = 'π’ Authentic'; | |
| badge.className = 'verdict-badge is-real'; | |
| verdict.textContent = 'Likely Authentic'; | |
| verdict.className = 'verdict-heading is-real'; | |
| } | |
| // ββ Confidence label β based on distance from 50% | |
| const margin = Math.abs(aiScore - 50); | |
| let confidenceLabel; | |
| if (margin >= 35) confidenceLabel = 'Very High Confidence'; | |
| else if (margin >= 20) confidenceLabel = 'High Confidence'; | |
| else if (margin >= 10) confidenceLabel = 'Medium Confidence'; | |
| else confidenceLabel = 'Low Confidence'; | |
| document.getElementById('resultConfidenceLevel').textContent = confidenceLabel; | |
| // ββ Gauge bar (animate after a tick so CSS transition fires) | |
| setTimeout(() => { | |
| document.getElementById('gaugeFill').style.width = aiScore + '%'; | |
| document.getElementById('gaugeDot').style.left = aiScore + '%'; | |
| document.getElementById('realPct').textContent = realScore.toFixed(1) + '%'; | |
| document.getElementById('aiPct').textContent = aiScore.toFixed(1) + '%'; | |
| }, 80); | |
| // ββ Plain-language summary | |
| document.getElementById('resultSummary').textContent = buildSummary(aiScore); | |
| // ββ "Why this result" cards | |
| buildWhyCards(data, aiScore); | |
| // ββ Heatmaps | |
| buildHeatmaps(data); | |
| // ββ Advanced technical section | |
| buildAdvanced(data); | |
| // ββ Advanced toggle β clone to remove stale listeners | |
| const oldBtn = document.getElementById('advancedToggle'); | |
| const newBtn = oldBtn.cloneNode(true); | |
| oldBtn.parentNode.replaceChild(newBtn, oldBtn); | |
| newBtn.addEventListener('click', function () { | |
| const body = document.getElementById('advancedBody'); | |
| const isOpen = body.style.display === 'block'; | |
| body.style.display = isOpen ? 'none' : 'block'; | |
| this.setAttribute('aria-expanded', String(!isOpen)); | |
| this.querySelector('span').textContent = isOpen | |
| ? 'Show Technical Analysis' | |
| : 'Hide Technical Analysis'; | |
| }); | |
| // ββ Show result section then scroll TO IT β BUG FIX | |
| resultSection.style.display = 'block'; | |
| setTimeout(() => { | |
| resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 60); | |
| } | |
| // ββ Build plain-language summary ββββββββββββββββββββββββββββββββββ | |
| function buildSummary(aiScore) { | |
| if (aiScore >= 80) return 'This image shows strong patterns typically found in AI-generated content. Multiple models flagged it with high confidence.'; | |
| if (aiScore >= 60) return 'This image shows several patterns commonly associated with AI-generated images.'; | |
| if (aiScore >= 50) return 'This image leans towards AI-generated, but the signal is relatively weak.'; | |
| if (aiScore >= 35) return 'This image appears to be authentic, though some minor signals were detected.'; | |
| return 'This image shows patterns consistent with authentic, real-world photographs.'; | |
| } | |
| // ββ Build "Why this result" cards βββββββββββββββββββββββββββββββββ | |
| function buildWhyCards(data, aiScore) { | |
| // Visual Patterns | |
| let visualText; | |
| if (aiScore >= 70) | |
| visualText = 'Detected unusual texture patterns and edge artifacts commonly found in AI-generated images.'; | |
| else if (aiScore >= 50) | |
| visualText = 'Some visual patterns are consistent with AI generation, though results are mixed.'; | |
| else | |
| visualText = 'Visual patterns appear natural and consistent with authentic photographs.'; | |
| document.getElementById('explainVisual').textContent = visualText; | |
| // AI Model Analysis | |
| let modelText; | |
| if (aiScore >= 70) | |
| modelText = 'Multiple AI detection models identified patterns strongly associated with AI-generated images.'; | |
| else if (aiScore >= 50) | |
| modelText = 'AI detection models show a mild lean toward generated content.'; | |
| else | |
| modelText = 'AI models detected characteristics more consistent with real, authentic photographs.'; | |
| document.getElementById('explainModels').textContent = modelText; | |
| // Metadata Check | |
| let metaText = 'No reliable camera metadata was found. This may indicate the image was generated or heavily edited.'; | |
| const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis'); | |
| if (mdLayer && mdLayer.signals) { | |
| const sigs = mdLayer.signals.join(' '); | |
| if (sigs.includes('camera EXIF') || sigs.includes('GPS')) | |
| metaText = 'Camera metadata was found, suggesting the image may have been captured with a real device.'; | |
| else if (sigs.includes('Software = AI') || sigs.includes('PNG metadata key')) | |
| metaText = 'Metadata explicitly references AI generation tools β a strong indicator of synthetic origin.'; | |
| } | |
| document.getElementById('explainMetadata').textContent = metaText; | |
| } | |
| // ββ Build Heatmaps ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildHeatmaps(data) { | |
| const focusAreas = document.getElementById('focusAreas'); | |
| const dfiCard = document.getElementById('dfiCardContainer'); | |
| const attImg = document.getElementById('attentionHeatmapImg'); | |
| const dfiImg = document.getElementById('dfiHeatmapImg'); | |
| let hasAny = false; | |
| const ml = data.layers && data.layers.models ? data.layers.models : {}; | |
| if (ml.attention_heatmap) { | |
| attImg.src = ml.attention_heatmap; | |
| hasAny = true; | |
| } | |
| if (ml.dfi_heatmap) { | |
| dfiImg.src = ml.dfi_heatmap; | |
| dfiCard.style.display = 'block'; | |
| hasAny = true; | |
| } else { | |
| dfiCard.style.display = 'none'; | |
| } | |
| focusAreas.style.display = hasAny ? 'block' : 'none'; | |
| } | |
| // ββ Build Advanced Technical Section βββββββββββββββββββββββββββββ | |
| function buildAdvanced(data) { | |
| const container = document.getElementById('advancedContent'); | |
| container.innerHTML = ''; | |
| const fnLayer = (data.breakdown || []).find(b => b.layer === 'Filename Analysis'); | |
| const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis'); | |
| const mlLayer = (data.breakdown || []).find(b => b.layer === 'AI Model and Forensic Detectors'); | |
| const votes = mlLayer ? (mlLayer.votes || []) : []; | |
| const forensics = mlLayer ? (mlLayer.forensics || {}) : {}; | |
| // Card 1 β Model Results | |
| const dlVotes = votes.filter(v => v.type === 'deep_learning'); | |
| const modelItems = dlVotes.length | |
| ? dlVotes.map(v => `<li><span>${esc(v.detector)}</span><span class="adv-val">${(v.ai_prob * 100).toFixed(1)}% AI</span></li>`).join('') | |
| : '<li><span>No models available</span></li>'; | |
| container.appendChild(makeAdvCard('Model Results', modelItems)); | |
| // Card 2 β Forensic Signatures | |
| const fmtNum = (v, dec = 4) => (v !== undefined && v !== null) ? Number(v).toFixed(dec) : 'N/A'; | |
| const kurtosis = forensics.kurtosis || {}; | |
| const dfi = forensics.dfi || {}; | |
| const fft = forensics.fft || {}; | |
| const chist = forensics.color_histogram || {}; | |
| const jpeg = forensics.jpeg_ghost || {}; | |
| const jpegNA = jpeg.detail === 'Not a JPEG' || !jpeg.ghost_spread; | |
| const forensicItems = ` | |
| <li><span>Noise Kurtosis</span> <span class="adv-val">${fmtNum(kurtosis.kurtosis)}</span></li> | |
| <li><span>DFI Variance</span> <span class="adv-val">${fmtNum(dfi.variance, 5)}</span></li> | |
| <li><span>FFT Spike Ratio</span> <span class="adv-val">${fmtNum(fft.spike_ratio, 5)}</span></li> | |
| <li><span>Histogram Roughness</span><span class="adv-val">${fmtNum(chist.roughness, 6)}</span></li> | |
| <li><span>JPEG Ghost Spread</span> <span class="adv-val">${jpegNA ? 'N/A (non-JPEG)' : fmtNum(jpeg.ghost_spread, 3)}</span></li> | |
| `; | |
| container.appendChild(makeAdvCard('Advanced Signals', forensicItems)); | |
| // Card 3 β Layer Weights | |
| const weightItems = ` | |
| <li><span>Filename Weight</span> <span class="adv-val">${fnLayer ? fnLayer.weight_pct : '0%'}</span></li> | |
| <li><span>Filename AI Score</span><span class="adv-val">${fnLayer ? fnLayer.ai_pts + ' pts' : 'β'}</span></li> | |
| <li><span>Metadata Weight</span> <span class="adv-val">${mdLayer ? mdLayer.weight_pct : '0%'}</span></li> | |
| <li><span>Metadata AI Score</span><span class="adv-val">${mdLayer ? mdLayer.ai_pts + ' pts' : 'β'}</span></li> | |
| <li><span>Model & Forensic Wt</span><span class="adv-val">${mlLayer ? mlLayer.weight_pct : '0%'}</span></li> | |
| <li><span>Model AI Score</span> <span class="adv-val">${mlLayer ? mlLayer.ai_pts + ' pts' : 'β'}</span></li> | |
| `; | |
| container.appendChild(makeAdvCard('Layer Breakdown', weightItems)); | |
| // Signal Log β full width | |
| const allSignals = [ | |
| ...(fnLayer ? fnLayer.signals || [] : []), | |
| ...(mdLayer ? mdLayer.signals || [] : []), | |
| ...(mlLayer ? mlLayer.signals || [] : []), | |
| ]; | |
| const logHtml = allSignals.length | |
| ? allSignals.map(s => { | |
| const clean = esc(s) | |
| .replace(/^\[AI\]\s*/i, '<span style="color:var(--red)">β </span>') | |
| .replace(/^\[REAL\]\s*/i, '<span style="color:var(--green)">β </span>') | |
| .replace(/^\[INFO\]\s*/i, '<span style="color:var(--text-3)">βΉ </span>') | |
| .replace(/^\[ERROR\]\s*/i, '<span style="color:var(--orange)">β </span>'); | |
| return clean; | |
| }).join('\n') | |
| : 'No signals detected.'; | |
| const logEl = document.createElement('div'); | |
| logEl.className = 'adv-log'; | |
| logEl.style.gridColumn = '1 / -1'; | |
| logEl.innerHTML = `<div style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-3);margin-bottom:10px;">Diagnostic Signal Log</div>${logHtml}`; | |
| container.appendChild(logEl); | |
| } | |
| function makeAdvCard(title, itemsHtml) { | |
| const card = document.createElement('div'); | |
| card.className = 'adv-card'; | |
| card.innerHTML = `<div class="adv-card-title">${title}</div><ul class="adv-list">${itemsHtml}</ul>`; | |
| return card; | |
| } | |
| // ββ Toast Notification ββββββββββββββββββββββββββββββββββββββββββββ | |
| function showToast(msg, type = 'info') { | |
| // Remove existing toast if any | |
| const old = document.getElementById('imgauth-toast'); | |
| if (old) old.remove(); | |
| const toast = document.createElement('div'); | |
| toast.id = 'imgauth-toast'; | |
| toast.style.cssText = ` | |
| position:fixed; bottom:28px; left:50%; transform:translateX(-50%); | |
| background:${type === 'error' ? 'var(--red-bg)' : 'var(--surface-solid)'}; | |
| border:1px solid ${type === 'error' ? 'var(--red-border)' : 'var(--border)'}; | |
| color:var(--text); padding:12px 24px; border-radius:10px; | |
| font-size:0.875rem; font-weight:600; font-family:var(--font); | |
| box-shadow:var(--shadow-lg); z-index:9999; | |
| animation:fadeInUp 0.25s ease; | |
| `; | |
| toast.textContent = msg; | |
| document.body.appendChild(toast); | |
| setTimeout(() => toast.remove(), 4000); | |
| } | |
| // Add toast animation | |
| const toastStyle = document.createElement('style'); | |
| toastStyle.textContent = ` | |
| @keyframes fadeInUp { | |
| from { opacity:0; transform:translateX(-50%) translateY(12px); } | |
| to { opacity:1; transform:translateX(-50%) translateY(0); } | |
| } | |
| `; | |
| document.head.appendChild(toastStyle); | |
| // ββ Utility βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function esc(str) { | |
| if (!str) return ''; | |
| return String(str).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); | |
| } |