ytDownloader / Youtube-tool /src /pages /transcript.astro
Anshishere's picture
Upload 16532 files
494fa67 verified
Raw
History Blame Contribute Delete
14.1 kB
---
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>