AI_Image_detection / script.js
mani880740255's picture
Upload 16 files
6cc75b5 verified
Raw
History Blame Contribute Delete
17.1 kB
/* ========================================================
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)); }