YouTube Gemini Summarizer

Summarizes YouTube videos using Gemini API with a GUI for API key, model, and language selection (with RTL support).

当前为 2025-06-04 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Gemini Summarizer
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @description  Summarizes YouTube videos using Gemini API with a GUI for API key, model, and language selection (with RTL support).
// @author       kobaltGIT
// @match        https://www.youtube.com/watch*
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI0NCIgaGVpZ2h0PSI0NCIgcng9IjUiIHJ5PSI1IiBmaWxsPSIjNENBRjUwIi8+PGxpbmUgeDE9IjE4IiB5MT0iMjQiIHgyPSI0NiIgeTI9IjI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiLz48bGluZSB4MT0iMTgiIHkxPSIzMiIgeDI9IjQ2IiB5Mj0iMzIiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIvPjxsaW5lIHgxPSIxOCIgeTE9IjQwIiB4Mj0iMzQiIHkyPSI0MCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIzIi8+PHBvbHlnb24gcG9pbnRzPSIzOCwzNyA0Niw0MCAzOCw0MyIgZmlsbD0id2hpdGUiLz48L3N2Zz4=
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      generativelanguage.googleapis.com
// ==/UserScript==

(function() {
    'use strict';
    console.log('MY SCRIPT IS TRYING TO RUN (v0.2.0)');

    const SCRIPT_PREFIX = 'yt_gemini_summarizer_';
    const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";

    const HIDE_BUTTON_DELAY = 2000;
    const HIDE_BUTTON_DELAY_FAST = 500;
    const DIAGNOSTIC_INTERVAL_MS = 3000;

    const SUPPORTED_LANGUAGES = [
        { value: 'Russian', display: 'Russian', isRTL: false },
        { value: 'English', display: 'English', isRTL: false },
        { value: 'German', display: 'German', isRTL: false },
        { value: 'French', display: 'French', isRTL: false },
        { value: 'Italian', display: 'Italian', isRTL: false },
        { value: 'Spanish', display: 'Spanish', isRTL: false },
        { value: 'Hebrew', display: 'Hebrew', isRTL: true },
        { value: 'Yiddish', display: 'Yiddish', isRTL: true },
        { value: 'Arabic', display: 'Arabic', isRTL: true },
        { value: 'Chinese', display: 'Chinese', isRTL: false },
        { value: 'Korean', display: 'Korean', isRTL: false },
        { value: 'Japanese', display: 'Japanese', isRTL: false },
        { value: 'Czech', display: 'Czech', isRTL: false },
        { value: 'Polish', display: 'Polish', isRTL: false }
    ];
    const DEFAULT_LANGUAGE = 'Russian'; // This value must match one of the 'value' properties above

    let apiKey = '';
    let models = [];
    let selectedModel = '';
    let selectedLanguage = DEFAULT_LANGUAGE;

    let panel, apiKeyInput, modelSelect, languageSelect, summarizeButton, summaryOutput, statusDiv, fetchModelsButton;
    let togglePanelButton;
    let isPanelVisible = false;
    let playerContainer = null;

    let hideButtonTimer = null;
    let diagnosticIntervalId = null;


    function loadSettings() {
        apiKey = GM_getValue(SCRIPT_PREFIX + 'api_key', '');
        selectedModel = GM_getValue(SCRIPT_PREFIX + 'selected_model', '');
        selectedLanguage = GM_getValue(SCRIPT_PREFIX + 'selected_language', DEFAULT_LANGUAGE);
        isPanelVisible = GM_getValue(SCRIPT_PREFIX + 'is_panel_visible', false);
        if (apiKeyInput) apiKeyInput.value = apiKey;
        if (languageSelect) {
            languageSelect.value = selectedLanguage;
        }
    }

    function saveSettings() {
        GM_setValue(SCRIPT_PREFIX + 'api_key', apiKey);
        GM_setValue(SCRIPT_PREFIX + 'selected_model', selectedModel);
        GM_setValue(SCRIPT_PREFIX + 'selected_language', selectedLanguage);
        GM_setValue(SCRIPT_PREFIX + 'is_panel_visible', isPanelVisible);
    }

    function createGUI() {
        panel = document.createElement('div');
        panel.id = SCRIPT_PREFIX + 'panel';
        panel.style.display = isPanelVisible ? 'block' : 'none';

        const title = document.createElement('h3');
        title.textContent = 'Gemini Video Summarizer';
        panel.appendChild(title);

        const apiKeyDiv = document.createElement('div');
        const apiKeyLabel = document.createElement('label');
        apiKeyLabel.htmlFor = SCRIPT_PREFIX + 'api_key_input';
        apiKeyLabel.textContent = 'Gemini API Key:';
        apiKeyDiv.appendChild(apiKeyLabel);
        apiKeyInput = document.createElement('input');
        apiKeyInput.type = 'password';
        apiKeyInput.id = SCRIPT_PREFIX + 'api_key_input';
        apiKeyDiv.appendChild(apiKeyInput);
        fetchModelsButton = document.createElement('button');
        fetchModelsButton.id = SCRIPT_PREFIX + 'fetch_models_button';
        fetchModelsButton.textContent = 'Save & Fetch Models';
        apiKeyDiv.appendChild(fetchModelsButton);
        panel.appendChild(apiKeyDiv);

        const modelDiv = document.createElement('div');
        const modelLabel = document.createElement('label');
        modelLabel.htmlFor = SCRIPT_PREFIX + 'model_select';
        modelLabel.textContent = 'Select Model:';
        modelDiv.appendChild(modelLabel);
        modelSelect = document.createElement('select');
        modelSelect.id = SCRIPT_PREFIX + 'model_select';
        modelDiv.appendChild(modelSelect);
        panel.appendChild(modelDiv);

        const languageDiv = document.createElement('div');
        const languageLabel = document.createElement('label');
        languageLabel.htmlFor = SCRIPT_PREFIX + 'language_select';
        languageLabel.textContent = 'Select Summary Language:';
        languageDiv.appendChild(languageLabel);
        languageSelect = document.createElement('select');
        languageSelect.id = SCRIPT_PREFIX + 'language_select';
        SUPPORTED_LANGUAGES.forEach(lang => {
            const option = document.createElement('option');
            option.value = lang.value;
            option.textContent = lang.display; // Now using English display name
            languageSelect.appendChild(option);
        });
        languageSelect.value = selectedLanguage;
        languageDiv.appendChild(languageSelect);
        panel.appendChild(languageDiv);

        const summarizeDiv = document.createElement('div');
        summarizeButton = document.createElement('button');
        summarizeButton.id = SCRIPT_PREFIX + 'summarize_button';
        summarizeButton.textContent = 'Summarize Video';
        summarizeButton.disabled = true;
        summarizeDiv.appendChild(summarizeButton);
        panel.appendChild(summarizeDiv);

        statusDiv = document.createElement('div');
        statusDiv.id = SCRIPT_PREFIX + 'status_div';
        statusDiv.style.marginTop = '5px';
        statusDiv.style.fontStyle = 'italic';
        panel.appendChild(statusDiv);

        const summaryTitle = document.createElement('h4');
        summaryTitle.textContent = 'Summary:';
        panel.appendChild(summaryTitle);
        summaryOutput = document.createElement('textarea');
        summaryOutput.id = SCRIPT_PREFIX + 'summary_output';
        summaryOutput.rows = 10;
        summaryOutput.readOnly = true;
        summaryOutput.dir = 'ltr'; // Default LTR
        summaryOutput.style.textAlign = 'left'; // Default left align
        panel.appendChild(summaryOutput);

        document.body.appendChild(panel);

        fetchModelsButton.addEventListener('click', handleFetchModelsClick);
        modelSelect.addEventListener('change', (e) => {
            selectedModel = e.target.value;
            saveSettings();
            updateSummarizeButtonState();
        });
        languageSelect.addEventListener('change', (e) => {
            selectedLanguage = e.target.value;
            saveSettings();
            // Update text direction if summary already exists (optional, or just on new summary)
            const languageInfo = SUPPORTED_LANGUAGES.find(lang => lang.value === selectedLanguage);
            if (summaryOutput.value.trim() !== "") { // Only if there's text
                 if (languageInfo && languageInfo.isRTL) {
                    summaryOutput.dir = 'rtl';
                    summaryOutput.style.textAlign = 'right';
                } else {
                    summaryOutput.dir = 'ltr';
                    summaryOutput.style.textAlign = 'left';
                }
            }
        });
        summarizeButton.addEventListener('click', handleSummarizeClick);
    }

    function createToggleButton() {
        togglePanelButton = document.createElement('button');
        togglePanelButton.id = SCRIPT_PREFIX + 'toggle_button';
        togglePanelButton.textContent = 'Summarize';
        togglePanelButton.title = 'Toggle Summarizer Panel';
        togglePanelButton.addEventListener('click', handleTogglePanelClick);
    }

    function handleTogglePanelClick() {
        isPanelVisible = !isPanelVisible;
        panel.style.display = isPanelVisible ? 'block' : 'none';
        saveSettings();
        console.log(SCRIPT_PREFIX + "Panel visibility toggled. Is visible:", isPanelVisible);
        if (isPanelVisible && panel.matches(':hover')) {
            showToggleButton();
        } else if (!isPanelVisible && playerContainer && !playerContainer.matches(':hover')) {
            startHideButtonTimer(0);
        }
    }

    function showToggleButton() {
        if (!togglePanelButton) {
            console.warn(SCRIPT_PREFIX + "showToggleButton called but togglePanelButton is null!");
            return;
        }
        clearTimeout(hideButtonTimer);
        hideButtonTimer = null;
        if (togglePanelButton.style.opacity !== '1') {
            togglePanelButton.style.opacity = '1';
            togglePanelButton.style.pointerEvents = 'auto';
            // console.log(SCRIPT_PREFIX + "Toggle button SHOWN (opacity 1, pointerEvents auto).");
        }
    }

    function hideToggleButton() {
        if (!togglePanelButton) {
            console.warn(SCRIPT_PREFIX + "hideToggleButton called but togglePanelButton is null!");
            return;
        }
        if (togglePanelButton.style.opacity !== '0') {
            togglePanelButton.style.opacity = '0';
            togglePanelButton.style.pointerEvents = 'none';
            // console.log(SCRIPT_PREFIX + "Toggle button HIDDEN (opacity 0, pointerEvents none).");
        }
    }

    function startHideButtonTimer(delay = HIDE_BUTTON_DELAY) {
        clearTimeout(hideButtonTimer);
        hideButtonTimer = setTimeout(() => {
            if (playerContainer && playerContainer.matches(':hover')) return;
            if (panel && panel.matches(':hover') && isPanelVisible) return;
            if (togglePanelButton && togglePanelButton.matches(':hover')) return;
            hideToggleButton();
        }, delay);
    }

    function addStyles() {
        GM_addStyle(`
            #${SCRIPT_PREFIX}panel {
                position: fixed; top: 60px; right: 10px; width: 350px;
                background-color: #f9f9f9; border: 1px solid #ccc; padding: 15px;
                z-index: 9999; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                font-family: Arial, sans-serif; font-size: 13px;
            }
            #${SCRIPT_PREFIX}toggle_button {
                position: absolute; bottom: 55px; left: 15px;
                padding: 5px 8px; background-color: rgba(20, 20, 20, 0.7); color: white;
                border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px;
                cursor: pointer; z-index: 65; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
                font-size: 12px; line-height: 1;
                opacity: 0;
                pointer-events: none;
                transition: opacity 0.2s ease-in-out;
            }
            #${SCRIPT_PREFIX}toggle_button:hover {
                background-color: rgba(40, 40, 40, 0.9);
            }
            #movie_player.relative-positioning,
            .html5-video-player.relative-positioning,
            ytd-player.relative-positioning {
                position: relative !important;
            }
            #${SCRIPT_PREFIX}panel h3, #${SCRIPT_PREFIX}panel h4 { margin-top: 0; margin-bottom: 10px; color: #333; }
            #${SCRIPT_PREFIX}panel label { display: block; margin-bottom: 3px; font-weight: bold; color: #555; }
            #${SCRIPT_PREFIX}panel div { margin-bottom: 10px; }
            #${SCRIPT_PREFIX}panel input[type="password"],
            #${SCRIPT_PREFIX}panel select,
            #${SCRIPT_PREFIX}panel textarea { padding: 6px; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; width: 100%;}
            #${SCRIPT_PREFIX}api_key_input { width: calc(100% - 165px); margin-right: 5px; vertical-align: middle; }
            #${SCRIPT_PREFIX}summary_output { width: 100%; background-color: #fff; }
            #${SCRIPT_PREFIX}panel button { background-color: #4CAF50; color: white; padding: 8px 12px; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px; vertical-align: middle; }
            #${SCRIPT_PREFIX}panel button:last-child { margin-right: 0; }
            #${SCRIPT_PREFIX}panel button:hover { background-color: #45a049; }
            #${SCRIPT_PREFIX}panel button:disabled { background-color: #ccc; cursor: not-allowed; }
        `);
    }

    function updateStatus(message, isError = false) {
        if (statusDiv) {
            statusDiv.textContent = message;
            statusDiv.style.color = isError ? 'red' : 'green';
        }
        // console.log(SCRIPT_PREFIX + (isError ? "Error: " : "Status: ") + message);
    }

    function handleFetchModelsClick() {
        const newApiKey = apiKeyInput.value.trim();
        if (!newApiKey) {
            updateStatus("API Key cannot be empty.", true);
            return;
        }
        apiKey = newApiKey;
        saveSettings();
        updateStatus("API Key saved. Fetching models...");
        fetchAndPopulateModels();
    }

    function fetchAndPopulateModels() {
        if (!apiKey) {
            updateStatus("API Key is not set. Cannot fetch models.", true);
            populateModelDropdown([]);
            return;
        }
        updateStatus("Fetching models...");
        GM_xmlhttpRequest({
            method: "GET",
            url: `${GEMINI_API_BASE_URL}?key=${apiKey}`,
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        models = data.models.filter(model =>
                            model.supportedGenerationMethods && model.supportedGenerationMethods.includes("generateContent")
                        );
                        if (models.length === 0) {
                            updateStatus("No models supporting 'generateContent' found for this API key.", true);
                            populateModelDropdown([]);
                            return;
                        }
                        updateStatus(`Found ${models.length} compatible models.`, false);
                        populateModelDropdown(models);
                    } catch (e) {
                        updateStatus("Error parsing models response: " + e.message, true);
                        console.error(SCRIPT_PREFIX + "Error parsing models response:", e, response.responseText);
                        populateModelDropdown([]);
                    }
                } else {
                    let errorMsg = `Error fetching models: ${response.status}`;
                    try {
                        const errorData = JSON.parse(response.responseText);
                        if (errorData.error && errorData.error.message) {
                            errorMsg += ` - ${errorData.error.message}`;
                        }
                    } catch (e) { /* ignore */ }
                    updateStatus(errorMsg, true);
                    console.error(SCRIPT_PREFIX + "Error fetching models:", response);
                    populateModelDropdown([]);
                }
                updateSummarizeButtonState();
            },
            onerror: function(response) {
                updateStatus("Network error fetching models: " + response.statusText, true);
                console.error(SCRIPT_PREFIX + "Network error fetching models:", response);
                populateModelDropdown([]);
                updateSummarizeButtonState();
            }
        });
    }

    function populateModelDropdown(modelsToPopulate) {
        while (modelSelect.firstChild) {
            modelSelect.removeChild(modelSelect.firstChild);
        }
        if (modelsToPopulate.length === 0) {
            const option = document.createElement('option');
            option.value = "";
            option.textContent = "No models available";
            modelSelect.appendChild(option);
            modelSelect.disabled = true;
            selectedModel = "";
        } else {
            modelsToPopulate.forEach(model => {
                const option = document.createElement('option');
                option.value = model.name;
                option.textContent = model.displayName + ` (${model.name.split('/')[1]})`;
                modelSelect.appendChild(option);
            });
            modelSelect.disabled = false;
            let foundPrevious = false;
            if (selectedModel) {
                for (let i = 0; i < modelSelect.options.length; i++) {
                    if (modelSelect.options[i].value === selectedModel) {
                        modelSelect.selectedIndex = i;
                        foundPrevious = true;
                        break;
                    }
                }
            }
            if (!foundPrevious) {
                let flashModelIndex = modelsToPopulate.findIndex(m => m.name.includes("flash"));
                if (flashModelIndex !== -1) {
                    modelSelect.selectedIndex = flashModelIndex;
                } else if (modelSelect.options.length > 0) {
                    modelSelect.selectedIndex = 0;
                }
            }
            selectedModel = modelSelect.value;
        }
        saveSettings();
        updateSummarizeButtonState();
    }

    function updateSummarizeButtonState() {
        if (summarizeButton) {
            summarizeButton.disabled = !(apiKey && selectedModel && models.length > 0 && modelSelect.value && !modelSelect.disabled && selectedLanguage);
        }
    }

    function setSummaryTextAndDirection(text, languageValue) {
        summaryOutput.value = text;
        const languageInfo = SUPPORTED_LANGUAGES.find(lang => lang.value === languageValue);
        if (languageInfo && languageInfo.isRTL) {
            summaryOutput.dir = 'rtl';
            summaryOutput.style.textAlign = 'right';
        } else {
            summaryOutput.dir = 'ltr';
            summaryOutput.style.textAlign = 'left';
        }
    }

    async function handleSummarizeClick() {
        updateStatus("Starting summarization...", false);
        setSummaryTextAndDirection("", selectedLanguage); // Clear previous summary and set initial direction

        if (!apiKey || !selectedModel || !modelSelect.value || !selectedLanguage) {
            updateStatus("API Key, Model, or Language not selected correctly.", true);
            return;
        }
        const currentSelectedModel = modelSelect.value;
        const currentSelectedLanguage = languageSelect.value;

        const transcript = await getYouTubeTranscript();
        if (!transcript) {
            updateStatus("Could not retrieve video transcript.", true);
            // summaryOutput is already cleared and set to LTR by setSummaryTextAndDirection above
            return;
        }
        updateStatus(`Transcript retrieved. Sending to Gemini API for summary in ${currentSelectedLanguage}...`, false);

        GM_xmlhttpRequest({
            method: "POST",
            url: `${GEMINI_API_BASE_URL}/${currentSelectedModel.replace("models/", "")}:generateContent?key=${apiKey}`,
            headers: {
                "Content-Type": "application/json"
            },
            data: JSON.stringify({
                "contents": [{
                    "parts": [{
                        "text": `Please summarize the following video transcript in ${currentSelectedLanguage}:\n\n${transcript}`
                    }]
                }],
                "generationConfig": {
                    "temperature": 0.7,
                    "topK": 1,
                    "topP": 1,
                    "maxOutputTokens": 2048,
                }
            }),
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.candidates && data.candidates.length > 0 &&
                            data.candidates[0].content && data.candidates[0].content.parts &&
                            data.candidates[0].content.parts.length > 0 && data.candidates[0].content.parts[0].text) {
                            const summaryText = data.candidates[0].content.parts[0].text;
                            setSummaryTextAndDirection(summaryText, currentSelectedLanguage);
                            updateStatus("Summary generated successfully!", false);
                        } else if (data.promptFeedback && data.promptFeedback.blockReason) {
                            const errorText = `Error: Content generation blocked. Reason: ${data.promptFeedback.blockReasonDetails || data.promptFeedback.blockReason}`;
                            setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE); // Show error in default LTR language
                            updateStatus(`Content blocked: ${data.promptFeedback.blockReason}`, true);
                        } else {
                            const errorText = "Error: Received an unexpected response format from Gemini.";
                            setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
                            updateStatus("Could not extract summary from Gemini response.", true);
                            console.error(SCRIPT_PREFIX + "Gemini response format error:", data);
                        }
                    } catch (e) {
                        const errorText = "Error: Could not parse the response from Gemini.";
                        setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
                        updateStatus("Error parsing Gemini response: " + e.message, true);
                        console.error(SCRIPT_PREFIX + "Error parsing Gemini response:", e, response.responseText);
                    }
                } else {
                    let errorMsg = `Error from Gemini API: ${response.status}`;
                     try {
                        const errorData = JSON.parse(response.responseText);
                        if (errorData.error && errorData.error.message) {
                            errorMsg += ` - ${errorData.error.message}`;
                        }
                    } catch (e) { /* ignore */ }
                    setSummaryTextAndDirection(`Error: Gemini API returned status ${response.status}.`, DEFAULT_LANGUAGE);
                    updateStatus(errorMsg, true);
                    console.error(SCRIPT_PREFIX + "Error from Gemini API:", response);
                }
            },
            onerror: function(response) {
                const errorText = "Error: Network problem while contacting Gemini API.";
                setSummaryTextAndDirection(errorText, DEFAULT_LANGUAGE);
                updateStatus("Network error during summarization: " + response.statusText, true);
                console.error(SCRIPT_PREFIX + "Network error during summarization:", response);
            }
        });
    }

    async function getYouTubeTranscript() {
        updateStatus("Attempting to fetch transcript...", false);
        try {
            // Try to find the three-dots menu or settings button
            const threeDotsButton = document.querySelector('button#button.style-scope.ytd-menu-renderer[aria-label="More actions"], button.ytp-button[title="Settings"],  button[aria-label*="More actions" i][aria-haspopup="true"]');
            if (threeDotsButton) {
                threeDotsButton.click();
                await new Promise(resolve => setTimeout(resolve, 600)); // Wait for menu to appear
            } else {
                console.warn(SCRIPT_PREFIX + "Three dots/Settings button not found initially.");
            }

            // Try to find "Show transcript" or similar button in the menu
            let transcriptButton = Array.from(document.querySelectorAll('yt-formatted-string, ytd-menu-service-item-renderer, tp-yt-paper-item, div.ytp-menuitem-label'))
                                         .find(el => {
                                             const text = el.textContent?.trim().toLowerCase();
                                             // More robust matching for different languages and phrasings
                                             return text === 'show transcript' || text === 'показать расшифровку видео' || text === 'расшифровка' || text === 'transkript anzeigen' || text === 'mostrar transcripción';
                                         });
            if (transcriptButton) {
                 // Ensure it's a clickable element within a popup/menu context
                 if (transcriptButton.closest('ytd-menu-popup-renderer') || transcriptButton.closest('.ytp-popup') || transcriptButton.closest('tp-yt-paper-listbox')) {
                    transcriptButton.click();
                 } else {
                    // If not in a typical popup, try clicking anyway if it's visible.
                    // This might be the case if YouTube changes structure slightly.
                    if(transcriptButton.offsetParent !== null) transcriptButton.click();
                    else console.warn(SCRIPT_PREFIX + "Transcript button found but not in expected parent, or not visible.");
                 }
            } else {
                // Fallback: try to find a direct transcript button in description (less common for video watch page)
                const directTranscriptButton = document.querySelector('#description-inline-expander ytd-structured-description-content-renderer [aria-label*="transcript"], #description ytd-text-inline-expander [aria-label*="transcript"]');
                if(directTranscriptButton) {
                    directTranscriptButton.click();
                } else {
                    // Check if transcript panel is already open
                    const existingPanel = document.querySelector("ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript'], ytd-transcript-renderer, .ytd-transcript-segment-list-renderer");
                    if (!existingPanel) {
                        updateStatus("Show transcript button/panel not found or could not be opened.", true);
                        return getTranscriptFromWindow(); // Fallback to window method
                    } else {
                         updateStatus("Transcript panel might be already open or button was not found.", false);
                    }
                }
            }

            updateStatus("Transcript panel requested or already open. Waiting for content...", false);
            await new Promise(resolve => setTimeout(resolve, 2500)); // Wait for panel to load

            const transcriptPanelSelectors = [
                "ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript']",
                "ytd-transcript-renderer", // Generic renderer
                ".ytd-transcript-segment-list-renderer", // Segment list
                "div#transcript.ytd-item-section-renderer",
                "ytd-transcript-body-renderer"
            ];
            let transcriptPanelElement;
            for (const selector of transcriptPanelSelectors) {
                transcriptPanelElement = document.querySelector(selector);
                if (transcriptPanelElement) break;
            }

            if (!transcriptPanelElement) {
                 updateStatus("Transcript panel DOM element not found after click/check.", true);
                 return getTranscriptFromWindow(); // Fallback
            }

            const segmentSelectors = [
                'ytd-transcript-segment-renderer .segment-text',
                '.ytd-transcript-body-renderer .cue-group .cue',
                'yt-formatted-string.ytd-transcript-segment-renderer',
                'div[role="button"].ytd-transcript-segment-renderer div.text',
                'yt-formatted-string.ytd-transcript-cue-group-renderer',
                '.cue.ytd-transcript-body-renderer' // Simpler selector for cue text
            ];
            let segments;
            for (const selector of segmentSelectors) {
                segments = transcriptPanelElement.querySelectorAll(selector);
                if (segments && segments.length > 0) break;
            }

            let fullTranscript = "";
            if (segments && segments.length > 0) {
                segments.forEach(segment => {
                    fullTranscript += (segment.textContent || "").trim().replace(/\s+/g, ' ') + " ";
                });
            }

            if (fullTranscript.trim()) {
                updateStatus("Transcript extracted from panel.", false);
                // Attempt to close transcript panel or menu
                try {
                    const closeButtonSelectors = [
                        '#engagement-panel-searchable-transcript button[aria-label*="Close" i]', // More generic close
                        'ytd-engagement-panel-title-header-renderer button[aria-label*="Close" i]',
                        'button.ytp-settings-button[title="Settings"][aria-expanded="true"]',
                        'ytd-transcript-renderer #header button[aria-label*="Close" i]',
                        'ytd-popup-container tp-yt-paper-dialog #header #close-button',
                        'button[aria-label*="Close transcript" i]' // Case-insensitive
                    ];
                    let closeButton;
                    for(const selector of closeButtonSelectors){
                        closeButton = document.querySelector(selector);
                        if(closeButton && closeButton.offsetParent !== null) break;
                        else closeButton = null;
                    }

                    if (closeButton) {
                         // console.log(SCRIPT_PREFIX + "Attempting to close transcript panel with button:", closeButton);
                         closeButton.click();
                    } else if (threeDotsButton && threeDotsButton.getAttribute('aria-expanded') === 'true') {
                        // console.log(SCRIPT_PREFIX + "Attempting to close three-dots menu (fallback).");
                        threeDotsButton.click();
                    }
                } catch(e) { console.warn(SCRIPT_PREFIX + "Could not close transcript panel/menu cleanly:", e); }
                return fullTranscript.trim();
            } else {
                 updateStatus("Transcript panel/segments found, but no text extracted.", true);
                 return getTranscriptFromWindow(); // Fallback
            }
        } catch (error) {
            updateStatus("Error interacting with transcript UI: " + error.message, true);
            console.error(SCRIPT_PREFIX + "Error in getYouTubeTranscript (UI interaction):", error);
            return getTranscriptFromWindow(); // Fallback on any UI error
        }
    }

    function getTranscriptFromWindow() {
        updateStatus("Attempting fallback transcript extraction (window.ytInitialPlayerResponse)...", false);
        try {
            const playerResponse = window.ytInitialPlayerResponse || (window.ytplayer && window.ytplayer.config && window.ytplayer.config.args && window.ytplayer.config.args.player_response && JSON.parse(window.ytplayer.config.args.player_response));

            if (playerResponse && playerResponse.captions &&
                playerResponse.captions.playerCaptionsTracklistRenderer) {
                const tracks = playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks;
                if (tracks && tracks.length > 0) {
                    let chosenTrack = tracks.find(t => t.kind === 'asr' && (t.languageCode === 'en' || t.languageCode.startsWith('en-'))) ||
                                      tracks.find(t => t.kind === 'asr') ||
                                      tracks.find(t => (t.languageCode === 'en' || t.languageCode.startsWith('en-'))) ||
                                      tracks[0];

                    if (chosenTrack && chosenTrack.baseUrl) {
                        updateStatus(`Found caption track: ${chosenTrack.name ? (chosenTrack.name.simpleText || JSON.stringify(chosenTrack.name)) : 'Unknown'} (${chosenTrack.languageCode}). Fetching...`, false);
                        return new Promise((resolve) => {
                            GM_xmlhttpRequest({
                                method: "GET",
                                url: chosenTrack.baseUrl + "&fmt=json3",
                                onload: function(response) {
                                    if (response.status === 200) {
                                        try {
                                            const captionData = JSON.parse(response.responseText);
                                            let transcriptText = "";
                                            if (captionData.events) {
                                                captionData.events.forEach(event => {
                                                    if (event.segs) {
                                                        event.segs.forEach(seg => {
                                                            transcriptText += seg.utf8.replace(/\n/g, ' ').trim() + " ";
                                                        });
                                                    }
                                                });
                                            }
                                            if (transcriptText.trim()) {
                                                updateStatus("Transcript extracted from caption track.", false);
                                                resolve(transcriptText.trim());
                                            } else {
                                                updateStatus("Caption track fetched but no text found.", true);
                                                resolve(null);
                                            }
                                        } catch (e) {
                                            updateStatus("Error parsing caption data: " + e.message, true);
                                            console.error(SCRIPT_PREFIX + "Error parsing caption data:", e);
                                            resolve(null);
                                        }
                                    } else {
                                        updateStatus(`Error fetching caption track: ${response.status}`, true);
                                        resolve(null);
                                    }
                                },
                                onerror: function() {
                                    updateStatus("Network error fetching caption track.", true);
                                    resolve(null);
                                }
                            });
                        });
                    }
                }
            }
            updateStatus("No suitable caption tracks found in player response.", true);
            return Promise.resolve(null);
        } catch (e) {
            updateStatus("Error accessing player response for transcripts: " + e.message, true);
            console.error(SCRIPT_PREFIX + "Error in getTranscriptFromWindow:", e);
            return Promise.resolve(null);
        }
    }

    function runDiagnosticChecks() {
        // Diagnostics can be verbose, enable if needed by uncommenting console.logs
        if (!playerContainer || !togglePanelButton) {
            return;
        }
        // const currentMoviePlayer = document.getElementById('movie_player');
        // console.log(SCRIPT_PREFIX + "DIAG --- Interval Check ---");
        // console.log(SCRIPT_PREFIX + `DIAG: playerContainer.isConnected: ${playerContainer.isConnected}`);
        // console.log(SCRIPT_PREFIX + `DIAG: current #movie_player === initial playerContainer: ${currentMoviePlayer === playerContainer}`);
        // if(currentMoviePlayer !== playerContainer && currentMoviePlayer) {
        //     console.warn(SCRIPT_PREFIX + `DIAG: #movie_player element seems to have been REPLACED!`);
        // }
        // try {
        //     const rect = playerContainer.getBoundingClientRect();
        //     console.log(SCRIPT_PREFIX + `DIAG: playerContainer rect (isActive: ${playerContainer.isConnected}): w=${rect.width.toFixed(0)}, h=${rect.height.toFixed(0)}, top=${rect.top.toFixed(0)}, left=${rect.left.toFixed(0)}`);
        // } catch (e) { /* ignore if disconnected */ }
        // console.log(SCRIPT_PREFIX + `DIAG: toggleButton opacity: ${togglePanelButton.style.opacity}, pointerEvents: ${togglePanelButton.style.pointerEvents}`);
        // console.log(SCRIPT_PREFIX + "DIAG --- End Interval Check ---");
    }

    function handleGlobalMouseOver(event) {
        if (playerContainer && (event.target === playerContainer || playerContainer.contains(event.target))) {
            showToggleButton();
            startHideButtonTimer();
        }
    }

    function handleGlobalMouseOut(event) {
        if (playerContainer && (event.target === playerContainer || playerContainer.contains(event.target))) {
            const isLeavingToActiveElements = (panel && panel.contains(event.relatedTarget)) ||
                                              (togglePanelButton && togglePanelButton.contains(event.relatedTarget)) ||
                                              (playerContainer && playerContainer.contains(event.relatedTarget));

            if (!isLeavingToActiveElements) {
                startHideButtonTimer(HIDE_BUTTON_DELAY_FAST);
            }
        }
    }


    function init() {
        console.log(SCRIPT_PREFIX + "Attempting to initialize script...");
        loadSettings();

        document.body.addEventListener('mouseover', handleGlobalMouseOver, true);
        document.body.addEventListener('mouseout', handleGlobalMouseOut, true);
        // console.log(SCRIPT_PREFIX + "Global mouseover/mouseout listeners ADDED to document.body (capture).");

        const playerCheckInterval = setInterval(function() {
            playerContainer = document.getElementById('movie_player') ||
                              document.querySelector('.html5-video-player') ||
                              document.querySelector('ytd-player');

            if (playerContainer && (document.readyState === 'interactive' || document.readyState === 'complete')) {
                clearInterval(playerCheckInterval);
                console.log(SCRIPT_PREFIX + "Player container found.");

                addStyles();
                createGUI();
                loadSettings(); // Ensure selects are populated correctly
                createToggleButton();

                if (togglePanelButton && playerContainer) {
                    const currentPosition = window.getComputedStyle(playerContainer).position;
                    if (currentPosition === 'static' || !currentPosition) {
                         playerContainer.classList.add('relative-positioning');
                    }
                    playerContainer.appendChild(togglePanelButton);
                    // console.log(SCRIPT_PREFIX + "Toggle button appended.");

                    const buttonPanelMouseOverHandler = (event) => {
                        showToggleButton();
                        event.stopPropagation();
                    };
                    togglePanelButton.addEventListener('mouseover', buttonPanelMouseOverHandler);
                    if (panel) panel.addEventListener('mouseover', buttonPanelMouseOverHandler);

                    const buttonPanelMouseLeaveHandler = (event) => {
                         if (playerContainer && !playerContainer.contains(event.relatedTarget) &&
                             togglePanelButton && !togglePanelButton.contains(event.relatedTarget) &&
                             panel && !panel.contains(event.relatedTarget)) {
                            startHideButtonTimer(HIDE_BUTTON_DELAY_FAST);
                         }
                    };
                    togglePanelButton.addEventListener('mouseleave', buttonPanelMouseLeaveHandler);
                    if (panel) panel.addEventListener('mouseleave', buttonPanelMouseLeaveHandler);

                } else {
                    console.error(SCRIPT_PREFIX + "togglePanelButton or playerContainer not available for event setup.");
                }

                if (apiKeyInput && apiKey) {
                    apiKeyInput.value = apiKey;
                    fetchAndPopulateModels();
                } else {
                    populateModelDropdown([]);
                }
                updateStatus("Summarizer ready. " + (isPanelVisible ? "Panel is visible." : ""), false);

                if (diagnosticIntervalId) clearInterval(diagnosticIntervalId);
                diagnosticIntervalId = setInterval(runDiagnosticChecks, DIAGNOSTIC_INTERVAL_MS);

            }
        }, 1000);

        setTimeout(() => {
            if (!document.getElementById(SCRIPT_PREFIX + 'panel')) {
                clearInterval(playerCheckInterval);
                if (diagnosticIntervalId) clearInterval(diagnosticIntervalId);
                document.body.removeEventListener('mouseover', handleGlobalMouseOver, true);
                document.body.removeEventListener('mouseout', handleGlobalMouseOut, true);
                console.warn(SCRIPT_PREFIX + "Fallback init triggered. Main init likely failed.");

                 playerContainer = document.getElementById('movie_player') || document.querySelector('.html5-video-player') || document.querySelector('ytd-player');
                 if (playerContainer && !document.getElementById(SCRIPT_PREFIX + 'toggle_button')) {
                    console.log(SCRIPT_PREFIX + "FALLBACK: Player found, attempting to setup GUI.");
                    addStyles(); createGUI(); loadSettings(); createToggleButton();
                    if (togglePanelButton) {
                        const currentPosition = window.getComputedStyle(playerContainer).position;
                        if (currentPosition === 'static' || !currentPosition) playerContainer.classList.add('relative-positioning');
                        playerContainer.appendChild(togglePanelButton);
                        playerContainer.addEventListener('mouseover', () => { showToggleButton(); startHideButtonTimer(); }, false);
                        playerContainer.addEventListener('mouseout', () => { startHideButtonTimer(HIDE_BUTTON_DELAY_FAST); }, false);
                        togglePanelButton.addEventListener('mouseover', () => { showToggleButton(); });
                        if(panel) panel.addEventListener('mouseover', () => { showToggleButton(); });


                        if(isPanelVisible) showToggleButton(); else hideToggleButton();
                    }
                    if (apiKeyInput && apiKey) { apiKeyInput.value = apiKey; fetchAndPopulateModels(); }
                    else { populateModelDropdown([]); }
                    updateStatus("Summarizer ready (Fallback Init). " + (isPanelVisible ? "Panel is visible." : ""), false);
                 } else if (!playerContainer) {
                    console.error(SCRIPT_PREFIX + "FALLBACK: Player container NOT FOUND.");
                 }
            }
        }, 15000);
    }

    function main() {
        init();
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        main();
    } else {
        window.addEventListener('DOMContentLoaded', main, { once: true }); // Changed to DOMContentLoaded for slightly earlier execution
    }

})();