Plan activated!
Your subscription is now active. Start extracting clips!
Basic
0 / 20 clips this month
Manage plan
Drop your video here or click to browse
MP4, MOV, AVI, WebM · Max 2GB
Uploading…
Your clips are ready
// Upload drag/drop uploadBox.addEventListener('click', () => videoInput.click()); uploadBox.addEventListener('dragover', (e) => { e.preventDefault(); uploadBox.classList.add('drag-over'); }); uploadBox.addEventListener('dragleave', () => uploadBox.classList.remove('drag-over')); uploadBox.addEventListener('drop', (e) => { e.preventDefault(); uploadBox.classList.remove('drag-over'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); videoInput.addEventListener('change', () => { if (videoInput.files.length) handleFile(videoInput.files[0]); }); async function handleFile(file) { if (!file.type.startsWith('video/')) { showError('Please select a video file.'); return; } hideError(); setStatus('Uploading video…', 10); try { const fd = new FormData(); fd.append('video', file); const res = await fetch('/api/upload', { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Upload failed'); jobId = data.jobId; videoUrl = data.videoUrl; // Save user ID for billing if (data.userId) setUserId(data.userId); setStatus('Analyzing footage…', 40); await analyzeVideo(file); } catch (err) { showError(err.message); statusBar.classList.remove('visible'); } } async function analyzeVideo(file) { setStatus('Sampling frames…', 60); const video = document.createElement('video'); video.src = URL.createObjectURL(file); video.muted = true; await new Promise((resolve) => { video.onloadedmetadata = resolve; video.load(); }); const duration = video.duration; const frameCount = Math.min(15, Math.floor(duration / 10) + 1); const interval = duration / frameCount; const frames = []; // Seek + capture frames via canvas const canvas = document.createElement('canvas'); canvas.width = 320; canvas.height = 180; const ctx = canvas.getContext('2d'); for (let i = 0; i < frameCount; i++) { const t = i * interval; await new Promise((resolve) => { video.currentTime = t; video.onseeked = () => { ctx.drawImage(video, 0, 0, 320, 180); frames.push({ timestamp: Math.round(t), dataUrl: canvas.toDataURL('image/jpeg', 0.6) }); resolve(); }; }); setStatus(`Analyzing frame ${i + 1}/${frameCount}…`, 60 + Math.round((i / frameCount) * 20)); } URL.revokeObjectURL(video.src); setStatus('AI picking the best clips…', 85); const res = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jobId, frames }) }); if (res.status === 402) { // Usage limit hit — redirect to pricing const data = await res.json(); showErrorHtml('Monthly clip limit reached. Upgrade your plan to continue.'); statusBar.classList.remove('visible'); uploadBox.style.display = ''; return; } const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Analysis failed'); setStatus('Done!', 100); showClips(data.clips); } function setStatus(label, pct) { statusBar.classList.add('visible'); uploadBox.style.display = 'none'; statusLabel.textContent = label; progressFill.style.width = pct + '%'; if (pct >= 100) progressFill.classList.add('full'); } function showClips(clips) { clipsSection.classList.add('visible'); statusBar.classList.remove('visible'); if (!clips || clips.length === 0) { clipsList.innerHTML = '
No clips generated. Try a longer video.
'; return; } clipsList.innerHTML = clips.map(clip => `
${Math.floor(clip.start/60)}:${String(clip.start%60).padStart(2,'0')}–${Math.floor(clip.end/60)}:${String(clip.end%60).padStart(2,'0')}
${escapeHtml(clip.name)}
${formatDuration(clip.start, clip.end)} · ${clip.format === 'vertical' ? '9:16' : '16:9'}
${escapeHtml(clip.reason || '')}
${clip.format || 'h'}
`).join(''); } function formatDuration(start, end) { const s = Math.floor(start); const e = Math.floor(end); const dur = e - s; return `${fmt(s)} → ${fmt(e)} (${dur}s)`; } function fmt(s) { const m = Math.floor(s / 60); const sec = s % 60; return `${m}:${String(sec).padStart(2, '0')}`; } function showError(msg) { errorMsg.textContent = msg; errorMsg.classList.add('visible'); } function showErrorHtml(html) { errorMsg.innerHTML = html; errorMsg.classList.add('visible'); } function hideError() { errorMsg.classList.remove('visible'); } function escapeHtml(str) { return str.replace(/&/g,'&').replace(//g,'>'); }