Spaces:
Configuration error
Configuration error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="description" content="CapVideo turns a lesson video into captions, a searchable transcript, chapters, and recall cards."> | |
| <title>CapVideo — Learn from every video</title> | |
| <style> | |
| :root { --ink:#edf1ff; --muted:#a7b1d4; --line:rgba(184,196,255,.17); --card:rgba(15,21,47,.78); --purple:#9d7bff; --mint:#54e3c6; --danger:#ff7b9c; } | |
| * { box-sizing:border-box; } body { margin:0; color:var(--ink); background:#080b1c; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } | |
| body:before, body:after { content:""; position:fixed; z-index:-1; border-radius:999px; filter:blur(2px); opacity:.45; } body:before { width:34rem; height:34rem; top:-16rem; left:-9rem; background:#5329bd; } body:after { width:30rem; height:30rem; right:-12rem; bottom:-14rem; background:#0e7a79; } | |
| a { color:inherit; } .page { width:min(1160px, calc(100% - 32px)); margin:auto; padding:24px 0 64px; } | |
| nav { display:flex; align-items:center; justify-content:space-between; gap:16px; } .brand { display:flex; align-items:center; gap:10px; text-decoration:none; font-weight:800; letter-spacing:-.03em; font-size:1.18rem; }.mark { display:grid; place-items:center; width:34px; height:34px; color:#0b1127; background:linear-gradient(135deg,var(--mint),#9dc6ff); border-radius:11px; font-size:1.05rem; } | |
| .tag { border:1px solid var(--line); color:#cbd5ff; padding:7px 10px; border-radius:999px; font-size:.78rem; } | |
| .hero { padding:74px 0 38px; max-width:820px; } .eyebrow { display:inline-flex; gap:8px; align-items:center; color:#aaffea; border:1px solid rgba(84,227,198,.24); background:rgba(28,119,108,.16); padding:7px 11px; border-radius:999px; font-size:.8rem; font-weight:700; letter-spacing:.02em; } | |
| h1 { font-size:clamp(2.8rem,8vw,5.9rem); line-height:.94; letter-spacing:-.075em; margin:20px 0; } h1 span { color:transparent; background:linear-gradient(100deg,#ad91ff,#6fe8d0); background-clip:text; } .lead { max-width:670px; margin:0; color:var(--muted); font-size:clamp(1.05rem,2vw,1.27rem); line-height:1.65; } | |
| .grid { display:grid; grid-template-columns:minmax(0,1.45fr) minmax(280px,.75fr); gap:20px; } .panel { padding:clamp(20px,4vw,34px); border:1px solid var(--line); background:var(--card); box-shadow:0 24px 80px rgba(0,0,0,.22); border-radius:22px; backdrop-filter:blur(16px); } | |
| .panel h2 { margin:0 0 6px; letter-spacing:-.035em; font-size:1.45rem; } .small { color:var(--muted); line-height:1.55; font-size:.91rem; } | |
| .source-switch { display:flex; gap:8px; padding:5px; border-radius:13px; background:rgba(5,8,23,.7); margin:24px 0 16px; width:max-content; max-width:100%; } .source-switch button { appearance:none; border:0; color:var(--muted); background:transparent; padding:10px 13px; cursor:pointer; font:inherit; font-size:.9rem; font-weight:700; border-radius:9px; } .source-switch button[aria-selected="true"] { color:#0b1127; background:linear-gradient(100deg,#a896ff,#69e6cf); } | |
| .source { display:none; }.source.active { display:block; }.drop { display:block; border:1.5px dashed rgba(157,123,255,.62); background:rgba(89,65,164,.1); padding:31px 20px; border-radius:16px; text-align:center; cursor:pointer; transition:.2s ease; }.drop:hover,.drop:focus-within { border-color:var(--mint); background:rgba(84,227,198,.09); } .drop strong { display:block; font-size:1.05rem; margin:5px 0 4px; }.drop input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; } | |
| label.field { display:block; color:#c8d1f7; font-size:.83rem; font-weight:750; margin:12px 0 7px; } input[type="url"], select { width:100%; color:var(--ink); background:#090d24; border:1px solid var(--line); border-radius:12px; padding:13px 14px; font:inherit; outline:none; } input:focus,select:focus { border-color:var(--mint); box-shadow:0 0 0 3px rgba(84,227,198,.12); } select { padding:10px; } | |
| .settings { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin-top:17px; }.settings label { color:var(--muted); font-size:.78rem; }.settings select { margin-top:6px; font-size:.85rem; } | |
| .button { width:100%; border:0; border-radius:13px; padding:14px 18px; cursor:pointer; color:#0a1023; background:linear-gradient(100deg,#a58aff,#64ebd0); font:inherit; font-weight:850; margin-top:22px; transition:transform .18s,filter .18s; }.button:hover { transform:translateY(-1px); filter:brightness(1.07); }.button:disabled { cursor:wait; opacity:.6; transform:none; } | |
| .notice { display:flex; gap:9px; padding:13px; border-left:3px solid #ebb96a; margin-top:15px; color:#e3d6bd; background:rgba(164,105,20,.1); border-radius:8px; font-size:.82rem; line-height:1.45; }.features { display:grid; gap:12px; margin-top:22px; }.feature { display:flex; gap:12px; padding:14px 0; border-bottom:1px solid var(--line); }.feature:last-child { border:0; }.feature b { display:block; margin-bottom:3px; }.feature-icon { width:33px; height:33px; flex:0 0 auto; display:grid; place-items:center; background:rgba(125,104,213,.2); border-radius:10px; } | |
| #status { display:none; margin-top:20px; }.status-top { display:flex; justify-content:space-between; gap:15px; align-items:baseline; }.status-top b { font-size:.94rem; }.status-top span { color:var(--mint); font-size:.8rem; font-weight:800; }.progress { height:8px; overflow:hidden; border-radius:99px; background:#06091a; margin:12px 0 8px; }.progress div { height:100%; width:4%; border-radius:inherit; background:linear-gradient(90deg,#9d7bff,#54e3c6); transition:width .45s ease; } | |
| #error { display:none; margin-top:18px; padding:13px; border:1px solid rgba(255,123,156,.3); color:#ffc1cf; background:rgba(165,41,73,.16); border-radius:12px; font-size:.9rem; line-height:1.45; } | |
| #results { display:none; margin-top:22px; }.result-head { display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px; margin-bottom:14px; }.result-head h2 { margin:0; }.downloads { display:flex; flex-wrap:wrap; gap:8px; }.downloads a { text-decoration:none; padding:8px 10px; border:1px solid var(--line); border-radius:9px; color:#dce2ff; font-size:.8rem; font-weight:700; }.downloads a:hover { border-color:var(--mint); color:#aaffea; } | |
| .output-video { width:100%; max-height:470px; background:#03050e; border-radius:15px; }.study-grid { display:grid; grid-template-columns:1.15fr .85fr; gap:14px; margin-top:17px; }.study-card { border:1px solid var(--line); border-radius:15px; padding:17px; background:rgba(8,12,31,.66); }.study-card h3 { font-size:1rem; margin:0 0 12px; }.chapter { display:grid; grid-template-columns:43px 1fr; gap:10px; padding:10px 0; border-top:1px solid var(--line); }.chapter:first-of-type { border-top:0; }.time { color:#80f0d7; font-size:.76rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; padding-top:2px; }.chapter b { display:block; font-size:.9rem; }.chapter p { margin:4px 0 0; color:var(--muted); font-size:.82rem; line-height:1.45; }.terms { display:flex; flex-wrap:wrap; gap:7px; }.term { padding:6px 8px; color:#d9d0ff; background:rgba(141,110,241,.16); border-radius:999px; font-size:.78rem; }.card { border-top:1px solid var(--line); padding:11px 0; }.card:first-of-type { border-top:0; }.card summary { cursor:pointer; color:#e7ebff; font-size:.88rem; font-weight:700; }.card p { color:var(--muted); line-height:1.45; font-size:.84rem; margin:8px 0 0; }.transcript { margin-top:14px; }.transcript summary { cursor:pointer; color:#dce2ff; font-weight:700; }.transcript p { white-space:pre-wrap; max-height:230px; overflow:auto; color:var(--muted); font-size:.86rem; line-height:1.6; } | |
| footer { color:#7f89ad; font-size:.8rem; text-align:center; padding-top:36px; }.hidden { display:none; } | |
| @media (max-width:760px) { .page { width:min(100% - 22px, 1160px); }.hero { padding:51px 4px 30px; }.grid,.study-grid { grid-template-columns:1fr; }.settings { grid-template-columns:1fr; }.panel { border-radius:18px; }.tag { display:none; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page"> | |
| <nav><a class="brand" href="#top"><span class="mark">▶</span> CapVideo</a><span class="tag">Built for focused learning</span></nav> | |
| <header class="hero" id="top"><span class="eyebrow">✦ From watching to remembering</span><h1>Give every lesson a <span>second life.</span></h1><p class="lead">CapVideo transforms an educational video into accessible captions, a timestamped transcript, chapter highlights, and recall cards—so learners can return to the ideas that matter.</p></header> | |
| <main class="grid"> | |
| <section class="panel" aria-labelledby="create-title"> | |
| <h2 id="create-title">Create a learning kit</h2><p class="small">Upload a video you have permission to use. CapVideo processes it privately in this session and prepares study-ready outputs.</p> | |
| <form id="create-form"> | |
| <div class="source-switch" role="tablist" aria-label="Choose video source"> | |
| <button type="button" role="tab" id="upload-tab" aria-selected="true" aria-controls="upload-source">Upload a file</button> | |
| <button type="button" role="tab" id="link-tab" aria-selected="false" aria-controls="link-source">YouTube link (beta)</button> | |
| </div> | |
| <div id="upload-source" class="source active" role="tabpanel"> | |
| <label class="drop" for="video-file"><span aria-hidden="true">↑</span><strong id="file-name">Choose a lecture or explainer video</strong><span class="small">MP4, MOV, MKV, WebM and more</span><input id="video-file" name="video" type="file" accept="video/*"></label> | |
| </div> | |
| <div id="link-source" class="source" role="tabpanel"> | |
| <label class="field" for="youtube-url">Public YouTube URL</label><input id="youtube-url" type="url" inputmode="url" placeholder="https://www.youtube.com/watch?v=..."> | |
| <div class="notice"><span>⚠</span><span>Some YouTube videos block automated access. For a dependable demo, use a short video file you own or have permission to upload.</span></div> | |
| </div> | |
| <div class="settings"> | |
| <label>Caption color<select id="caption-color"><option value="white">White</option><option value="yellow">Yellow</option><option value="cyan">Cyan</option></select></label> | |
| <label>Caption size<select id="caption-size"><option value="20">Standard</option><option value="24">Large</option><option value="28">Extra large</option></select></label> | |
| <label>Spoken language<select id="caption-language"><option value="">Auto-detect</option><option value="en">English</option><option value="hi">Hindi</option><option value="te">Telugu</option></select></label> | |
| </div> | |
| <button class="button" id="submit-button" type="submit">Build my learning kit <span aria-hidden="true">→</span></button> | |
| </form> | |
| <div id="status" aria-live="polite"><div class="status-top"><b id="status-title">Preparing your video</b><span id="status-percent">5%</span></div><div class="progress"><div id="progress-bar"></div></div><p class="small" id="status-detail">Starting securely…</p></div> | |
| <div id="error" role="alert"></div> | |
| </section> | |
| <aside class="panel"><h2>Made for learning, not just captions.</h2><div class="features"><div class="feature"><span class="feature-icon">◷</span><div><b>Find the moment</b><span class="small">Timestamped chapters make a long lecture easy to revisit.</span></div></div><div class="feature"><span class="feature-icon">⌁</span><div><b>Learn accessibly</b><span class="small">Download a captioned video, SRT captions, or readable transcript.</span></div></div><div class="feature"><span class="feature-icon">✦</span><div><b>Recall, don’t rewatch</b><span class="small">Use source-linked prompts to test what you understood.</span></div></div></div></aside> | |
| </main> | |
| <section id="results" class="panel" aria-live="polite"> | |
| <div class="result-head"><div><p class="eyebrow">✦ Your learning kit is ready</p><h2 id="result-title">Ready to study</h2></div><div class="downloads"><a id="video-download" href="#" download>Captioned video</a><a id="srt-download" href="#" download>Captions (.srt)</a><a id="transcript-download" href="#" download>Transcript (.txt)</a></div></div> | |
| <video id="output-video" class="output-video" controls preload="metadata"></video> | |
| <div class="study-grid"><div class="study-card"><h3>Lecture map</h3><div id="chapters"></div></div><div class="study-card"><h3>Key terms</h3><div id="terms" class="terms"></div><h3 style="margin-top:20px">Recall cards</h3><div id="recall-cards"></div></div></div> | |
| <details class="transcript"><summary>Read transcript</summary><p id="transcript"></p></details> | |
| </section> | |
| <footer>CapVideo · Upload only media you own or are authorized to process.</footer> | |
| </div> | |
| <script> | |
| const form = document.getElementById('create-form'); | |
| const fileInput = document.getElementById('video-file'); | |
| const fileName = document.getElementById('file-name'); | |
| const uploadTab = document.getElementById('upload-tab'); const linkTab = document.getElementById('link-tab'); | |
| const uploadSource = document.getElementById('upload-source'); const linkSource = document.getElementById('link-source'); | |
| const statusBox = document.getElementById('status'); const statusTitle = document.getElementById('status-title'); const statusPercent = document.getElementById('status-percent'); const statusDetail = document.getElementById('status-detail'); const progressBar = document.getElementById('progress-bar'); | |
| const errorBox = document.getElementById('error'); const submitButton = document.getElementById('submit-button'); const results = document.getElementById('results'); | |
| let sourceType = 'upload'; let pollTimer; | |
| const messages = { | |
| uploaded: ['Video uploaded', 'Preparing the audio track', 12], downloading: ['Fetching video', 'Getting the public video source', 18], transcribing: ['Listening closely', 'Creating a timestamped transcript', 42], generating_captions: ['Writing captions', 'Synchronizing every spoken segment', 65], building_learning_kit: ['Building your learning kit', 'Finding chapters and recall prompts', 82], embedding_subtitles: ['Finishing video', 'Embedding your chosen caption style', 92], completed: ['Learning kit ready', 'Your study outputs are ready to explore', 100] | |
| }; | |
| function escapeHtml(value) { return String(value || '').replace(/[&<>'"]/g, char => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char])); } | |
| function selectSource(next) { sourceType = next; const isUpload = next === 'upload'; uploadTab.setAttribute('aria-selected', isUpload); linkTab.setAttribute('aria-selected', !isUpload); uploadSource.classList.toggle('active', isUpload); linkSource.classList.toggle('active', !isUpload); } | |
| uploadTab.addEventListener('click', () => selectSource('upload')); linkTab.addEventListener('click', () => selectSource('link')); | |
| fileInput.addEventListener('change', () => { fileName.textContent = fileInput.files[0] ? fileInput.files[0].name : 'Choose a lecture or explainer video'; }); | |
| function showError(message) { errorBox.textContent = message; errorBox.style.display = 'block'; } | |
| function setStatus(status) { const [title, detail, percent] = messages[status] || ['Working on it', 'This may take a few minutes for longer videos.', 30]; statusBox.style.display = 'block'; statusTitle.textContent = title; statusDetail.textContent = detail; statusPercent.textContent = `${percent}%`; progressBar.style.width = `${percent}%`; } | |
| function captionSettings() { return { color: document.getElementById('caption-color').value, size: document.getElementById('caption-size').value, language: document.getElementById('caption-language').value, position: 'bottom', alignment: 'center', outlineThickness: 'medium', shadowDistance: 'medium', font: 'arial', fontStyle: 'normal', bgColor: 'none' }; } | |
| async function createJob() { | |
| if (sourceType === 'upload') { const file = fileInput.files[0]; if (!file) throw new Error('Choose a video file first.'); const data = new FormData(); data.append('video', file); data.append('captionSettings', JSON.stringify(captionSettings())); return fetch('/upload', {method:'POST', body:data}); } | |
| const url = document.getElementById('youtube-url').value.trim(); if (!url) throw new Error('Paste a public YouTube URL first.'); return fetch('/transcribe', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({url, captionSettings:captionSettings()})}); | |
| } | |
| function renderResults(job) { clearInterval(pollTimer); setStatus('completed'); document.getElementById('result-title').textContent = job.filename || 'Ready to study'; const output = document.getElementById('output-video'); output.src = job.download_url; document.getElementById('video-download').href = job.download_url; document.getElementById('srt-download').href = job.captions_url || '#'; document.getElementById('transcript-download').href = job.transcript_url || '#'; document.getElementById('transcript').textContent = job.transcription || 'No transcript was returned.'; const kit = job.learning_kit || {}; document.getElementById('chapters').innerHTML = (kit.chapters || []).map(chapter => `<div class="chapter"><span class="time">${escapeHtml(chapter.timestamp)}</span><div><b>${escapeHtml(chapter.title)}</b><p>${escapeHtml(chapter.summary)}</p></div></div>`).join('') || '<p class="small">Chapter markers were not available for this video.</p>'; document.getElementById('terms').innerHTML = (kit.key_terms || []).map(term => `<span class="term">${escapeHtml(term)}</span>`).join('') || '<span class="small">No key terms available.</span>'; document.getElementById('recall-cards').innerHTML = (kit.recall_cards || []).map(card => `<details class="card"><summary>${escapeHtml(card.question)} <span class="time">${escapeHtml(card.timestamp)}</span></summary><p>${escapeHtml(card.answer)}</p></details>`).join('') || '<p class="small">Recall prompts were not available for this video.</p>'; results.style.display = 'block'; results.scrollIntoView({behavior:'smooth', block:'start'}); } | |
| async function poll(jobId) { try { const response = await fetch(`/status/${encodeURIComponent(jobId)}`); const job = await response.json(); if (!response.ok) throw new Error(job.error || 'Unable to check processing status.'); if (job.status === 'failed') throw new Error(job.error || 'The video could not be processed. Try a short MP4 upload.'); setStatus(job.status); if (job.status === 'completed') return renderResults(job); pollTimer = setTimeout(() => poll(jobId), 1600); } catch (error) { submitButton.disabled = false; submitButton.textContent = 'Build my learning kit →'; showError(error.message); } } | |
| form.addEventListener('submit', async event => { event.preventDefault(); clearTimeout(pollTimer); errorBox.style.display = 'none'; results.style.display = 'none'; submitButton.disabled = true; submitButton.textContent = 'Starting…'; setStatus(sourceType === 'upload' ? 'uploaded' : 'downloading'); try { const response = await createJob(); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || 'Unable to start processing.'); submitButton.textContent = 'Processing…'; poll(payload.job_id); } catch (error) { submitButton.disabled = false; submitButton.textContent = 'Build my learning kit →'; showError(error.message); } }); | |
| </script> | |
| </body> | |
| </html> | |