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})();