YouTube Comment Summarizer

Summarizes YouTube comments using AI to provide quick insights into viewer opinions and discussions

Size

11.5 KB

Version

1.1.3

Created

Nov 27, 2025

Updated

19 days ago

1// ==UserScript==
2// @name		YouTube Comment Summarizer
3// @description		Summarizes YouTube comments using AI to provide quick insights into viewer opinions and discussions
4// @version		1.1.3
5// @match		https://*.youtube.com/*
6// @icon		https://www.youtube.com/s/desktop/e5522eef/img/logos/favicon_32x32.png
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    let isProcessing = false;
12
13    function debounce(func, wait) {
14        let timeout;
15        return function(...args) {
16            clearTimeout(timeout);
17            timeout = setTimeout(() => func.apply(this, args), wait);
18        };
19    }
20
21    function createSummaryButton() {
22        const commentsHeader = document.querySelector('ytd-comments-header-renderer[class*="style-scope"] h2#count') || 
23                      document.querySelector('#comments #header h2');
24
25        if (!commentsHeader) {
26            return null;
27        }
28
29        if (document.querySelector('#comment-summary-btn')) {
30            return;
31        }
32
33        const button = document.createElement('button');
34        button.id = 'comment-summary-btn';
35        button.textContent = '🤖 Summarize Comments';
36        button.style.backgroundColor = '#1976d2';
37        button.style.color = 'white';
38        button.style.border = 'none';
39        button.style.padding = '8px 16px';
40        button.style.borderRadius = '20px';
41        button.style.cursor = 'pointer';
42        button.style.fontSize = '14px';
43        button.style.marginLeft = '16px';
44
45        button.addEventListener('click', handleSummaryClick);
46        commentsHeader.appendChild(button);
47    }
48
49    async function handleSummaryClick() {
50        if (isProcessing) return;
51
52        isProcessing = true;
53        const button = document.querySelector('#comment-summary-btn');
54        button.textContent = 'Processing...';
55        button.disabled = true;
56
57        const videoId = extractVideoId();
58        if (!videoId) {
59            alert('Unable to identify video');
60            button.textContent = '🤖 Summarize Comments';
61            button.disabled = false;
62            isProcessing = false;
63            return;
64        }
65
66        const cachedSummary = await loadCachedSummary(videoId);
67        if (cachedSummary) {
68            displaySummary(cachedSummary);
69            button.textContent = '🤖 Summarize Comments';
70            button.disabled = false;
71            isProcessing = false;
72            return;
73        }
74
75        const comments = await collectVisibleComments();
76        if (comments.length === 0) {
77            alert('No comments found to summarize');
78            button.textContent = '🤖 Summarize Comments';
79            button.disabled = false;
80            isProcessing = false;
81            return;
82        }
83
84        try {
85            const summary = await generateSummary(comments);
86            await saveSummaryToCache(videoId, summary);
87            displaySummary(summary);
88        } catch (error) {
89            console.error('Summary generation failed:', error);
90            alert('Failed to generate summary. Please try again.');
91        }
92
93        isProcessing = false;
94        button.textContent = '🤖 Summarize Comments';
95        button.disabled = false;
96    }
97
98    function extractVideoId() {
99        const urlParams = new URLSearchParams(window.location.search);
100        return urlParams.get('v') || null;
101    }
102
103    async function loadCachedSummary(videoId) {
104        try {
105            const cachedData = await GM.getValue('summary_' + videoId, null);
106            if (cachedData) {
107                return JSON.parse(cachedData);
108            }
109            return null;
110        } catch (error) {
111            console.error('Error loading cached summary:', error);
112            return null;
113        }
114    }
115
116    async function saveSummaryToCache(videoId, summary) {
117        try {
118            await GM.setValue('summary_' + videoId, JSON.stringify(summary));
119        } catch (error) {
120            console.error('Failed to cache summary:', error);
121        }
122    }
123
124    async function collectVisibleComments() {
125        const commentElements = document.querySelectorAll('yt-attributed-string#content-text[class*="style-scope"] span[role="text"]');
126        const comments = [];
127    
128        for (const element of commentElements) {
129            const text = element.textContent.trim();
130            if (text.length > 10) {
131                comments.push(text);
132            }
133        }
134    
135        return comments.slice(0, 50);
136    }
137
138    async function generateSummary(comments) {
139        const prompt = 'Analyze and summarize these YouTube comments. Provide key themes, sentiment analysis, and main discussion points:\n\n' + comments.join('\n---\n');
140    
141        const summary = await RM.aiCall(prompt, {
142            type: 'json_schema',
143            json_schema: {
144                name: 'comment_summary',
145                schema: {
146                    type: 'object',
147                    properties: {
148                        overallSentiment: {
149                            type: 'string',
150                            enum: ['positive', 'negative', 'neutral', 'mixed']
151                        },
152                        keyThemes: {
153                            type: 'array',
154                            items: { type: 'string' }
155                        },
156                        mainPoints: {
157                            type: 'array',
158                            items: { type: 'string' }
159                        },
160                        controversialTopics: {
161                            type: 'array',
162                            items: { type: 'string' }
163                        },
164                        viewerOpinions: {
165                            type: 'array',
166                            items: { type: 'string' }
167                        },
168                        commentCount: {
169                            type: 'number'
170                        }
171                    },
172                    required: ['overallSentiment', 'keyThemes', 'mainPoints', 'commentCount']
173                }
174            }
175        });
176    
177        return summary;
178    }
179
180    function displaySummary(summary) {
181        removePreviousSummary();
182    
183        const summaryContainer = document.createElement('div');
184        summaryContainer.id = 'youtube-comment-summary';
185        summaryContainer.style.backgroundColor = '#f8f9fa';
186        summaryContainer.style.border = '1px solid #e0e0e0';
187        summaryContainer.style.borderRadius = '8px';
188        summaryContainer.style.padding = '16px';
189        summaryContainer.style.margin = '16px 0';
190        summaryContainer.style.fontFamily = 'Roboto, Arial, sans-serif';
191    
192        const title = document.createElement('h3');
193        title.textContent = 'Comment Summary';
194        title.style.margin = '0 0 12px 0';
195        title.style.fontSize = '16px';
196        title.style.fontWeight = 'bold';
197        title.style.color = '#333';
198    
199        const sentimentSection = createSummarySection('Overall Sentiment', summary.overallSentiment, getSentimentColor(summary.overallSentiment));
200        const themesSection = createSummarySection('Key Themes', summary.keyThemes.join(', '), '#1976d2');
201    
202        const mainPointsList = summary.mainPoints.map(point => `<li>${point}</li>`).join('');
203        const pointsSection = createSummarySection('Main Discussion Points', `<ul>${mainPointsList}</ul>`, '#4caf50');
204    
205        summaryContainer.appendChild(title);
206        summaryContainer.appendChild(sentimentSection);
207        summaryContainer.appendChild(themesSection);
208        summaryContainer.appendChild(pointsSection);
209    
210        if (summary.controversialTopics && summary.controversialTopics.length > 0) {
211            const controversySection = createSummarySection('Controversial Topics', summary.controversialTopics, '#ff5722');
212            summaryContainer.appendChild(controversySection);
213        }
214    
215        const header = document.querySelector('#comments #header');
216        if (header) {
217            header.insertAdjacentElement('afterend', summaryContainer);
218        }
219    }
220
221    function createSummarySection(title, content, color) {
222        const section = document.createElement('div');
223    
224        const sectionTitle = document.createElement('div');
225        sectionTitle.textContent = title;
226        sectionTitle.style.fontWeight = 'bold';
227        sectionTitle.style.color = color;
228        sectionTitle.style.marginBottom = '8px';
229        sectionTitle.style.fontSize = '14px';
230    
231        const sectionContent = document.createElement('div');
232        if (Array.isArray(content)) {
233            const ul = document.createElement('ul');
234            content.forEach(item => {
235                const li = document.createElement('li');
236                li.textContent = item;
237                ul.appendChild(li);
238            });
239            sectionContent.appendChild(ul);
240        } else {
241            sectionContent.textContent = content;
242        }
243    
244        sectionContent.style.fontSize = '13px';
245        sectionContent.style.color = '#666';
246        sectionContent.style.lineHeight = '1.4';
247    
248        section.appendChild(sectionTitle);
249        section.appendChild(sectionContent);
250    
251        return section;
252    }
253
254    function getSentimentColor(sentiment) {
255        switch (sentiment) {
256        case 'positive':
257            return '#4caf50';
258        case 'negative':
259            return '#f44336';
260        case 'mixed':
261            return '#ff9800';
262        default:
263            return '#757575';
264        }
265    }
266
267    function removePreviousSummary() {
268        const existingSummary = document.querySelector('#youtube-comment-summary');
269        if (existingSummary) {
270            existingSummary.remove();
271        }
272    }
273
274    function waitForComments() {
275        let attempts = 0;
276        const maxAttempts = 20;
277
278        function checkForComments() {
279            const commentsSection = document.querySelector('#comments');
280            if (commentsSection) {
281                createSummaryButton();
282                return;
283            }
284    
285            attempts++;
286            if (attempts < maxAttempts) {
287                setTimeout(() => checkForComments(), 5000);
288            }
289        }
290
291        checkForComments();
292    }
293
294    function handlePageChange() {
295        if (window.location.href.includes('/watch?v=') && document.querySelector('#comments')) {
296            createSummaryButton();
297        } else {
298            console.log('Not on video page or comments not loaded yet');
299        }
300    }
301
302    const debouncedHandlePageChange = debounce(handlePageChange, 1000);
303
304    function init() {
305    // Check if window.location.href.includes('/watch?v=') to ensure we're on a video page, if not return early
306        if (!window.location.href.includes('/watch?v=')) {
307            return;
308        }
309
310        // Call waitForComments() to initialize the summary button when comments are loaded
311        waitForComments();
312
313        // Add window.addEventListener('popstate', debouncedHandlePageChange) to reinitialize when user navigates using browser back/forward with debounce
314        window.addEventListener('popstate', debouncedHandlePageChange);
315
316        // Create MutationObserver with debounced callback: new MutationObserver(debounce(() => { if (window.location.href.includes('/watch?v=')) handlePageChange(); }, 500)).observe(document.body, { childList: true, subtree: true }) with 500ms debounce
317        new MutationObserver(debounce(() => { 
318            if (window.location.href.includes('/watch?v=')) {
319                handlePageChange();
320            }
321        }, 500)).observe(document.body, { childList: true, subtree: true });
322    }
323
324    if (document.readyState === 'loading') {
325        document.addEventListener('DOMContentLoaded', init);
326    } else {
327        init();
328    }
329})();
YouTube Comment Summarizer | Robomonkey