const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const analysisSection = document.getElementById('analysis-section'); const statusText = document.getElementById('status-text'); const results = document.getElementById('results'); const loadingSpinner = document.getElementById('loading-spinner'); const chartCard = document.getElementById('chart-card'); // ------------------------------------------------------- // Chart setup // ------------------------------------------------------- const ctx = document.getElementById('audioChart').getContext('2d'); let audioChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, animation: { duration: 600, easing: 'easeInOutQuart' }, plugins: { legend: { display: true, labels: { color: '#94a3b8', font: { size: 12 } } }, tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)}% fake`, title: items => `Segment @ ${items[0].label}s` } } }, scales: { y: { beginAtZero: true, max: 100, ticks: { color: '#94a3b8', callback: v => v + '%' }, grid: { color: 'rgba(148,163,184,0.1)' }, title: { display: true, text: 'Fake Probability (%)', color: '#64748b' } }, x: { ticks: { color: '#94a3b8', callback: (_, i, ticks) => { // Show fewer labels when there are many windows const step = Math.max(1, Math.floor(ticks.length / 8)); return i % step === 0 ? audioChart.data.labels[i] + 's' : ''; } }, grid: { color: 'rgba(148,163,184,0.05)' }, title: { display: true, text: 'Time (seconds)', color: '#64748b' } } } } }); // Palette and display names for the four models const MODEL_META = { wav2vec2: { label: 'Wav2Vec2', color: '#3b82f6' }, aasist: { label: 'AASIST', color: '#f43f5e' }, cqcc_baseline: { label: 'CQCC Baseline', color: '#fbbf24' }, custom_hybrid: { label: 'Proposed Custom Hybrid', color: '#10b981' }, }; // ------------------------------------------------------- // File handling // ------------------------------------------------------- function handleFile(file) { if (!file) return; // Show sections analysisSection.classList.remove('hidden'); chartCard.classList.remove('hidden'); setTimeout(() => { analysisSection.classList.remove('opacity-0'); chartCard.classList.remove('opacity-0'); }, 50); results.classList.add('hidden'); loadingSpinner.classList.remove('hidden'); statusText.innerText = `Analyzing "${file.name}"…`; // Clear previous state document.getElementById('model-panels').innerHTML = ''; audioChart.data.labels = []; audioChart.data.datasets = []; audioChart.update(); // Animated placeholder while waiting: a single pulsing dataset const placeholder = { label: 'Analyzing…', data: Array.from({ length: 20 }, (_, i) => 45 + Math.sin(i / 2) * 10), borderColor: 'rgba(99,102,241,0.5)', backgroundColor: 'rgba(99,102,241,0.05)', borderDash: [4, 4], fill: true, tension: 0.4, pointRadius: 0, }; audioChart.data.labels = Array.from({ length: 20 }, (_, i) => i); audioChart.data.datasets = [placeholder]; audioChart.update(); let tick = 0; const loadingAnim = setInterval(() => { tick++; placeholder.data = Array.from({ length: 20 }, (_, i) => 45 + Math.sin((i + tick) / 2) * 10 ); audioChart.update('none'); // skip animation for perf }, 80); const formData = new FormData(); formData.append('file', file); const HF_API_URL = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' ? '/api/predict' : 'https://junsiang26-odiocheck-backend.hf.space/api/predict'; fetch(HF_API_URL, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { clearInterval(loadingAnim); loadingSpinner.classList.add('hidden'); if (data.error) { statusText.innerText = 'Error analyzing file.'; console.error(data.error); return; } renderResults(data); }) .catch(() => { clearInterval(loadingAnim); loadingSpinner.classList.add('hidden'); statusText.innerText = 'Connection error. Is the backend running?'; }); } // ------------------------------------------------------- // Render results from the new response shape: // data.overall → { model: { prediction, fake_probability, real_probability } } // data.timeline → { model: [fake_prob_pct, ...] } // data.window_labels → [centre_sec, ...] // ------------------------------------------------------- function renderResults(data) { const { overall, timeline, window_labels } = data; statusText.innerText = 'Analysis Complete'; results.classList.remove('hidden'); // --- Model panels (overall verdict) --- const panelsEl = document.getElementById('model-panels'); panelsEl.innerHTML = ''; for (const [key, info] of Object.entries(overall)) { const meta = MODEL_META[key] || { label: key, color: '#94a3b8' }; const isFake = info.prediction === 'FAKE'; const barColor = isFake ? 'from-rose-500 to-rose-400' : 'from-emerald-400 to-emerald-500'; const displayPct = isFake ? info.fake_probability : info.real_probability; panelsEl.insertAdjacentHTML('beforeend', `