Spaces:
Configuration error
Configuration error
| /* ======================================================== | |
| VisionAI β script.js | |
| Real app flow: | |
| - Uploads image to /analyze (Flask + HuggingFace model) | |
| - Model: umm-maybe/AI-image-detector | |
| - Labels: "artificial" | "real" | |
| - Threshold: 50% β Twilio voice call alert | |
| ======================================================== */ | |
| // βββ SESSION STATS βββ | |
| const SESSION = { total: 0, ai: 0, real: 0, calls: 0 }; | |
| // βββ HISTORY βββ | |
| const HISTORY = []; | |
| // βββ DOM REFS βββ | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const uploadSection = document.getElementById('upload-section'); | |
| const previewSection = document.getElementById('preview-section'); | |
| const previewImg = document.getElementById('preview-img'); | |
| const previewOverlay = document.getElementById('preview-overlay'); | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const resultsSection = document.getElementById('results-section'); | |
| const historyList = document.getElementById('history-list'); | |
| const toastWrap = document.getElementById('toast-wrap'); | |
| const ringFill = document.getElementById('ring-fill'); | |
| const ringPct = document.getElementById('ring-pct'); | |
| const ringLabelText = document.getElementById('ring-label-text'); | |
| const callPanel = document.getElementById('call-panel'); | |
| let currentFile = null; | |
| let lastResult = null; | |
| // βββ INIT βββ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Splash boot | |
| const splash = document.getElementById('splash'); | |
| const splashBar = document.getElementById('splash-bar'); | |
| requestAnimationFrame(() => requestAnimationFrame(() => { splashBar.style.width = '100%'; })); | |
| setTimeout(() => { | |
| splash.classList.add('hidden'); | |
| setTimeout(() => splash.remove(), 650); | |
| }, 1800); | |
| initDrag(); | |
| updateStats(); | |
| renderHistory(); | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.target.files[0]); }); | |
| analyzeBtn.addEventListener('click', runAnalysis); | |
| document.getElementById('export-btn').addEventListener('click', exportReport); | |
| initNav(); | |
| }); | |
| // βββ NAV βββ | |
| function initNav() { | |
| document.querySelectorAll('.nav-item').forEach(item => { | |
| item.addEventListener('click', e => { | |
| e.preventDefault(); | |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); | |
| item.classList.add('active'); | |
| }); | |
| }); | |
| } | |
| // βββ DRAG & DROP βββ | |
| function initDrag() { | |
| ['dragenter','dragover'].forEach(ev => | |
| dropZone.addEventListener(ev, e => { e.preventDefault(); dropZone.classList.add('dragover'); }) | |
| ); | |
| ['dragleave','dragend'].forEach(ev => | |
| dropZone.addEventListener(ev, () => dropZone.classList.remove('dragover')) | |
| ); | |
| dropZone.addEventListener('drop', e => { | |
| e.preventDefault(); dropZone.classList.remove('dragover'); | |
| if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| } | |
| // βββ FILE HANDLER βββ | |
| function handleFile(file) { | |
| if (!file.type.startsWith('image/')) { showToast('Only image files are supported.', 'error'); return; } | |
| currentFile = file; | |
| const reader = new FileReader(); | |
| reader.onload = evt => { | |
| previewImg.src = evt.target.result; | |
| const img = new Image(); | |
| img.onload = () => document.getElementById('meta-dims').textContent = `${img.width} Γ ${img.height} px`; | |
| img.src = evt.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| document.getElementById('meta-name').textContent = file.name; | |
| document.getElementById('meta-size').textContent = formatBytes(file.size); | |
| document.getElementById('meta-type').textContent = file.type.split('/')[1].toUpperCase(); | |
| uploadSection.style.display = 'none'; | |
| previewSection.style.display = 'flex'; | |
| resultsSection.style.display = 'none'; | |
| showToast('Image ready β click Run Detection', 'info'); | |
| } | |
| // βββ ANALYSIS βββ | |
| async function runAnalysis() { | |
| if (!currentFile) return; | |
| analyzeBtn.disabled = true; | |
| previewOverlay.classList.add('active'); | |
| const formData = new FormData(); | |
| formData.append('image', currentFile); | |
| let result; | |
| try { | |
| const resp = await fetch('/analyze', { method: 'POST', body: formData }); | |
| if (!resp.ok) throw new Error('Server error ' + resp.status); | |
| result = await resp.json(); | |
| if (result.error) throw new Error(result.error); | |
| } catch (err) { | |
| // ββ DEMO MODE (when Flask is not running) ββββββββββββββββββ | |
| await sleep(2200); | |
| const isAI = Math.random() > 0.48; | |
| const artScore = isAI | |
| ? Math.round(55 + Math.random() * 44) // 55β99 | |
| : Math.round(5 + Math.random() * 38); // 5β43 | |
| const realScore = 100 - artScore; | |
| const callPlaced = isAI; | |
| result = { | |
| filename: currentFile.name, | |
| is_ai: isAI, | |
| artificial_score: artScore, | |
| real_score: realScore, | |
| all_scores: [ | |
| { label: 'artificial', score: artScore }, | |
| { label: 'real', score: realScore }, | |
| ], | |
| threshold: 50, | |
| call_placed: callPlaced, | |
| call_sid: callPlaced ? 'CA' + Math.random().toString(36).substr(2,32).toUpperCase() : null, | |
| call_error: null, | |
| alert_phone: '+919047432845', | |
| _demo: true, | |
| }; | |
| } | |
| previewOverlay.classList.remove('active'); | |
| analyzeBtn.disabled = false; | |
| lastResult = result; | |
| displayResults(result); | |
| updateSessionStats(result); | |
| addHistory(result); | |
| } | |
| // βββ DISPLAY RESULTS βββ | |
| function displayResults(r) { | |
| const isAI = r.is_ai; | |
| const score = r.artificial_score; // confidence in "artificial" | |
| const color = isAI ? '#f43f5e' : '#22d3ee'; | |
| const glow = isAI ? 'rgba(244,63,94,.5)' : 'rgba(34,211,238,.4)'; | |
| const labelName = isAI ? 'artificial' : 'real'; | |
| // ββ Ring ββ | |
| ringFill.style.stroke = color; | |
| ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`; | |
| const offset = 283 - (score / 100) * 283; | |
| setTimeout(() => { ringFill.style.strokeDashoffset = offset; }, 80); | |
| animateCount(0, score, 1200, v => { ringPct.textContent = v + '%'; }); | |
| ringLabelText.textContent = labelName; | |
| // ββ Verdict ββ | |
| document.getElementById('result-verdict').textContent = | |
| isAI ? 'β AI-Generated Image Detected' : 'β Real / Human-Captured Image'; | |
| document.getElementById('result-desc').textContent = isAI | |
| ? `The model classified this image as AI-generated with ${score}% confidence. ` + | |
| `This exceeds the 50% threshold β a Twilio voice call has been placed to ${r.alert_phone}.` | |
| : `The model found no AI-generation patterns. Artificial score: ${score}% (below the 50% alert threshold). ` + | |
| `This image appears to be real or human-captured.`; | |
| // ββ Badges ββ | |
| const badges = isAI | |
| ? ['AI-Generated', `${score}% Confidence`, 'Alert Fired'] | |
| : ['Image Authentic', `${score}% AI Score`, 'No Alert']; | |
| document.getElementById('result-badges').innerHTML = badges.map((b, i) => | |
| `<span class="result-badge ${isAI ? (i===2?'badge-neutral':'badge-danger') : 'badge-success'}">${b}</span>` | |
| ).join(''); | |
| // ββ Score Breakdown ββ | |
| document.getElementById('breakdown-metrics').innerHTML = r.all_scores.map(s => { | |
| const c = s.label === 'artificial' ? '#f43f5e' : '#22d3ee'; | |
| return ` | |
| <div class="metric"> | |
| <div class="metric-header"> | |
| <span class="metric-label">${s.label}</span> | |
| <span class="metric-value" style="color:${c}">${s.score}%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="background:${c}" data-target="${s.score}"></div> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // ββ Threshold panel ββ | |
| const thresholdFired = score > r.threshold; | |
| document.getElementById('threshold-metrics').innerHTML = ` | |
| <div class="metric"> | |
| <div class="metric-header"> | |
| <span class="metric-label">AI Score</span> | |
| <span class="metric-value" style="color:${color}">${score}%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="background:${color}" data-target="${score}"></div> | |
| </div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-header"> | |
| <span class="metric-label">Alert Threshold</span> | |
| <span class="metric-value" style="color:#f59e0b">50%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="background:#f59e0b" data-target="50"></div> | |
| </div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-header"> | |
| <span class="metric-label">Threshold Status</span> | |
| <span class="metric-value" style="color:${thresholdFired?'#f43f5e':'#22d3ee'}"> | |
| ${thresholdFired ? 'β Exceeded' : 'β Below'} | |
| </span> | |
| </div> | |
| </div>`; | |
| // Animate progress bars | |
| setTimeout(() => { | |
| document.querySelectorAll('.progress-fill').forEach(bar => { | |
| bar.style.width = bar.dataset.target + '%'; | |
| }); | |
| }, 120); | |
| // ββ Twilio Call Panel ββ | |
| renderCallPanel(r); | |
| // ββ Show ββ | |
| previewSection.style.display = 'none'; | |
| resultsSection.style.display = 'flex'; | |
| const msg = isAI | |
| ? `β AI Detected! ${score}% confidence${r.call_placed ? ' β Call placed to ' + r.alert_phone : ''}` | |
| : `β Image verified as real (${score}% AI score)`; | |
| showToast(msg, isAI ? 'error' : 'success'); | |
| setTimeout(() => resultsSection.scrollIntoView({ behavior:'smooth', block:'start' }), 150); | |
| } | |
| // βββ CALL PANEL βββ | |
| function renderCallPanel(r) { | |
| callPanel.className = 'call-panel'; | |
| const icon = document.getElementById('call-panel-icon'); | |
| const title = document.getElementById('call-panel-title'); | |
| const detail = document.getElementById('call-panel-detail'); | |
| const badge = document.getElementById('call-panel-badge'); | |
| const sidRow = document.getElementById('call-sid-row'); | |
| const sidVal = document.getElementById('call-sid-val'); | |
| if (r.call_placed) { | |
| callPanel.classList.add('call-fired'); | |
| title.textContent = 'π Twilio Alert Call Placed'; | |
| detail.textContent = `Voice call placed to ${r.alert_phone} from +17125825991 Β· AI confidence was ${r.artificial_score}%`; | |
| badge.textContent = 'ALERT FIRED'; | |
| if (r.call_sid) { | |
| sidRow.style.display = 'flex'; | |
| sidVal.textContent = r.call_sid; | |
| } | |
| } else if (r.call_error) { | |
| callPanel.classList.add('call-error'); | |
| title.textContent = 'β Call Failed'; | |
| detail.textContent = `Alert would have fired, but Twilio returned an error: ${r.call_error}`; | |
| badge.textContent = 'ERROR'; | |
| sidRow.style.display = 'none'; | |
| } else { | |
| callPanel.classList.add('call-safe'); | |
| title.textContent = 'β No Alert Required'; | |
| detail.textContent = `AI score (${r.artificial_score}%) is below the 50% threshold β no call placed to ${r.alert_phone}`; | |
| badge.textContent = 'SAFE'; | |
| sidRow.style.display = 'none'; | |
| } | |
| if (r._demo) { | |
| detail.textContent += ' (demo mode β Flask not running)'; | |
| } | |
| } | |
| // βββ SESSION STATS βββ | |
| function updateSessionStats(r) { | |
| SESSION.total++; | |
| if (r.is_ai) SESSION.ai++; else SESSION.real++; | |
| if (r.call_placed) SESSION.calls++; | |
| updateStats(); | |
| } | |
| function updateStats() { | |
| document.getElementById('stat-total').textContent = SESSION.total; | |
| document.getElementById('stat-ai').textContent = SESSION.ai; | |
| document.getElementById('stat-real').textContent = SESSION.real; | |
| document.getElementById('stat-calls').textContent = SESSION.calls; | |
| } | |
| // βββ HISTORY βββ | |
| function addHistory(r) { | |
| HISTORY.unshift({ | |
| name: r.filename, | |
| time: 'Just now', | |
| score: r.artificial_score, | |
| label: r.is_ai ? 'AI-Gen' : 'Real', | |
| isAI: r.is_ai, | |
| called: r.call_placed, | |
| }); | |
| if (HISTORY.length > 10) HISTORY.pop(); | |
| renderHistory(); | |
| } | |
| function renderHistory() { | |
| if (!HISTORY.length) { | |
| historyList.innerHTML = `<div style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">No scans yet β upload an image to get started</div>`; | |
| return; | |
| } | |
| historyList.innerHTML = HISTORY.map(h => { | |
| const color = h.isAI ? '#f43f5e' : '#22d3ee'; | |
| const bg = h.isAI ? 'rgba(244,63,94,.12)' : 'rgba(34,211,238,.10)'; | |
| const callBadge = h.called | |
| ? `<span style="font-size:10px;color:#c4b5fd;background:rgba(168,85,247,.12);padding:2px 7px;border-radius:999px;border:1px solid rgba(168,85,247,.25);margin-left:6px;">π Alerted</span>` | |
| : ''; | |
| return ` | |
| <div class="history-item"> | |
| <div class="history-thumb"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> | |
| </div> | |
| <div class="history-meta"> | |
| <div class="history-name">${h.name}</div> | |
| <div class="history-time">${h.time}</div> | |
| </div> | |
| <span style="color:${color};background:${bg};border:1px solid ${color}33;border-radius:999px;padding:3px 10px;font-size:11px;font-weight:700;">${h.label} Β· ${h.score}%</span> | |
| ${callBadge} | |
| </div>`; | |
| }).join(''); | |
| } | |
| // βββ RESET βββ | |
| function resetAll() { | |
| currentFile = null; | |
| fileInput.value = ''; | |
| uploadSection.style.display = ''; | |
| previewSection.style.display = 'none'; | |
| resultsSection.style.display = 'none'; | |
| previewImg.src = ''; | |
| previewOverlay.classList.remove('active'); | |
| ringFill.style.strokeDashoffset = 283; | |
| ringPct.textContent = '0%'; | |
| showToast('Ready for new scan.', 'info'); | |
| } | |
| // βββ EXPORT βββ | |
| function exportReport() { | |
| if (!lastResult) { showToast('No result to export yet.', 'error'); return; } | |
| const r = lastResult; | |
| const date = new Date().toLocaleString(); | |
| const divider = 'β'.repeat(40); | |
| const text = [ | |
| `VisionAI β Detection Report`, | |
| divider, | |
| `Date : ${date}`, | |
| `Model : umm-maybe/AI-image-detector`, | |
| `File : ${r.filename}`, | |
| ``, | |
| `Results`, | |
| divider, | |
| `Verdict : ${r.is_ai ? 'AI-GENERATED' : 'REAL IMAGE'}`, | |
| `Artificial Score: ${r.artificial_score}%`, | |
| `Real Score : ${r.real_score}%`, | |
| `Alert Threshold : ${r.threshold}%`, | |
| ``, | |
| `Twilio Alert`, | |
| divider, | |
| `Alert Phone : ${r.alert_phone}`, | |
| `Call Placed : ${r.call_placed ? 'YES' : 'NO'}`, | |
| r.call_sid ? `Call SID : ${r.call_sid}` : '', | |
| r.call_error ? `Call Error : ${r.call_error}` : '', | |
| ``, | |
| r._demo ? '(Note: Demo mode β Flask backend was not running)' : '', | |
| `Generated by VisionAI Pro`, | |
| ].filter(l => l !== undefined).join('\n'); | |
| const blob = new Blob([text], { type: 'text/plain' }); | |
| const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: `visionai_${r.filename}_report.txt` }); | |
| a.click(); | |
| showToast('Report downloaded!', 'success'); | |
| } | |
| // βββ TOAST βββ | |
| function showToast(msg, type = 'info') { | |
| const iconMap = { | |
| success: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`, | |
| error: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`, | |
| info: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`, | |
| warning: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`, | |
| }; | |
| const el = document.createElement('div'); | |
| el.className = `toast toast-${type}`; | |
| el.innerHTML = `${iconMap[type]}<span class="toast-msg">${msg}</span>`; | |
| toastWrap.appendChild(el); | |
| setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px)'; el.style.transition = '.3s ease'; setTimeout(() => el.remove(), 350); }, 3800); | |
| } | |
| // βββ HELPERS βββ | |
| function formatBytes(b) { | |
| if (b < 1024) return b + ' B'; | |
| if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; | |
| return (b / 1048576).toFixed(2) + ' MB'; | |
| } | |
| function animateCount(from, to, dur, cb) { | |
| const start = performance.now(); | |
| (function step(ts) { | |
| const p = Math.min((ts - start) / dur, 1); | |
| cb(Math.round(from + (to - from) * (1 - Math.pow(1 - p, 3)))); | |
| if (p < 1) requestAnimationFrame(step); | |
| })(performance.now()); | |
| } | |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | |