Spaces:
No application file
No application file
| --- | |
| import Layout from '../layouts/Layout.astro'; | |
| const seoTitle = "YouTube Transcript Generator - Free AI Transcription"; | |
| const seoDescription = "Free YouTube transcript generator tool. Extract text from any YouTube video instantly using AI. Perfect for repurposing content and accessibility."; | |
| const seoKeywords = "youtube transcript generator, youtube transcript generator free, free youtube transcript generator, youtube transcript generator tool, youtube transcript generator ai"; | |
| --- | |
| <Layout title={seoTitle} description={seoDescription} keywords={seoKeywords}> | |
| <div class="relative pt-12 pb-24 px-4 sm:px-8"> | |
| <div class="max-w-5xl mx-auto"> | |
| <!-- Header Section --> | |
| <div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-12 animate-slide-up"> | |
| <div class="space-y-2"> | |
| <div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 stagger-1"> | |
| <span class="relative flex h-2 w-2"> | |
| <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span> | |
| <span class="relative inline-flex rounded-full h-2 w-2 bg-primary"></span> | |
| </span> | |
| <span class="text-[10px] font-bold text-primary uppercase tracking-widest text-white">Neural Text Extraction</span> | |
| </div> | |
| <h1 class="text-4xl sm:text-6xl font-black uppercase tracking-tighter text-white stagger-2">Transcript <span class="text-primary">Generator</span></h1> | |
| <p class="text-body text-base sm:text-lg max-w-xl leading-relaxed stagger-3"> | |
| Extract high-fidelity transcripts from any YouTube video or Shorts. Support for multiple languages and professional formats. | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Search Box --> | |
| <div class="bg-canvas-soft border border-hairline p-1 rounded-2xl shadow-2xl mb-12 animate-slide-up stagger-4 search-box-glow"> | |
| <div class="flex flex-col sm:flex-row gap-1"> | |
| <input | |
| type="text" | |
| id="url-input" | |
| placeholder="Paste YouTube Video or Shorts URL..." | |
| class="flex-grow h-14 bg-transparent px-6 text-lg focus:outline-none text-white placeholder:text-mute font-medium" | |
| /> | |
| <button id="analyze-btn" class="btn-primary !h-14 !px-10 !rounded-xl text-sm font-bold uppercase tracking-[0.1em]">Analyze Captions</button> | |
| </div> | |
| </div> | |
| <!-- Loading State --> | |
| <div id="loading" class="hidden py-24 flex flex-col items-center justify-center space-y-6"> | |
| <div class="relative"> | |
| <div class="w-16 h-16 border-4 border-hairline rounded-full"></div> | |
| <div class="w-16 h-16 border-4 border-t-primary rounded-full animate-spin absolute top-0 left-0"></div> | |
| </div> | |
| <p class="text-xs font-bold text-primary uppercase tracking-[0.3em] animate-pulse">Extracting Timed Metadata</p> | |
| </div> | |
| <!-- Results Grid --> | |
| <div class="hidden grid-cols-1 lg:grid-cols-12 gap-8 items-start animate-scale-in" id="results"> | |
| <!-- Left Column: Video Info & Settings --> | |
| <div class="lg:col-span-4 space-y-6"> | |
| <div class="bg-canvas-soft-2/50 p-6 rounded-2xl border border-hairline/30 space-y-6"> | |
| <div class="aspect-video bg-black rounded-xl overflow-hidden border border-hairline shadow-lg"> | |
| <img id="video-thumb" src="" alt="" class="w-full h-full object-cover opacity-90" /> | |
| </div> | |
| <div> | |
| <h3 id="video-title" class="font-bold text-lg text-white leading-tight uppercase tracking-tighter mb-2">Video Title</h3> | |
| <p id="video-uploader" class="text-primary font-bold text-[10px] uppercase tracking-widest">---</p> | |
| </div> | |
| <div class="space-y-4 pt-6 border-t border-hairline/30"> | |
| <div class="space-y-2"> | |
| <label class="text-[10px] font-black text-mute uppercase tracking-widest">Select Language</label> | |
| <select id="lang-select" class="w-full h-11 bg-black/40 border border-hairline rounded-xl px-4 text-xs font-bold text-white focus:outline-none focus:border-primary/50 transition-colors"> | |
| <!-- Languages populated by JS --> | |
| </select> | |
| </div> | |
| <div class="space-y-2"> | |
| <label class="text-[10px] font-black text-mute uppercase tracking-widest">Select Format</label> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button class="format-btn active h-10 rounded-xl border border-hairline bg-black/20 text-[10px] font-black uppercase tracking-widest text-mute transition-all" data-format="plain">Plain</button> | |
| <button class="format-btn h-10 rounded-xl border border-hairline bg-black/20 text-[10px] font-black uppercase tracking-widest text-mute transition-all" data-format="srt">SRT</button> | |
| <button class="format-btn h-10 rounded-xl border border-hairline bg-black/20 text-[10px] font-black uppercase tracking-widest text-mute transition-all" data-format="vtt">VTT</button> | |
| </div> | |
| </div> | |
| <button id="generate-btn" class="btn-primary w-full !h-12 !text-xs !font-black !uppercase !tracking-widest mt-4"> | |
| Fetch Transcript | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Column: Transcript Content --> | |
| <div class="lg:col-span-8 space-y-6"> | |
| <div class="bg-canvas-soft-2 p-8 rounded-3xl border border-hairline/50 relative flex flex-col min-h-[500px]"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h4 class="text-xs font-black text-primary uppercase tracking-[0.2em]">Live Transcript Stream</h4> | |
| <button id="copy-btn" class="flex items-center gap-2 px-4 py-2 rounded-xl bg-primary/10 border border-primary/20 text-[10px] font-black text-primary uppercase tracking-widest hover:bg-primary/20 transition-all active:scale-95 disabled:opacity-50" disabled> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> | |
| Copy to Clipboard | |
| </button> | |
| </div> | |
| <div id="transcript-container" class="flex-grow bg-black/40 rounded-2xl border border-hairline/50 p-6 overflow-y-auto max-h-[600px] scrollbar-hide"> | |
| <p id="transcript-placeholder" class="text-mute text-xs font-medium italic text-center py-24">Select parameters and click "Fetch Transcript" to begin extraction.</p> | |
| <pre id="transcript-content" class="hidden text-white text-xs font-medium leading-relaxed whitespace-pre-wrap font-roboto"></pre> | |
| </div> | |
| <div id="fetch-loading" class="hidden absolute inset-0 bg-canvas-soft-2/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center space-y-4"> | |
| <div class="w-12 h-12 border-4 border-hairline border-t-primary rounded-full animate-spin"></div> | |
| <span class="text-[10px] font-black text-primary uppercase tracking-[0.2em]">Synchronizing Stream...</span> | |
| </div> | |
| <div class="mt-8 pt-8 border-t border-hairline flex items-start gap-4"> | |
| <div class="w-8 h-8 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center flex-shrink-0"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-primary"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> | |
| </div> | |
| <p class="text-[10px] text-mute leading-relaxed uppercase font-medium tracking-wider"> | |
| Powered by high-velocity extraction nodes. Transcripts are parsed in real-time and formatted for professional compatibility. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| @reference "../styles/global.css"; | |
| .format-btn.active { | |
| @apply border-primary bg-primary/10 text-primary; | |
| } | |
| </style> | |
| <script> | |
| const urlInput = document.getElementById('url-input') as HTMLInputElement; | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const loading = document.getElementById('loading'); | |
| const results = document.getElementById('results'); | |
| const langSelect = document.getElementById('lang-select') as HTMLSelectElement; | |
| const transcriptContent = document.getElementById('transcript-content'); | |
| const transcriptPlaceholder = document.getElementById('transcript-placeholder'); | |
| const fetchLoading = document.getElementById('fetch-loading'); | |
| const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement; | |
| let currentSubtitleData: any = null; | |
| let selectedFormat = 'plain'; | |
| // Handle format selection | |
| document.querySelectorAll('.format-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.format-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| selectedFormat = (btn as HTMLElement).dataset.format!; | |
| }); | |
| }); | |
| analyzeBtn?.addEventListener('click', async () => { | |
| const url = urlInput.value.trim(); | |
| if (!url) return; | |
| loading?.classList.remove('hidden'); | |
| results?.classList.add('hidden'); | |
| try { | |
| const response = await fetch('/api/transcript-info', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url }) | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| currentSubtitleData = data; | |
| // Update UI | |
| (document.getElementById('video-thumb') as HTMLImageElement).src = data.thumbnail; | |
| document.getElementById('video-title')!.textContent = data.title; | |
| document.getElementById('video-uploader')!.textContent = data.uploader; | |
| // Populate Languages | |
| langSelect.innerHTML = ''; | |
| const langs = Object.keys(data.languages); | |
| if (langs.length === 0) { | |
| langSelect.innerHTML = '<option value="">No captions available</option>'; | |
| generateBtn!.setAttribute('disabled', 'true'); | |
| } else { | |
| generateBtn!.removeAttribute('disabled'); | |
| langs.forEach(code => { | |
| const lang = data.languages[code]; | |
| const option = document.createElement('option'); | |
| option.value = code; | |
| option.textContent = `${lang.name} ${lang.isAuto ? '(Auto)' : ''}`; | |
| langSelect.appendChild(option); | |
| }); | |
| } | |
| results?.classList.remove('hidden'); | |
| results?.classList.add('grid'); | |
| } catch (error: any) { | |
| alert('Analysis failed: ' + error.message); | |
| } finally { | |
| loading?.classList.add('hidden'); | |
| } | |
| }); | |
| generateBtn?.addEventListener('click', async () => { | |
| const lang = langSelect.value; | |
| if (!lang || !currentSubtitleData) return; | |
| fetchLoading?.classList.remove('hidden'); | |
| transcriptContent?.classList.add('hidden'); | |
| transcriptPlaceholder?.classList.add('hidden'); | |
| copyBtn.disabled = true; | |
| try { | |
| // Find URL for the selected language and format | |
| let subUrl = ''; | |
| const subs = currentSubtitleData.rawSubtitles[lang] || currentSubtitleData.rawAutoCaptions[lang]; | |
| if (selectedFormat === 'plain') { | |
| // Prefer json3 for easy plain text conversion | |
| const json3 = subs.find((s: any) => s.ext === 'json3'); | |
| subUrl = json3 ? json3.url : subs[0].url; | |
| } else { | |
| // Look for specific extension | |
| const match = subs.find((s: any) => s.ext === selectedFormat); | |
| subUrl = match ? match.url : subs[0].url; | |
| } | |
| const response = await fetch('/api/transcript-fetch', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url: subUrl, format: selectedFormat }) | |
| }); | |
| const data = await response.json(); | |
| if (response.status === 429) { | |
| transcriptPlaceholder?.classList.remove('hidden'); | |
| transcriptPlaceholder!.textContent = data.error; | |
| transcriptPlaceholder!.classList.add('text-primary'); | |
| return; | |
| } | |
| if (data.error) throw new Error(data.error); | |
| transcriptContent!.textContent = data.content; | |
| transcriptContent?.classList.remove('hidden'); | |
| copyBtn.disabled = false; | |
| } catch (error: any) { | |
| alert('Fetch failed: ' + error.message); | |
| transcriptPlaceholder?.classList.remove('hidden'); | |
| } finally { | |
| fetchLoading?.classList.add('hidden'); | |
| } | |
| }); | |
| copyBtn?.addEventListener('click', () => { | |
| const text = transcriptContent?.textContent; | |
| if (text) { | |
| navigator.clipboard.writeText(text); | |
| const originalText = copyBtn.innerHTML; | |
| copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!'; | |
| setTimeout(() => { | |
| copyBtn.innerHTML = originalText; | |
| }, 2000); | |
| } | |
| }); | |
| </script> | |
| </Layout> | |