YouTube Video Summarizer

Generate AI-powered summaries of YouTube video transcripts

Size

15.5 KB

Version

1.1.2

Created

Oct 23, 2025

Updated

about 2 months ago

1// ==UserScript==
2// @name		YouTube Video Summarizer
3// @description		Generate AI-powered summaries of YouTube video transcripts
4// @version		1.1.2
5// @match		https://*.youtube.com/*
6// @icon		https://www.youtube.com/s/desktop/83278897/img/favicon_32x32.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// ==/UserScript==
10(function() {
11    'use strict';
12
13    console.log('YouTube Video Summarizer: Extension loaded');
14
15    // Debounce function to prevent multiple rapid calls
16    function debounce(func, wait) {
17        let timeout;
18        return function executedFunction(...args) {
19            const later = () => {
20                clearTimeout(timeout);
21                func(...args);
22            };
23            clearTimeout(timeout);
24            timeout = setTimeout(later, wait);
25        };
26    }
27
28    // Get video ID from URL
29    function getVideoId() {
30        const urlParams = new URLSearchParams(window.location.search);
31        return urlParams.get('v');
32    }
33
34    // Check if summary exists in cache
35    async function getCachedSummary(videoId) {
36        try {
37            const cacheKey = `yt_summary_${videoId}`;
38            const cachedData = await GM.getValue(cacheKey);
39            if (cachedData) {
40                console.log('Found cached summary for video:', videoId);
41                return JSON.parse(cachedData);
42            }
43            return null;
44        } catch (error) {
45            console.error('Error reading cached summary:', error);
46            return null;
47        }
48    }
49
50    // Save summary to cache
51    async function cacheSummary(videoId, summary) {
52        try {
53            const cacheKey = `yt_summary_${videoId}`;
54            const cacheData = {
55                summary: summary,
56                timestamp: Date.now(),
57                videoId: videoId
58            };
59            await GM.setValue(cacheKey, JSON.stringify(cacheData));
60            console.log('Summary cached for video:', videoId);
61        } catch (error) {
62            console.error('Error caching summary:', error);
63        }
64    }
65
66    // Wait for element to appear in DOM
67    function waitForElement(selector, timeout = 10000) {
68        return new Promise((resolve, reject) => {
69            const startTime = Date.now();
70            
71            const checkElement = () => {
72                const element = document.querySelector(selector);
73                if (element) {
74                    resolve(element);
75                } else if (Date.now() - startTime > timeout) {
76                    reject(new Error(`Element ${selector} not found within ${timeout}ms`));
77                } else {
78                    setTimeout(checkElement, 100);
79                }
80            };
81            
82            checkElement();
83        });
84    }
85
86    // Extract transcript from the page
87    async function extractTranscript() {
88        console.log('Extracting transcript...');
89        
90        // Wait for transcript segments to load
91        await new Promise(resolve => setTimeout(resolve, 2000));
92        
93        // Find all transcript segments - updated selector
94        const transcriptSegments = document.querySelectorAll('ytd-transcript-segment-renderer');
95        
96        if (transcriptSegments.length === 0) {
97            console.error('No transcript segments found');
98            return null;
99        }
100        
101        console.log(`Found ${transcriptSegments.length} transcript segments`);
102        
103        // Extract text from each segment
104        let transcriptText = '';
105        transcriptSegments.forEach(segment => {
106            const textElement = segment.querySelector('yt-formatted-string.segment-text');
107            if (textElement) {
108                transcriptText += textElement.textContent.trim() + ' ';
109            }
110        });
111        
112        console.log(`Extracted transcript length: ${transcriptText.length} characters`);
113        return transcriptText.trim();
114    }
115
116    // Generate summary using AI
117    async function generateSummary(transcript) {
118        console.log('Generating AI summary...');
119        
120        try {
121            const prompt = `Please provide a comprehensive summary of this YouTube video transcript. Include:
1221. Main topic and key points
1232. Important insights or conclusions
1243. Any notable examples or demonstrations mentioned
125
126Transcript:
127${transcript}`;
128
129            const summary = await RM.aiCall(prompt);
130            console.log('Summary generated successfully');
131            return summary;
132        } catch (error) {
133            console.error('Error generating summary:', error);
134            throw error;
135        }
136    }
137
138    // Display summary below the description
139    function displaySummaryBelowDescription(summary) {
140        console.log('Displaying summary below description...');
141        
142        // Remove existing summary if present
143        const existingSummary = document.getElementById('yt-custom-summary-container');
144        if (existingSummary) {
145            existingSummary.remove();
146        }
147
148        // Find the description container
149        const descriptionContainer = document.querySelector('#description.item.style-scope.ytd-watch-metadata');
150        
151        if (!descriptionContainer) {
152            console.error('Description container not found');
153            return;
154        }
155
156        // Create summary container
157        const summaryContainer = document.createElement('div');
158        summaryContainer.id = 'yt-custom-summary-container';
159        summaryContainer.style.cssText = `
160            background: #0f0f0f;
161            border: 1px solid #3f3f3f;
162            border-radius: 12px;
163            padding: 16px;
164            margin-top: 12px;
165            font-family: "Roboto", "Arial", sans-serif;
166        `;
167
168        // Create title with icon
169        const titleContainer = document.createElement('div');
170        titleContainer.style.cssText = `
171            display: flex;
172            align-items: center;
173            margin-bottom: 12px;
174        `;
175
176        const icon = document.createElement('span');
177        icon.textContent = '✨';
178        icon.style.cssText = `
179            font-size: 20px;
180            margin-right: 8px;
181        `;
182
183        const title = document.createElement('h3');
184        title.textContent = 'AI-Generated Summary';
185        title.style.cssText = `
186            margin: 0;
187            font-size: 16px;
188            font-weight: 500;
189            color: #f1f1f1;
190        `;
191
192        titleContainer.appendChild(icon);
193        titleContainer.appendChild(title);
194
195        // Create summary content
196        const content = document.createElement('div');
197        content.textContent = summary;
198        content.style.cssText = `
199            line-height: 1.6;
200            font-size: 14px;
201            white-space: pre-wrap;
202            color: #aaaaaa;
203        `;
204
205        summaryContainer.appendChild(titleContainer);
206        summaryContainer.appendChild(content);
207
208        // Insert after description
209        descriptionContainer.parentNode.insertBefore(summaryContainer, descriptionContainer.nextSibling);
210        
211        console.log('Summary displayed successfully');
212    }
213
214    // Remove Gemini summary if it exists
215    function removeGeminiSummary() {
216        console.log('Checking for Gemini summary...');
217        const geminiSummary = document.querySelector('ytd-expandable-metadata-renderer[has-video-summary]');
218        
219        if (geminiSummary) {
220            console.log('Removing Gemini summary...');
221            geminiSummary.remove();
222        } else {
223            console.log('No Gemini summary found');
224        }
225    }
226
227    // Show loading indicator on button
228    function showLoading(button) {
229        button.disabled = true;
230        button.textContent = 'Generating Summary...';
231        button.style.opacity = '0.6';
232    }
233
234    // Hide loading indicator on button
235    function hideLoading(button) {
236        button.disabled = false;
237        button.textContent = 'Generate Summary';
238        button.style.opacity = '1';
239    }
240
241    // Main function to generate summary
242    async function handleGenerateSummary(button) {
243        try {
244            showLoading(button);
245
246            // Get video ID
247            const videoId = getVideoId();
248            if (!videoId) {
249                throw new Error('Could not get video ID');
250            }
251
252            // Step 1: Remove Gemini summary if it exists
253            removeGeminiSummary();
254
255            // Step 2: Expand description if needed
256            console.log('Step 2: Expanding description...');
257            const expandButton = document.querySelector('tp-yt-paper-button#expand[class*="ytd-text-inline-expander"]');
258            if (expandButton && expandButton.offsetParent !== null) {
259                expandButton.click();
260                await new Promise(resolve => setTimeout(resolve, 500));
261            }
262
263            // Step 3: Click "Show transcript" button
264            console.log('Step 3: Opening transcript...');
265            const transcriptButton = document.querySelector('button[aria-label="Show transcript"]');
266            
267            if (!transcriptButton) {
268                throw new Error('Transcript button not found. This video may not have a transcript available.');
269            }
270
271            transcriptButton.click();
272            
273            // Step 4: Wait for transcript to load and extract it
274            console.log('Step 4: Waiting for transcript to load...');
275            await waitForElement('ytd-transcript-segment-renderer', 15000);
276            
277            const transcript = await extractTranscript();
278            
279            if (!transcript || transcript.length < 50) {
280                throw new Error('Could not extract transcript. The transcript may be too short or unavailable.');
281            }
282
283            // Step 5: Generate summary with AI
284            console.log('Step 5: Generating AI summary...');
285            const summary = await generateSummary(transcript);
286
287            // Step 6: Cache the summary
288            console.log('Step 6: Caching summary...');
289            await cacheSummary(videoId, summary);
290
291            // Step 7: Display summary below description
292            console.log('Step 7: Displaying summary...');
293            displaySummaryBelowDescription(summary);
294
295            // Hide the button after successful generation
296            button.style.display = 'none';
297
298        } catch (error) {
299            console.error('Error generating summary:', error);
300            alert(`Error: ${error.message}`);
301        } finally {
302            hideLoading(button);
303        }
304    }
305
306    // Check and display cached summary if available
307    async function checkAndDisplayCachedSummary() {
308        const videoId = getVideoId();
309        if (!videoId) {
310            console.log('No video ID found');
311            return false;
312        }
313
314        const cachedData = await getCachedSummary(videoId);
315        if (cachedData && cachedData.summary) {
316            console.log('Displaying cached summary');
317            displaySummaryBelowDescription(cachedData.summary);
318            
319            // Hide the generate button since summary is already displayed
320            const button = document.getElementById('yt-generate-summary-btn');
321            if (button) {
322                button.style.display = 'none';
323            }
324            return true;
325        }
326        return false;
327    }
328
329    // Create and add the "Generate Summary" button
330    function addSummaryButton() {
331        // Check if button already exists
332        if (document.getElementById('yt-generate-summary-btn')) {
333            return;
334        }
335
336        // Find the description container
337        const descriptionContainer = document.querySelector('#description.item.style-scope.ytd-watch-metadata');
338        
339        if (!descriptionContainer) {
340            console.log('Description container not found, will retry...');
341            return;
342        }
343
344        console.log('Adding Generate Summary button...');
345
346        // Create button container
347        const buttonContainer = document.createElement('div');
348        buttonContainer.id = 'yt-summary-button-container';
349        buttonContainer.style.cssText = `
350            margin-top: 12px;
351            display: flex;
352            align-items: center;
353        `;
354
355        // Create the button
356        const summaryButton = document.createElement('button');
357        summaryButton.id = 'yt-generate-summary-btn';
358        summaryButton.textContent = 'Generate Summary';
359        summaryButton.style.cssText = `
360            background: #065fd4;
361            color: #ffffff;
362            border: none;
363            border-radius: 18px;
364            padding: 0 16px;
365            height: 36px;
366            font-size: 14px;
367            font-weight: 500;
368            font-family: "Roboto", "Arial", sans-serif;
369            cursor: pointer;
370            display: inline-flex;
371            align-items: center;
372            justify-content: center;
373            transition: background 0.2s;
374            white-space: nowrap;
375        `;
376
377        // Add hover effect
378        summaryButton.onmouseenter = () => {
379            if (!summaryButton.disabled) {
380                summaryButton.style.background = '#0c7ce5';
381            }
382        };
383        summaryButton.onmouseleave = () => {
384            if (!summaryButton.disabled) {
385                summaryButton.style.background = '#065fd4';
386            }
387        };
388
389        // Add click handler
390        summaryButton.onclick = () => handleGenerateSummary(summaryButton);
391
392        buttonContainer.appendChild(summaryButton);
393        
394        // Insert after description
395        descriptionContainer.parentNode.insertBefore(buttonContainer, descriptionContainer.nextSibling);
396        
397        console.log('Generate Summary button added successfully');
398    }
399
400    // Initialize the extension
401    async function init() {
402        console.log('Initializing YouTube Video Summarizer...');
403        
404        // Remove Gemini summary on page load
405        removeGeminiSummary();
406        
407        // Check for cached summary and display it if available
408        setTimeout(async () => {
409            const hasCachedSummary = await checkAndDisplayCachedSummary();
410            if (!hasCachedSummary) {
411                // Only add button if no cached summary exists
412                addSummaryButton();
413            }
414        }, 1000);
415
416        // Watch for navigation changes (YouTube is a SPA)
417        const debouncedInit = debounce(async () => {
418            removeGeminiSummary();
419            const hasCachedSummary = await checkAndDisplayCachedSummary();
420            if (!hasCachedSummary) {
421                addSummaryButton();
422            }
423        }, 500);
424        
425        // Observe URL changes
426        let lastUrl = location.href;
427        new MutationObserver(debounce(() => {
428            const url = location.href;
429            if (url !== lastUrl) {
430                lastUrl = url;
431                console.log('URL changed, re-initializing...');
432                setTimeout(debouncedInit, 1000);
433            }
434        }, 100)).observe(document.body, { subtree: true, childList: true });
435
436        // Also observe the description section specifically
437        const observer = new MutationObserver(debounce(async () => {
438            removeGeminiSummary();
439            const hasCachedSummary = await checkAndDisplayCachedSummary();
440            if (!hasCachedSummary) {
441                addSummaryButton();
442            }
443        }, 500));
444        
445        const observeDescription = () => {
446            const watchMetadata = document.querySelector('ytd-watch-metadata');
447            if (watchMetadata) {
448                observer.observe(watchMetadata, { childList: true, subtree: true });
449            }
450        };
451        
452        setTimeout(observeDescription, 2000);
453    }
454
455    // Start the extension when DOM is ready
456    if (document.readyState === 'loading') {
457        document.addEventListener('DOMContentLoaded', init);
458    } else {
459        init();
460    }
461
462})();
YouTube Video Summarizer | Robomonkey