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暴力猴,之后才能安装此脚本。

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

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

您需要先安装一个扩展,例如 篡改猴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
    }

})();