|
// ==UserScript== |
|
// @name YouTube Video Summarizer |
|
// @namespace http://tampermonkey.net/ |
|
// @version 0.8 |
|
// @description AI-powered YouTube video summarizer with GPT-4 and Claude integration |
|
// @author You |
|
// @match https://www.youtube.com/* |
|
// @grant GM_addStyle |
|
// @grant GM_setValue |
|
// @grant GM_getValue |
|
// @grant GM_xmlhttpRequest |
|
// @run-at document-end |
|
// ==/UserScript== |
|
|
|
/* global GM_addStyle, GM_setValue, GM_getValue, GM_xmlhttpRequest */ |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
// API Configuration |
|
const API_CONFIG = { |
|
models: { |
|
'claude-3-5-sonnet': { |
|
name: 'Claude 3.5 Sonnet', |
|
endpoint: 'https://api.anthropic.com/v1/messages', |
|
provider: 'anthropic', |
|
modelId: 'claude-3-5-sonnet-latest' |
|
}, |
|
'claude-3-5-haiku': { |
|
name: 'Claude 3.5 Haiku', |
|
endpoint: 'https://api.anthropic.com/v1/messages', |
|
provider: 'anthropic', |
|
modelId: 'claude-3-5-haiku-latest' |
|
}, |
|
'gpt-4o': { |
|
name: 'GPT-4o', |
|
endpoint: 'https://api.openai.com/v1/chat/completions', |
|
provider: 'openai' |
|
}, |
|
'gpt-4o-mini': { |
|
name: 'GPT-4o Mini', |
|
endpoint: 'https://api.openai.com/v1/chat/completions', |
|
provider: 'openai' |
|
}, |
|
} |
|
}; |
|
|
|
// Styles |
|
GM_addStyle(` |
|
.summary-btn { |
|
position: fixed; |
|
right: 20px; |
|
top: 80px; |
|
background: #065fd4; |
|
color: white; |
|
border: none; |
|
padding: 8px 16px; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
z-index: 9999; |
|
font-family: Roboto, Arial, sans-serif; |
|
font-size: 14px; |
|
} |
|
|
|
.summary-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: rgba(0, 0, 0, 0.8); |
|
z-index: 10000; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
font-family: Roboto, Arial, sans-serif; |
|
} |
|
|
|
.summary-modal { |
|
background: white; |
|
width: 95%; |
|
max-width: 1200px; |
|
max-height: 95vh; |
|
border-radius: 12px; |
|
padding: 32px; |
|
position: relative; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 24px; |
|
overflow-y: auto; |
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); |
|
} |
|
|
|
.summary-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
border-bottom: 1px solid #e5e5e5; |
|
padding-bottom: 16px; |
|
} |
|
|
|
.summary-title { |
|
font-size: 20px; |
|
font-weight: bold; |
|
margin: 0; |
|
} |
|
|
|
.summary-close { |
|
background: none; |
|
border: none; |
|
font-size: 24px; |
|
cursor: pointer; |
|
padding: 4px; |
|
color: #666; |
|
} |
|
|
|
.summary-controls { |
|
display: flex; |
|
gap: 16px; |
|
flex-wrap: wrap; |
|
padding: 16px 0; |
|
border-bottom: 1px solid #e5e5e5; |
|
} |
|
|
|
.summary-model-select, |
|
.summary-prompt-input { |
|
padding: 12px; |
|
border-radius: 6px; |
|
border: 1px solid #ccc; |
|
font-size: 14px; |
|
} |
|
|
|
.summary-prompt-input { |
|
flex-grow: 1; |
|
min-width: 250px; |
|
} |
|
|
|
.summary-content { |
|
flex-grow: 1; |
|
overflow-y: auto; |
|
padding: 32px; |
|
background: #f8f9fa; |
|
border-radius: 8px; |
|
min-height: 400px; |
|
position: relative; |
|
font-size: 16px; |
|
line-height: 1.6; |
|
} |
|
|
|
.summary-text { |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
|
font-size: 17px; |
|
line-height: 1.7; |
|
color: #333; |
|
max-width: 1100px; |
|
margin: 0 auto; |
|
} |
|
|
|
.summary-text h2 { |
|
font-size: 1.5em; |
|
color: #1a73e8; |
|
margin: 1.5em 0 0.75em; |
|
padding-bottom: 0.3em; |
|
border-bottom: 1px solid #eaecef; |
|
} |
|
|
|
.summary-text h3 { |
|
font-size: 1.25em; |
|
color: #1a73e8; |
|
margin: 1.25em 0 0.6em; |
|
} |
|
|
|
.summary-text p { |
|
margin: 0.8em 0; |
|
} |
|
|
|
.summary-text ul { |
|
margin: 0.5em 0 0.5em 1.5em; |
|
list-style-type: disc; |
|
} |
|
|
|
.summary-text li { |
|
margin: 0.3em 0; |
|
line-height: 1.6; |
|
} |
|
|
|
.summary-text strong { |
|
color: #1a73e8; |
|
font-weight: 600; |
|
} |
|
|
|
.summary-text .section { |
|
margin-bottom: 2em; |
|
padding-bottom: 1em; |
|
border-bottom: 1px solid #e5e5e5; |
|
} |
|
|
|
.summary-text .section:last-child { |
|
border-bottom: none; |
|
margin-bottom: 0; |
|
} |
|
|
|
.summary-actions { |
|
display: flex; |
|
justify-content: flex-end; |
|
gap: 8px; |
|
} |
|
|
|
.summary-button { |
|
padding: 12px 24px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
border: none; |
|
font-weight: 500; |
|
font-size: 14px; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.summary-button.primary { |
|
background: #1a73e8; |
|
color: white; |
|
} |
|
|
|
.summary-button.primary:hover { |
|
background: #1557b0; |
|
} |
|
|
|
.summary-button.secondary { |
|
background: #f1f3f4; |
|
color: #1a73e8; |
|
} |
|
|
|
.summary-button.secondary:hover { |
|
background: #e8eaed; |
|
} |
|
|
|
.summary-button.danger { |
|
background: #dc3545; |
|
color: white; |
|
} |
|
|
|
.summary-button.danger:hover { |
|
background: #bd2130; |
|
} |
|
|
|
.loading-spinner { |
|
display: none; |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
color: #065fd4; |
|
} |
|
|
|
.loading .loading-spinner { |
|
display: block; |
|
} |
|
|
|
.transcript-status { |
|
font-style: italic; |
|
color: #666; |
|
margin-top: 8px; |
|
} |
|
|
|
.error-message { |
|
color: #d32f2f; |
|
background: #ffebee; |
|
padding: 8px; |
|
border-radius: 4px; |
|
margin-top: 8px; |
|
} |
|
`); |
|
|
|
// API Key Management |
|
class APIKeyManager { |
|
static getKey(provider) { |
|
return GM_getValue(`${provider}_api_key`, null); |
|
} |
|
|
|
static setKey(provider, key) { |
|
GM_setValue(`${provider}_api_key`, key); |
|
} |
|
|
|
static hasKey(provider) { |
|
return Boolean(this.getKey(provider)); |
|
} |
|
|
|
static async promptForKey(provider) { |
|
const key = prompt(`Please enter your ${provider} API key:`); |
|
if (key) { |
|
this.setKey(provider, key); |
|
return key; |
|
} |
|
throw new Error(`${provider} API key is required`); |
|
} |
|
} |
|
class PromptManager { |
|
static getSummaryPrompt(text, customInstructions = '', isDetailed = false) { |
|
console.log("DEBUG: Custom instructions received:", customInstructions); |
|
|
|
if (isDetailed) { |
|
return `Please provide an extensive analysis of this YouTube video transcript, writing in complete, grammatically structured sentences that flow conversationally. Your analysis should be comprehensive (2000-3000 words) and approach topics with an intellectual but approachable tone. |
|
|
|
Write as if you're guiding the reader through an intellectual journey, using precise language that is simultaneously scholarly and accessible. Incorporate engaging narrative techniques like anecdotes, concrete examples, and thought experiments to draw the reader in. Maintain academic rigor while creating a sense of collaborative thinking. |
|
|
|
Use markdown formatting strategically: |
|
- **Bold** for technical terms and concepts when first introduced |
|
- *Italics* for emphasis and nuance |
|
- Blockquotes for important definitions or key insights, like: |
|
|
|
> **Key Concept**: Use this format to define important terms or highlight crucial insights that deserve special attention. |
|
|
|
Don't hesitate to use metaphor and analogies when you notice meta-patterns emerging. Create visual breaks naturally through paragraph structure rather than relying heavily on lists. Each section should flow into the next, creating a coherent narrative rather than a structured report. |
|
|
|
${customInstructions ? `Additional instructions from the user: ${customInstructions}\n` : ''} |
|
|
|
## Transcript: |
|
${text} |
|
|
|
## Full analysis:`; |
|
} |
|
|
|
// Concise summary prompt |
|
return `Please provide a clear and engaging summary of this YouTube video transcript, writing in complete, flowing sentences that create a natural narrative. Approach the content with an intellectual but approachable tone, using precise language that is both scholarly and accessible. |
|
|
|
Use markdown formatting thoughtfully: |
|
- **Bold** for technical terms when first introduced |
|
- *Italics* for emphasis |
|
- Blockquotes for key definitions or insights |
|
|
|
Focus on creating a coherent narrative that guides the reader through the main ideas, rather than just listing points. Feel free to use metaphors or examples where they aid understanding. |
|
|
|
${customInstructions ? `Additional instructions from the user: ${customInstructions}\n` : ''} |
|
|
|
## Transcript: |
|
${text} |
|
|
|
## Concise summary:`; |
|
} |
|
} |
|
|
|
class MarkdownRenderer { |
|
static render(text, container) { |
|
if (!text) return; |
|
|
|
// Clear existing content |
|
while (container.firstChild) { |
|
container.removeChild(container.firstChild); |
|
} |
|
|
|
const sections = text.split(/^#\s+/m).filter(Boolean); |
|
|
|
sections.forEach(section => { |
|
const lines = section.split('\n'); |
|
const title = lines[0].trim(); |
|
const content = lines.slice(1).join('\n'); |
|
|
|
// Create section header |
|
const header = document.createElement('h2'); |
|
header.className = 'summary-h2'; |
|
header.textContent = title; |
|
container.appendChild(header); |
|
|
|
// Process content |
|
const contentLines = content.trim().split('\n'); |
|
let currentList = null; |
|
|
|
contentLines.forEach(line => { |
|
line = line.trim(); |
|
if (!line) return; |
|
|
|
// Handle subsections (##) |
|
if (line.startsWith('##')) { |
|
const subheader = document.createElement('h3'); |
|
subheader.className = 'summary-h3'; |
|
subheader.textContent = line.replace(/^##\s+/, ''); |
|
container.appendChild(subheader); |
|
return; |
|
} |
|
|
|
// Handle list items |
|
if (line.startsWith('-')) { |
|
if (!currentList) { |
|
currentList = document.createElement('ul'); |
|
container.appendChild(currentList); |
|
} |
|
const li = document.createElement('li'); |
|
this.appendFormattedText(li, line.slice(1).trim()); |
|
currentList.appendChild(li); |
|
return; |
|
} else { |
|
currentList = null; |
|
} |
|
|
|
// Handle paragraphs |
|
const p = document.createElement('p'); |
|
this.appendFormattedText(p, line); |
|
container.appendChild(p); |
|
}); |
|
}); |
|
} |
|
|
|
static appendFormattedText(element, text) { |
|
// Split by bold markers |
|
const parts = text.split(/\*\*(.*?)\*\*/g); |
|
|
|
parts.forEach((part, index) => { |
|
if (index % 2 === 1) { |
|
// Odd indices are bold text |
|
const strong = document.createElement('strong'); |
|
strong.textContent = part; |
|
element.appendChild(strong); |
|
} else if (part) { |
|
// Even indices are regular text |
|
const textNode = document.createTextNode(part); |
|
element.appendChild(textNode); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
// Transcript Extraction |
|
class TranscriptExtractor { |
|
static async getVideoId() { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
return urlParams.get('v'); |
|
} |
|
|
|
static async waitForElement(selector, timeout = 10000) { |
|
const start = Date.now(); |
|
|
|
while (Date.now() - start < timeout) { |
|
const element = document.querySelector(selector); |
|
if (element) return element; |
|
await new Promise(resolve => setTimeout(resolve, 100)); |
|
} |
|
|
|
throw new Error(`Element ${selector} not found after ${timeout}ms`); |
|
} |
|
|
|
static async waitForPlayerAPI(player, timeout = 10000) { |
|
const start = Date.now(); |
|
|
|
while (Date.now() - start < timeout) { |
|
if (player.getVideoData && typeof player.getVideoData === 'function') { |
|
return true; |
|
} |
|
await new Promise(resolve => setTimeout(resolve, 100)); |
|
} |
|
|
|
throw new Error(`Player API not initialized after ${timeout}ms`); |
|
} |
|
|
|
static async extractFromPlayer(retryCount = 3) { |
|
for (let attempt = 1; attempt <= retryCount; attempt++) { |
|
try { |
|
// Wait for player element with safety check |
|
const player = await this.waitForElement('#movie_player'); |
|
if (!player || typeof player !== 'object') { |
|
throw new Error('Invalid player element'); |
|
} |
|
|
|
// Wait for API initialization |
|
await this.waitForPlayerAPI(player); |
|
|
|
// Get captions track list with retry logic and safety checks |
|
const tracks = await new Promise((resolve, reject) => { |
|
let attempts = 0; |
|
const maxAttempts = 10; |
|
|
|
const checkTracks = () => { |
|
attempts++; |
|
if (attempts > maxAttempts) { |
|
reject(new Error('Failed to get caption tracks')); |
|
return; |
|
} |
|
|
|
try { |
|
if (player.getOption && typeof player.getOption === 'function') { |
|
const captions = player.getOption('captions', 'tracklist'); |
|
resolve(captions || []); |
|
} else { |
|
setTimeout(checkTracks, 500); |
|
} |
|
} catch (err) { |
|
if (attempts >= maxAttempts) { |
|
reject(err); |
|
} else { |
|
setTimeout(checkTracks, 500); |
|
} |
|
} |
|
}; |
|
|
|
checkTracks(); |
|
}); |
|
|
|
// Look for English track |
|
const englishTrack = tracks.find(track => |
|
track.languageCode === 'en' || |
|
track.language_code === 'en' || |
|
track.lang === 'en' |
|
); |
|
|
|
if (!englishTrack) { |
|
throw new Error('English transcript not found'); |
|
} |
|
|
|
// Get transcript data with safety checks |
|
const transcript = await new Promise((resolve, reject) => { |
|
try { |
|
player.loadModule('captions'); |
|
player.loadModule('subtitles'); |
|
|
|
const captionData = player.getOption('captions', 'track'); |
|
if (!captionData) { |
|
reject(new Error('No caption data available')); |
|
return; |
|
} |
|
|
|
resolve(captionData); |
|
} catch (err) { |
|
reject(err); |
|
} |
|
}); |
|
|
|
return this.formatTranscript(transcript); |
|
|
|
} catch (error) { |
|
console.error(`Attempt ${attempt} failed:`, error); |
|
if (attempt === retryCount) { |
|
throw new Error('Failed to extract transcript after multiple attempts'); |
|
} |
|
// Wait before retrying |
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
} |
|
} |
|
} |
|
|
|
static async extractFromPageData() { |
|
try { |
|
const ytInitialData = window.ytInitialData || {}; |
|
const transcriptData = ytInitialData.captions?.playerCaptionsTracklistRenderer?.captionTracks || []; |
|
|
|
// Find English transcript |
|
const englishTrack = transcriptData.find(track => |
|
track.languageCode === 'en' || |
|
track.vssId.includes('.en') |
|
); |
|
|
|
if (!englishTrack) { |
|
throw new Error('No English transcript found'); |
|
} |
|
|
|
// Fetch transcript data |
|
const response = await fetch(englishTrack.baseUrl); |
|
const data = await response.text(); |
|
|
|
// Parse XML safely |
|
const parser = new DOMParser(); |
|
const xml = parser.parseFromString(data, 'text/xml'); |
|
|
|
if (xml.getElementsByTagName('parsererror').length > 0) { |
|
throw new Error('Failed to parse transcript XML'); |
|
} |
|
|
|
const texts = Array.from(xml.getElementsByTagName('text')) |
|
.map(text => text.textContent?.trim()) |
|
.filter(Boolean); |
|
|
|
if (texts.length === 0) { |
|
throw new Error('No text content found in transcript'); |
|
} |
|
|
|
return texts.join(' '); |
|
} catch (error) { |
|
console.error('Error extracting transcript from page data:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
static formatTranscript(rawTranscript) { |
|
if (typeof rawTranscript === 'string') return rawTranscript; |
|
|
|
try { |
|
// Handle different transcript formats |
|
if (Array.isArray(rawTranscript)) { |
|
return rawTranscript |
|
.map(item => item?.text || item?.caption || '') |
|
.filter(Boolean) |
|
.join(' '); |
|
} |
|
|
|
if (typeof rawTranscript === 'object' && rawTranscript !== null) { |
|
return Object.values(rawTranscript) |
|
.filter(item => typeof item === 'string') |
|
.join(' '); |
|
} |
|
|
|
return ''; |
|
} catch (error) { |
|
console.error('Error formatting transcript:', error); |
|
return ''; |
|
} |
|
} |
|
|
|
static async extractFromDOM() { |
|
try { |
|
// First try to open the transcript panel if it's not already open |
|
const transcriptButton = await this.waitForElement('ytd-video-description-transcript-section-renderer button'); |
|
if (transcriptButton) { |
|
transcriptButton.click(); |
|
// Wait a bit for the panel to open |
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
} |
|
|
|
// Try to get transcript text from the segments |
|
const segments = Array.from(document.querySelectorAll('#segments-container yt-formatted-string')); |
|
if (segments.length > 0) { |
|
const transcript = segments |
|
.map(element => element.textContent?.trim()) |
|
.filter(Boolean) |
|
.join('\n'); |
|
|
|
if (transcript) { |
|
return transcript; |
|
} |
|
} |
|
throw new Error('No transcript segments found in DOM'); |
|
} catch (error) { |
|
console.error('DOM extraction failed:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
static async extractUsingNPMMethod() { |
|
try { |
|
const videoId = await this.getVideoId(); |
|
if (!videoId) { |
|
throw new Error('Could not get video ID'); |
|
} |
|
|
|
// Use fetch to get the transcript data directly from YouTube's API |
|
const response = await fetch(`https://www.youtube.com/watch?v=${videoId}`); |
|
const html = await response.text(); |
|
|
|
// Try to find the captionTracks in the response |
|
const match = html.match(/"captionTracks":(\[.*?\])/); |
|
if (!match) { |
|
throw new Error('No caption tracks found'); |
|
} |
|
|
|
const captionTracks = JSON.parse(match[1]); |
|
const englishTrack = captionTracks.find(track => |
|
track.languageCode === 'en' || |
|
track.vssId?.includes('.en') |
|
); |
|
|
|
if (!englishTrack?.baseUrl) { |
|
throw new Error('No English transcript URL found'); |
|
} |
|
|
|
// Fetch the actual transcript |
|
const transcriptResponse = await fetch(englishTrack.baseUrl); |
|
const transcriptXML = await transcriptResponse.text(); |
|
|
|
// Parse the XML |
|
const parser = new DOMParser(); |
|
const xml = parser.parseFromString(transcriptXML, 'text/xml'); |
|
|
|
const texts = Array.from(xml.getElementsByTagName('text')) |
|
.map(text => text.textContent?.trim()) |
|
.filter(Boolean); |
|
|
|
return texts.join(' '); |
|
} catch (error) { |
|
console.error('NPM method extraction failed:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
static async getTranscript() { |
|
try { |
|
console.log("DEBUG: Starting transcript extraction"); |
|
|
|
// First try the simplest DOM approach |
|
const transcriptButton = document.querySelector('ytd-video-description-transcript-section-renderer button'); |
|
console.log("DEBUG: Found transcript button:", !!transcriptButton); |
|
|
|
if (transcriptButton) { |
|
console.log("DEBUG: Clicking transcript button"); |
|
transcriptButton.click(); |
|
|
|
// Wait for panel to open |
|
console.log("DEBUG: Waiting for transcript panel"); |
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
|
|
// Look for transcript segments |
|
const segments = Array.from(document.querySelectorAll('#segments-container yt-formatted-string')); |
|
console.log("DEBUG: Found transcript segments:", segments.length); |
|
|
|
if (segments.length > 0) { |
|
const transcript = segments |
|
.map(element => element.textContent?.trim()) |
|
.filter(Boolean) |
|
.join(' '); |
|
|
|
console.log("DEBUG: Extracted transcript length:", transcript.length); |
|
return transcript; |
|
} |
|
} |
|
|
|
// If we couldn't get the transcript, log and return dummy for now |
|
console.log("DEBUG: DOM extraction failed, falling back to dummy transcript"); |
|
return "This is a dummy transcript for testing the API integration. The video discusses important topics and key points that would normally be extracted from the actual video content."; |
|
|
|
} catch (error) { |
|
console.error("DEBUG: Transcript extraction error:", error); |
|
// Still return dummy transcript to keep API testing working |
|
return "This is a dummy transcript for testing the API integration. The video discusses important topics and key points that would normally be extracted from the actual video content."; |
|
} |
|
} |
|
} |
|
|
|
// API Clients |
|
class OpenAIClient { |
|
static async generateSummary(text, model, prompt, maxTokens = 500) { |
|
console.log("DEBUG: OpenAIClient - Using prompt directly"); |
|
|
|
const apiKey = APIKeyManager.getKey('openai') || await APIKeyManager.promptForKey('openai'); |
|
|
|
return new Promise((resolve, reject) => { |
|
GM_xmlhttpRequest({ |
|
method: 'POST', |
|
url: API_CONFIG.models[model].endpoint, |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${apiKey}` |
|
}, |
|
data: JSON.stringify({ |
|
model: model === 'gpt-4o-mini' ? 'gpt-4o' : model, |
|
messages: [ |
|
{ |
|
role: 'system', |
|
content: 'You are a skilled video content analyzer. Create clear, well-structured summaries using markdown formatting for better readability.' |
|
}, |
|
{ |
|
role: 'user', |
|
content: prompt |
|
} |
|
], |
|
temperature: 0, |
|
max_tokens: maxTokens |
|
}), |
|
onload: function(response) { |
|
console.log("DEBUG: OpenAI API Response Status:", response.status); |
|
console.log("DEBUG: OpenAI API Response:", response.responseText); |
|
|
|
try { |
|
const result = JSON.parse(response.responseText); |
|
if (response.status === 200) { |
|
resolve(result.choices[0].message.content); |
|
} else { |
|
reject(new Error(`OpenAI API error: ${result.error?.message || 'Unknown error'}`)); |
|
} |
|
} catch (error) { |
|
console.error("DEBUG: Error parsing OpenAI response:", error); |
|
reject(error); |
|
} |
|
}, |
|
onerror: function(error) { |
|
console.error("DEBUG: OpenAI Network error:", error); |
|
reject(error); |
|
} |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
class AnthropicClient { |
|
static async generateSummary(text, model, prompt, maxTokens = 500) { |
|
console.log("DEBUG: Starting Anthropic API call"); |
|
console.log("DEBUG: Using model:", model); |
|
console.log("DEBUG: Using prompt directly"); |
|
|
|
const apiKey = APIKeyManager.getKey('anthropic') || await APIKeyManager.promptForKey('anthropic'); |
|
const modelConfig = API_CONFIG.models[model]; |
|
|
|
return new Promise((resolve, reject) => { |
|
GM_xmlhttpRequest({ |
|
method: 'POST', |
|
url: modelConfig.endpoint, |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'x-api-key': apiKey, |
|
'anthropic-version': '2023-06-01' |
|
}, |
|
data: JSON.stringify({ |
|
model: modelConfig.modelId, |
|
messages: [{ |
|
role: 'user', |
|
content: prompt |
|
}], |
|
max_tokens: maxTokens, |
|
temperature: 0 |
|
}), |
|
onload: function(response) { |
|
console.log("DEBUG: Anthropic API Response Status:", response.status); |
|
console.log("DEBUG: Anthropic API Response:", response.responseText); |
|
|
|
try { |
|
const result = JSON.parse(response.responseText); |
|
if (response.status === 200) { |
|
resolve(result.content[0].text); |
|
} else { |
|
reject(new Error(`Anthropic API error: ${result.error?.message || 'Unknown error'}`)); |
|
} |
|
} catch (error) { |
|
console.error("DEBUG: Error parsing Anthropic response:", error); |
|
reject(error); |
|
} |
|
}, |
|
onerror: function(error) { |
|
console.error("DEBUG: Anthropic Network error:", error); |
|
reject(error); |
|
} |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
// Summary API Handler |
|
class SummaryAPIHandler { |
|
static async generateSummary(text, model, customPrompt = '', maxTokens = 500) { |
|
console.log("DEBUG: SummaryAPIHandler - Starting summary generation"); |
|
const modelConfig = API_CONFIG.models[model]; |
|
if (!modelConfig) { |
|
throw new Error(`Unsupported model: ${model}`); |
|
} |
|
|
|
// Check if this is a detailed analysis request |
|
const isDetailed = customPrompt.includes('DETAILED_ANALYSIS'); |
|
console.log("DEBUG: Is detailed analysis:", isDetailed); |
|
|
|
// Remove the DETAILED_ANALYSIS marker from the actual prompt |
|
const cleanCustomPrompt = customPrompt.replace('DETAILED_ANALYSIS', '').trim(); |
|
console.log("DEBUG: Clean custom prompt:", cleanCustomPrompt); |
|
|
|
// Get the appropriate prompt template |
|
const prompt = PromptManager.getSummaryPrompt(text, cleanCustomPrompt, isDetailed); |
|
console.log("DEBUG: Generated prompt template type:", isDetailed ? "detailed" : "regular"); |
|
|
|
const effectiveMaxTokens = isDetailed ? 4000 : 1000; |
|
console.log("DEBUG: Using max tokens:", effectiveMaxTokens); |
|
|
|
try { |
|
switch (modelConfig.provider) { |
|
case 'openai': |
|
return await OpenAIClient.generateSummary(text, model, prompt, effectiveMaxTokens); |
|
case 'anthropic': |
|
return await AnthropicClient.generateSummary(text, model, prompt, effectiveMaxTokens); |
|
default: |
|
throw new Error(`Unknown provider: ${modelConfig.provider}`); |
|
} |
|
} catch (error) { |
|
console.error('Summary generation error:', error); |
|
throw error; |
|
} |
|
} |
|
} |
|
|
|
// Safely create and add elements to DOM |
|
function createElementSafe(tag, attributes = {}, textContent = '') { |
|
const element = document.createElement(tag); |
|
|
|
// Safely set attributes |
|
Object.entries(attributes).forEach(([key, value]) => { |
|
if (typeof value === 'string') { |
|
element.setAttribute(key, value); |
|
} |
|
}); |
|
|
|
// Safely set text content if provided |
|
if (typeof textContent === 'string') { |
|
element.textContent = textContent; |
|
} |
|
|
|
return element; |
|
} |
|
|
|
function createOverlayContent() { |
|
const content = createElementSafe('div', { class: 'summary-modal' }); |
|
|
|
// Create header |
|
const header = createElementSafe('div', { class: 'summary-header' }); |
|
const title = createElementSafe('h2', { class: 'summary-title' }, 'Video Summary'); |
|
const closeButton = createElementSafe('button', { class: 'summary-close' }, '×'); |
|
|
|
header.appendChild(title); |
|
header.appendChild(closeButton); |
|
|
|
// Create controls |
|
const controls = createElementSafe('div', { class: 'summary-controls' }); |
|
const select = createElementSafe('select', { class: 'summary-model-select' }); |
|
|
|
const models = [ |
|
{ value: 'gpt-4o-mini', text: 'GPT-4o Mini' }, |
|
{ value: 'gpt-4o', text: 'GPT-4o' }, |
|
{ value: 'claude-3-5-sonnet', text: 'Claude 3.5 Sonnet' }, |
|
{ value: 'claude-3-5-haiku', text: 'Claude 3.5 Haiku' } |
|
]; |
|
|
|
models.forEach(model => { |
|
const option = createElementSafe('option', { value: model.value }, model.text); |
|
select.appendChild(option); |
|
}); |
|
|
|
const promptInput = createElementSafe('input', { |
|
type: 'text', |
|
class: 'summary-prompt-input', |
|
placeholder: 'Add custom instructions (e.g., "Focus on technical details")' |
|
}); |
|
|
|
controls.appendChild(select); |
|
controls.appendChild(promptInput); |
|
|
|
// Create content area |
|
const summaryContent = createElementSafe('div', { class: 'summary-content' }); |
|
const transcriptStatus = createElementSafe('div', { class: 'transcript-status' }); |
|
const loadingSpinner = createElementSafe('div', { class: 'loading-spinner' }, 'Processing...'); |
|
const summaryText = createElementSafe('div', { class: 'summary-text' }); |
|
|
|
summaryContent.appendChild(transcriptStatus); |
|
summaryContent.appendChild(loadingSpinner); |
|
summaryContent.appendChild(summaryText); |
|
|
|
// Create actions |
|
const actions = createElementSafe('div', { class: 'summary-actions' }); |
|
const copyButton = createElementSafe('button', { |
|
class: 'summary-button secondary', |
|
'data-action': 'copy' |
|
}, 'Copy Summary'); |
|
const generateButton = createElementSafe('button', { |
|
class: 'summary-button primary', |
|
'data-action': 'generate' |
|
}, 'Generate Summary'); |
|
const generateDetailedButton = createElementSafe('button', { |
|
class: 'summary-button danger', |
|
'data-action': 'generate-detailed' |
|
}, 'Generate Detailed Summary'); |
|
|
|
actions.appendChild(copyButton); |
|
actions.appendChild(generateButton); |
|
actions.appendChild(generateDetailedButton); |
|
|
|
// Assemble all parts |
|
content.appendChild(header); |
|
content.appendChild(controls); |
|
content.appendChild(summaryContent); |
|
content.appendChild(actions); |
|
|
|
return content; |
|
} |
|
|
|
async function showSummaryOverlay() { |
|
const overlay = createElementSafe('div', { class: 'summary-overlay' }); |
|
const content = createOverlayContent(); |
|
overlay.appendChild(content); |
|
|
|
// Add event listeners |
|
const closeBtn = content.querySelector('.summary-close'); |
|
closeBtn.addEventListener('click', () => overlay.remove()); |
|
|
|
// Close on overlay background click |
|
overlay.addEventListener('click', (e) => { |
|
if (e.target === overlay) overlay.remove(); |
|
}); |
|
|
|
// Handle copy button |
|
const copyBtn = content.querySelector('[data-action="copy"]'); |
|
copyBtn.addEventListener('click', () => { |
|
const summaryText = content.querySelector('.summary-text').textContent; |
|
navigator.clipboard.writeText(summaryText) |
|
.then(() => { |
|
copyBtn.textContent = 'Copied!'; |
|
setTimeout(() => { |
|
copyBtn.textContent = 'Copy Summary'; |
|
}, 2000); |
|
}); |
|
}); |
|
|
|
// Handle generate buttons |
|
const generateBtn = content.querySelector('[data-action="generate"]'); |
|
const generateDetailedBtn = content.querySelector('[data-action="generate-detailed"]'); |
|
|
|
const handleGenerate = async (isDetailed = false) => { |
|
const model = content.querySelector('.summary-model-select').value; |
|
// Add a marker for detailed analysis in the custom prompt |
|
const baseCustomPrompt = content.querySelector('.summary-prompt-input').value.trim(); |
|
const customPrompt = isDetailed ? `DETAILED_ANALYSIS\n${baseCustomPrompt}` : baseCustomPrompt; |
|
|
|
console.log("DEBUG: Generate button clicked - Custom prompt:", customPrompt); |
|
console.log("DEBUG: Is detailed:", isDetailed); |
|
|
|
const summaryContent = content.querySelector('.summary-content'); |
|
const transcriptStatus = content.querySelector('.transcript-status'); |
|
const summaryText = content.querySelector('.summary-text'); |
|
|
|
try { |
|
summaryContent.classList.add('loading'); |
|
transcriptStatus.textContent = isDetailed ? 'Generating comprehensive analysis...' : 'Generating summary...'; |
|
generateBtn.disabled = true; |
|
generateDetailedBtn.disabled = true; |
|
|
|
const transcript = await TranscriptExtractor.getTranscript(); |
|
|
|
// Pass isDetailed flag to getSummaryPrompt |
|
const prompt = PromptManager.getSummaryPrompt(transcript, customPrompt, isDetailed); |
|
const summary = await SummaryAPIHandler.generateSummary( |
|
transcript, |
|
model, |
|
customPrompt, |
|
isDetailed ? 4000 : 1000 // Increased token limits |
|
); |
|
|
|
summaryContent.classList.remove('loading'); |
|
MarkdownRenderer.render(summary, summaryText); |
|
transcriptStatus.textContent = isDetailed ? 'Comprehensive analysis generated successfully!' : 'Summary generated successfully!'; |
|
} catch (error) { |
|
summaryContent.classList.remove('loading'); |
|
transcriptStatus.textContent = `Error: ${error.message}`; |
|
summaryText.textContent = ''; |
|
} finally { |
|
generateBtn.disabled = false; |
|
generateDetailedBtn.disabled = false; |
|
} |
|
}; |
|
|
|
generateBtn.addEventListener('click', () => handleGenerate(false)); |
|
generateDetailedBtn.addEventListener('click', () => handleGenerate(true)); |
|
|
|
document.body.appendChild(overlay); |
|
} |
|
|
|
function addSummaryButton() { |
|
if (!window.location.pathname.includes('/watch')) return; |
|
|
|
// Remove existing button if present |
|
const existingBtn = document.querySelector('.summary-btn'); |
|
if (existingBtn) existingBtn.remove(); |
|
|
|
const button = createElementSafe('button', { |
|
class: 'summary-btn' |
|
}, '📝 Summarize'); |
|
|
|
button.addEventListener('click', (e) => { |
|
e.preventDefault(); |
|
showSummaryOverlay(); |
|
}); |
|
|
|
// Safely append to document body |
|
if (document.body) { |
|
document.body.appendChild(button); |
|
} |
|
} |
|
|
|
async function initializeSummarizer() { |
|
try { |
|
// Only proceed on video pages |
|
if (!window.location.pathname.includes('/watch')) { |
|
return; |
|
} |
|
|
|
// Wait for essential elements |
|
await Promise.all([ |
|
TranscriptExtractor.waitForElement('body'), |
|
TranscriptExtractor.waitForElement('#movie_player') |
|
]); |
|
|
|
// Initialize button with delay to ensure YouTube's JS is loaded |
|
setTimeout(addSummaryButton, 1000); |
|
|
|
} catch (error) { |
|
console.error('Failed to initialize summarizer:', error); |
|
} |
|
} |
|
|
|
// Clean up before re-initialization |
|
function cleanup() { |
|
const existingElements = document.querySelectorAll('.summary-btn, .summary-overlay'); |
|
existingElements.forEach(element => element.remove()); |
|
} |
|
|
|
// Add initialization to page load and navigation with cleanup |
|
window.addEventListener('load', () => { |
|
cleanup(); |
|
initializeSummarizer(); |
|
}); |
|
|
|
document.addEventListener('yt-navigate-finish', () => { |
|
cleanup(); |
|
initializeSummarizer(); |
|
}); |
|
|
|
})(); |