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>