YouTube Video Summarizer

AI-powered video summarizer that extracts and summarizes YouTube video content

Size

16.6 KB

Version

1.0.1

Created

Mar 10, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		YouTube Video Summarizer
3// @description		AI-powered video summarizer that extracts and summarizes YouTube video content
4// @version		1.0.1
5// @match		https://*.m.youtube.com/*
6// ==/UserScript==
7(function() {
8    'use strict';
9
10    console.log('YouTube Video Summarizer extension loaded');
11
12    // Debounce function to prevent excessive calls
13    function debounce(func, wait) {
14        let timeout;
15        return function executedFunction(...args) {
16            const later = () => {
17                clearTimeout(timeout);
18                func(...args);
19            };
20            clearTimeout(timeout);
21            timeout = setTimeout(later, wait);
22        };
23    }
24
25    // Create and inject the summarize button
26    function createSummarizeButton() {
27        console.log('Creating summarize button');
28        
29        // Check if button already exists
30        if (document.querySelector('#ai-summarize-button')) {
31            console.log('Summarize button already exists');
32            return;
33        }
34
35        // Find the action bar where like, share, save buttons are
36        const actionBar = document.querySelector('ytm-slim-video-action-bar-renderer .slim-video-action-bar-actions');
37        
38        if (!actionBar) {
39            console.log('Action bar not found, will retry');
40            return;
41        }
42
43        // Create button container matching YouTube's style
44        const buttonContainer = document.createElement('button-view-model');
45        buttonContainer.className = 'ytSpecButtonViewModelHost';
46        
47        const button = document.createElement('button');
48        button.id = 'ai-summarize-button';
49        button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading';
50        button.setAttribute('aria-label', 'Summarize video with AI');
51        button.setAttribute('aria-disabled', 'false');
52        
53        // Create icon container
54        const iconDiv = document.createElement('div');
55        iconDiv.className = 'yt-spec-button-shape-next__icon';
56        iconDiv.setAttribute('aria-hidden', 'true');
57        iconDiv.innerHTML = `
58            <svg viewBox="0 0 24 24" style="width: 24px; height: 24px; fill: currentColor;">
59                <path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>
60            </svg>
61        `;
62        
63        // Create text content
64        const textDiv = document.createElement('div');
65        textDiv.className = 'yt-spec-button-shape-next__button-text-content';
66        textDiv.textContent = 'Summarize';
67        
68        // Create touch feedback
69        const touchFeedback = document.createElement('yt-touch-feedback-shape');
70        touchFeedback.className = 'yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response';
71        touchFeedback.setAttribute('aria-hidden', 'true');
72        
73        button.appendChild(iconDiv);
74        button.appendChild(textDiv);
75        button.appendChild(touchFeedback);
76        buttonContainer.appendChild(button);
77        
78        // Add click event listener
79        button.addEventListener('click', handleSummarizeClick);
80        
81        // Insert button into action bar
82        actionBar.appendChild(buttonContainer);
83        console.log('Summarize button created successfully');
84    }
85
86    // Extract video ID from URL
87    function getVideoId() {
88        const urlParams = new URLSearchParams(window.location.search);
89        return urlParams.get('v');
90    }
91
92    // Fetch video transcript/captions
93    async function getVideoTranscript(videoId) {
94        console.log('Fetching transcript for video:', videoId);
95        
96        try {
97            // Try to get captions from YouTube's API
98            const response = await GM.xmlhttpRequest({
99                method: 'GET',
100                url: `https://www.youtube.com/watch?v=${videoId}`,
101                headers: {
102                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
103                }
104            });
105
106            const html = response.responseText;
107            
108            // Extract captions data from the page
109            const captionsRegex = /"captions":\s*(\{[^}]+\})/;
110            const match = html.match(captionsRegex);
111            
112            if (match) {
113                console.log('Found captions data');
114                // Try to extract caption tracks
115                const captionTracksRegex = /"captionTracks":\s*(\[[^\]]+\])/;
116                const tracksMatch = html.match(captionTracksRegex);
117                
118                if (tracksMatch) {
119                    const tracks = JSON.parse(tracksMatch[1]);
120                    if (tracks.length > 0) {
121                        const captionUrl = tracks[0].baseUrl;
122                        console.log('Fetching caption from:', captionUrl);
123                        
124                        const captionResponse = await GM.xmlhttpRequest({
125                            method: 'GET',
126                            url: captionUrl
127                        });
128                        
129                        // Parse XML captions
130                        const parser = new DOMParser();
131                        const xmlDoc = parser.parseFromString(captionResponse.responseText, 'text/xml');
132                        const textElements = xmlDoc.getElementsByTagName('text');
133                        
134                        let transcript = '';
135                        for (let i = 0; i < textElements.length; i++) {
136                            transcript += textElements[i].textContent + ' ';
137                        }
138                        
139                        console.log('Transcript extracted, length:', transcript.length);
140                        return transcript.trim();
141                    }
142                }
143            }
144            
145            // Fallback: extract video description and title
146            console.log('No captions found, using video metadata');
147            const titleElement = document.querySelector('.slim-video-information-title');
148            const title = titleElement ? titleElement.textContent.trim() : '';
149            
150            return `Video Title: ${title}\n\nNote: Transcript not available for this video. Summary will be based on title only.`;
151            
152        } catch (error) {
153            console.error('Error fetching transcript:', error);
154            throw error;
155        }
156    }
157
158    // Create summary display modal
159    function createSummaryModal() {
160        const modal = document.createElement('div');
161        modal.id = 'ai-summary-modal';
162        modal.style.cssText = `
163            position: fixed;
164            top: 0;
165            left: 0;
166            width: 100%;
167            height: 100%;
168            background: rgba(0, 0, 0, 0.8);
169            z-index: 10000;
170            display: flex;
171            align-items: center;
172            justify-content: center;
173            padding: 20px;
174        `;
175        
176        const modalContent = document.createElement('div');
177        modalContent.style.cssText = `
178            background: #282828;
179            color: #ffffff;
180            border-radius: 12px;
181            padding: 24px;
182            max-width: 600px;
183            width: 100%;
184            max-height: 80vh;
185            overflow-y: auto;
186            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
187        `;
188        
189        const header = document.createElement('div');
190        header.style.cssText = `
191            display: flex;
192            justify-content: space-between;
193            align-items: center;
194            margin-bottom: 20px;
195            border-bottom: 2px solid #3ea6ff;
196            padding-bottom: 12px;
197        `;
198        
199        const title = document.createElement('h2');
200        title.textContent = 'AI Video Summary';
201        title.style.cssText = `
202            margin: 0;
203            font-size: 20px;
204            font-weight: 600;
205            color: #3ea6ff;
206        `;
207        
208        const closeButton = document.createElement('button');
209        closeButton.textContent = '✕';
210        closeButton.style.cssText = `
211            background: transparent;
212            border: none;
213            color: #ffffff;
214            font-size: 24px;
215            cursor: pointer;
216            padding: 0;
217            width: 32px;
218            height: 32px;
219            display: flex;
220            align-items: center;
221            justify-content: center;
222            border-radius: 50%;
223            transition: background 0.2s;
224        `;
225        closeButton.onmouseover = () => closeButton.style.background = 'rgba(255, 255, 255, 0.1)';
226        closeButton.onmouseout = () => closeButton.style.background = 'transparent';
227        closeButton.onclick = () => modal.remove();
228        
229        const contentDiv = document.createElement('div');
230        contentDiv.id = 'ai-summary-content';
231        contentDiv.style.cssText = `
232            font-size: 15px;
233            line-height: 1.6;
234            color: #e0e0e0;
235        `;
236        
237        header.appendChild(title);
238        header.appendChild(closeButton);
239        modalContent.appendChild(header);
240        modalContent.appendChild(contentDiv);
241        modal.appendChild(modalContent);
242        
243        // Close on background click
244        modal.addEventListener('click', (e) => {
245            if (e.target === modal) {
246                modal.remove();
247            }
248        });
249        
250        return modal;
251    }
252
253    // Show loading state
254    function showLoading(contentDiv) {
255        contentDiv.innerHTML = `
256            <div style="text-align: center; padding: 40px 0;">
257                <div style="display: inline-block; width: 40px; height: 40px; border: 4px solid #3ea6ff; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
258                <p style="margin-top: 16px; color: #aaaaaa;">Analyzing video content with AI...</p>
259            </div>
260        `;
261        
262        // Add animation
263        const style = document.createElement('style');
264        style.textContent = `
265            @keyframes spin {
266                to { transform: rotate(360deg); }
267            }
268        `;
269        document.head.appendChild(style);
270    }
271
272    // Handle summarize button click
273    async function handleSummarizeClick() {
274        console.log('Summarize button clicked');
275        
276        const videoId = getVideoId();
277        if (!videoId) {
278            alert('Could not find video ID');
279            return;
280        }
281
282        // Check if we have a cached summary
283        const cacheKey = `summary_${videoId}`;
284        let cachedSummary = await GM.getValue(cacheKey);
285        
286        // Create and show modal
287        const modal = createSummaryModal();
288        document.body.appendChild(modal);
289        
290        const contentDiv = document.getElementById('ai-summary-content');
291        
292        if (cachedSummary) {
293            console.log('Using cached summary');
294            contentDiv.innerHTML = cachedSummary;
295            return;
296        }
297        
298        showLoading(contentDiv);
299        
300        try {
301            // Get video transcript
302            const transcript = await getVideoTranscript(videoId);
303            console.log('Transcript obtained, generating summary...');
304            
305            // Generate AI summary with structured output
306            const summary = await RM.aiCall(
307                `Please analyze and summarize this YouTube video content. Provide a comprehensive summary with key points.\n\nContent: ${transcript.substring(0, 8000)}`,
308                {
309                    type: "json_schema",
310                    json_schema: {
311                        name: "video_summary",
312                        schema: {
313                            type: "object",
314                            properties: {
315                                mainTopic: {
316                                    type: "string",
317                                    description: "The main topic or theme of the video"
318                                },
319                                keyPoints: {
320                                    type: "array",
321                                    items: { type: "string" },
322                                    description: "List of key points covered in the video"
323                                },
324                                summary: {
325                                    type: "string",
326                                    description: "A concise 2-3 sentence summary of the video"
327                                },
328                                targetAudience: {
329                                    type: "string",
330                                    description: "Who would benefit most from watching this video"
331                                }
332                            },
333                            required: ["mainTopic", "keyPoints", "summary"]
334                        }
335                    }
336                }
337            );
338            
339            console.log('Summary generated:', summary);
340            
341            // Format and display summary
342            const formattedSummary = `
343                <div style="margin-bottom: 20px;">
344                    <h3 style="color: #3ea6ff; font-size: 16px; margin-bottom: 8px;">📌 Main Topic</h3>
345                    <p style="margin: 0;">${summary.mainTopic}</p>
346                </div>
347                
348                <div style="margin-bottom: 20px;">
349                    <h3 style="color: #3ea6ff; font-size: 16px; margin-bottom: 8px;">📝 Summary</h3>
350                    <p style="margin: 0;">${summary.summary}</p>
351                </div>
352                
353                <div style="margin-bottom: 20px;">
354                    <h3 style="color: #3ea6ff; font-size: 16px; margin-bottom: 12px;">🔑 Key Points</h3>
355                    <ul style="margin: 0; padding-left: 20px;">
356                        ${summary.keyPoints.map(point => `<li style="margin-bottom: 8px;">${point}</li>`).join('')}
357                    </ul>
358                </div>
359                
360                ${summary.targetAudience ? `
361                    <div style="margin-bottom: 20px;">
362                        <h3 style="color: #3ea6ff; font-size: 16px; margin-bottom: 8px;">👥 Target Audience</h3>
363                        <p style="margin: 0;">${summary.targetAudience}</p>
364                    </div>
365                ` : ''}
366                
367                <div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid #404040; text-align: center; color: #888888; font-size: 13px;">
368                    Summary generated by AI • Cached for future views
369                </div>
370            `;
371            
372            contentDiv.innerHTML = formattedSummary;
373            
374            // Cache the summary
375            await GM.setValue(cacheKey, formattedSummary);
376            console.log('Summary cached successfully');
377            
378        } catch (error) {
379            console.error('Error generating summary:', error);
380            contentDiv.innerHTML = `
381                <div style="text-align: center; padding: 20px; color: #ff6b6b;">
382                    <p style="font-size: 18px; margin-bottom: 8px;">⚠️ Error</p>
383                    <p>Failed to generate summary. Please try again.</p>
384                    <p style="font-size: 13px; color: #888888; margin-top: 12px;">${error.message}</p>
385                </div>
386            `;
387        }
388    }
389
390    // Initialize the extension
391    function init() {
392        console.log('Initializing YouTube Video Summarizer');
393        
394        // Wait for page to load
395        if (document.readyState === 'loading') {
396            document.addEventListener('DOMContentLoaded', init);
397            return;
398        }
399        
400        // Create button after a short delay to ensure YouTube elements are loaded
401        setTimeout(createSummarizeButton, 2000);
402        
403        // Watch for navigation changes (YouTube is a SPA)
404        const debouncedCreate = debounce(createSummarizeButton, 1000);
405        
406        const observer = new MutationObserver((mutations) => {
407            // Check if we're on a watch page
408            if (window.location.pathname === '/watch' && window.location.search.includes('v=')) {
409                debouncedCreate();
410            }
411        });
412        
413        observer.observe(document.body, {
414            childList: true,
415            subtree: true
416        });
417        
418        // Also listen for URL changes
419        let lastUrl = location.href;
420        new MutationObserver(() => {
421            const url = location.href;
422            if (url !== lastUrl) {
423                lastUrl = url;
424                if (url.includes('/watch?v=')) {
425                    setTimeout(createSummarizeButton, 2000);
426                }
427            }
428        }).observe(document, { subtree: true, childList: true });
429    }
430
431    // Start the extension
432    init();
433})();