'use strict'; const $ = selector => document.querySelector(selector); const form = $('#transcript-form'); const input = $('#reel-url'); let result = ''; let active = false; let resultSource = ''; function normalizeLink(value) { let url; try { url = new URL(value.trim()); } catch { return null; } if (url.protocol !== 'https:' || !['instagram.com', 'www.instagram.com', 'm.instagram.com'].includes(url.hostname.toLowerCase()) || url.port || url.username || url.password) return null; const match = url.pathname.match(/^\/(?:reel|reels|p|tv)\/([A-Za-z0-9_-]+)\/?$/); const kind = url.pathname.startsWith('/p/') ? 'p' : url.pathname.startsWith('/tv/') ? 'tv' : 'reel'; return match ? `https://www.instagram.com/${kind}/${match[1]}/` : null; } function showError(message) { $('#input-error').textContent = message; $('#input-error').hidden = !message; $('.url-field').classList.toggle('invalid', Boolean(message)); input.setAttribute('aria-invalid', message ? 'true' : 'false'); } function status(message, state = '') { $('#status').textContent = message; $('#status').className = `status ${state}`; } function clearResult() { result = ''; resultSource = ''; $('#transcript').value = ''; $('#transcript').hidden = true; $('#empty-state').hidden = false; $('#copy').disabled = true; $('#download').disabled = true; $('#word-count').textContent = 'Nothing to copy yet'; $('#result-meta').textContent = 'YOUR TRANSCRIPT'; } function setBusy(busy) { active = busy; input.disabled = busy; $('#generate').disabled = busy; $('#clear-url').disabled = busy; $('#generate-label').textContent = busy ? 'Working on your Reel…' : 'Get transcript'; $('.spinner').hidden = !busy; $('#button-arrow').hidden = busy; $('#result-panel').setAttribute('aria-busy', String(busy)); } input.addEventListener('input', () => { showError(''); $('#clear-url').hidden = !input.value; if (resultSource && normalizeLink(input.value) !== resultSource) { clearResult(); status('Ready for another link.'); } }); input.addEventListener('blur', () => { if (input.value.trim() && !normalizeLink(input.value)) showError('Use a full public Instagram Reel, video-post or IGTV link.'); }); $('#clear-url').addEventListener('click', () => { input.value = ''; $('#clear-url').hidden = true; showError(''); clearResult(); status('Ready when you are.'); input.focus(); }); form.addEventListener('submit', async event => { event.preventDefault(); if (active) return; const url = normalizeLink(input.value); if (!url) { showError('Paste a full Instagram Reel, video-post or IGTV link to continue.'); input.focus(); return; } showError(''); clearResult(); setBusy(true); const requestStarted = performance.now(); form.dataset.requestStartedUtc = new Date().toISOString(); delete form.dataset.elapsedMilliseconds; delete form.dataset.completedUtc; status('Checking the public video and transcribing in the cloud. Keep this tab open.', 'working'); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 600000); try { const response = await fetch('/api/transcribe', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({url}), signal: controller.signal }); const data = await response.json(); if (!response.ok) throw new Error(data.error || 'The request could not be completed.'); if (!data.text || !data.text.trim()) throw new Error('No spoken text was detected. No transcript was created.'); result = data.text.trim(); resultSource = url; $('#transcript').value = result; $('#transcript').hidden = false; $('#empty-state').hidden = true; $('#copy').disabled = false; $('#download').disabled = false; if (Number.isInteger(data.word_count_whitespace)) { $('#word-count').textContent = `${data.word_count_whitespace} ${data.word_count_whitespace === 1 ? 'word' : 'words'}`; } else if (typeof Intl.Segmenter === 'function') { const count = [...new Intl.Segmenter(undefined, {granularity:'word'}).segment(result)].filter(part => part.isWordLike).length; $('#word-count').textContent = `${count} ${count === 1 ? 'word' : 'words'}`; } else $('#word-count').textContent = 'Ready to copy'; $('#result-meta').textContent = data.language_name || data.language || 'TRANSCRIPT'; status('Your transcript is ready. Copy it or download your text file.'); } catch (error) { status(error.name === 'AbortError' ? 'This request timed out. No transcript was created.' : error.message === 'Failed to fetch' ? 'The cloud service could not be reached. No transcript was created.' : error.message, 'failed'); } finally { clearTimeout(timer); setBusy(false); form.dataset.elapsedMilliseconds = String(performance.now() - requestStarted); form.dataset.completedUtc = new Date().toISOString(); } }); $('#copy').addEventListener('click', async () => { if (!result) return; try { await navigator.clipboard.writeText(result); $('#copy').innerHTML = ' Copied'; status('Copied to your clipboard.'); setTimeout(() => { $('#copy').innerHTML = ' Copy text'; }, 1800); } catch { $('#transcript').focus(); $('#transcript').select(); status('Clipboard access was blocked. The text is selected; use your device’s Copy command.'); } }); $('#download').addEventListener('click', () => { if (!result) return; const blob = new Blob([result + '\n'], {type:'text/plain;charset=utf-8'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'instagram-transcript.txt'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 2000); status('Text file prepared for download. Check your browser’s downloads.'); }); fetch('/api/status').then(r => r.json()).then(data => { if (!data.ready) status(data.message || 'Cloud processing is temporarily unavailable. No transcript was created.', 'failed'); }).catch(() => status('Cloud processing could not be reached. No transcript was created.', 'failed'));