Spaces:
No application file
No application file
File size: 14,122 Bytes
494fa67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | ---
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>
|