GPT4Free Page Summarizer (Local Free API)

Generate a summary of any webpage, selected text, YouTube video transcript using a local instance of g4f

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

// ==UserScript==
// @name         GPT4Free Page Summarizer (Local Free API)
// @version      1.6
// @description  Generate a summary of any webpage, selected text, YouTube video transcript using a local instance of g4f
// @author       SH3LL
// @match        *://*/*
// @grant        GM.xmlHttpRequest
// @run-at       document-end
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    let loading = false;

    function summarizePage(textToSummarize, language, maxWords, callback) {
        const apiUrl = 'http://localhost:1337/v1/chat/completions';
        const prompt = `Summarize the following text in ${language} using max ${maxWords} words.
                                         Summary is organised in paragraphs.
                                         Each paragraph is preceded by a title (inside by <b></b>).
                                         Each paragraph is it's followed by a <br> at the end of pagragraph (never at the beginning).
                                         Before each title (in the same line of the title) there si an emoji contextual to the paragraph topic.
                                         Don't add any other additional blank space or blank newline before or after each paragraph.
                                         Don't add any other sentence like "Here is the summary", write directly the summary itself.
                                         Exclude from the summary any advertisement or sponsorization.
                                         Here is the text: ${textToSummarize}`;
        const data = {
            messages: [{
                role: 'user',
                content: prompt
            }],
            model: 'DeepSeek-V3',
            provider: 'Blackbox'
        };

        GM.xmlHttpRequest({
            method: 'POST',
            url: apiUrl,
            headers: {
                'Content-Type': 'application/json'
            },
            data: JSON.stringify(data),
            onload: function(response) {
                let statusColor = '#00ff00'; // Green for success (2xx)
                if (response.status < 200 || response.status >= 300) {
                    statusColor = '#ffcc00'; // Yellow for non-2xx
                }
                callback(null, response.responseText, response.status, statusColor);
                loading = false;
                updateButtonState();
                updateButtonText();
            },
            onerror: function(error) {
                callback('Network error during the API call: ' + error, null, null, '#ff4444'); // Red for error
                loading = false;
                updateButtonState();
                updateButtonText();
            }
        });
        loading = true;
        updateButtonState();
        updateButtonText();
    }

    function getYouTubeTranscript() {
        return new Promise((resolve, reject) => {
            const TRANSCRIPT_TARGET_ID = "engagement-panel-searchable-transcript";
            const VISIBILITY_EXPANDED = "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED";
            const VISIBILITY_HIDDEN = "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN";
            const TRANSCRIPT_CONTENT_SELECTOR = `ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"] #content`;
            const SEGMENT_SELECTOR = '.segment-text';

            let transcriptPanelElement = null;

            function hideTranscriptPanel() {
                if (transcriptPanelElement) {
                    transcriptPanelElement.setAttribute("visibility", VISIBILITY_HIDDEN);
                    transcriptPanelElement = null; // Release the reference
                }
            }

            function extractTranscript() {
                const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
                if (transcriptPanelContent) {
                    let transcriptText = "";
                    transcriptPanelContent.querySelectorAll(SEGMENT_SELECTOR).forEach(segment => {
                        transcriptText += segment.textContent.trim() + " ";
                    });
                    hideTranscriptPanel();
                    resolve(transcriptText.trim());
                } else {
                    reject("Transcript panel content not found.");
                }
            }

            function onTranscriptPanelVisible() {
                const transcriptContentObserver = new MutationObserver((mutationsList, observer) => {
                    const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
                    if (transcriptPanelContent && transcriptPanelContent.children.length > 0) {
                        extractTranscript();
                        observer.disconnect(); // Stop observing once extracted
                    }
                });

                const transcriptPanel = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
                const contentTarget = transcriptPanel ? transcriptPanel.querySelector('#content') : null;
                const config = { childList: true, subtree: true }; // Observe for addition of child nodes
                if (contentTarget) {
                    transcriptContentObserver.observe(contentTarget, config);
                } else {
                    reject("Could not find the #content element within the transcript panel.");
                }
            }

            function showTranscriptPanelAndObserveContent() {
                const transcripts = document.querySelectorAll(`[target-id="${TRANSCRIPT_TARGET_ID}"]`);
                if (transcripts.length === 1) {
                    transcriptPanelElement = transcripts[0]; // Store panel element
                    transcriptPanelElement.setAttribute("visibility", VISIBILITY_EXPANDED);
                    onTranscriptPanelVisible();
                } else {
                    reject('Transcript panel element not found.');
                }
            }

            const panelObserver = new MutationObserver((mutationsList, observer) => {
                const transcriptPanelContainer = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
                if (transcriptPanelContainer) {
                    showTranscriptPanelAndObserveContent();
                    observer.disconnect(); // Stop observing once found
                }
            });

            // Start observing for the transcript panel container
            const targetNode = document.body;
            const config = { childList: true, subtree: true };
            panelObserver.observe(targetNode, config);

            // Set a timeout in case the transcript panel doesn't load
            setTimeout(() => {
                if (!transcriptPanelElement && !document.querySelector(TRANSCRIPT_CONTENT_SELECTOR)) {
                    panelObserver.disconnect(); // Ensure observer is stopped
                    reject("Timeout: Could not find transcript panel.");
                }
            }, 10000); // Adjust timeout as needed
        });
    }

    // GET LANGUAGE PAGE
    const browserLanguage = navigator.language;
    let selectedLanguage;
    try {
        const languageNames = new Intl.DisplayNames([browserLanguage], { type: 'language' });
        selectedLanguage = languageNames.of(browserLanguage);
        // Fallback to the raw language code if Intl.DisplayNames fails or returns undefined
        if (!selectedLanguage) {
            selectedLanguage = browserLanguage;
        }
    } catch (error) {
        console.error("Error initializing Intl.DisplayNames:", error);
        selectedLanguage = browserLanguage; // Fallback to raw language code
    }

    // GET YOUTUBE TRANSCRIPT
    let ytTranscript = null;
    const isYouTubeVideoPage = window.location.hostname.includes("youtube.com") && window.location.pathname === "/watch";
    if (isYouTubeVideoPage) {
        getYouTubeTranscript()
            .then(transcript => {
                ytTranscript = transcript;
                console.log("YOUTUBE Transcript:", transcript);
                updateButtonText(); // Update button text when transcript is available
            })
            .catch(error => {
                console.error("Error getting YOUTUBE transcript:", error);
            });
    }

    // CREATE DOM CONTAINER
    const shadowHost = document.createElement('div');
    document.body.appendChild(shadowHost);
    const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

    // CREATE SIDEBAR DOM
    const sidebar = document.createElement('div');
    sidebar.style.position = 'fixed';
    sidebar.style.right = '-300px';
    sidebar.style.top = '0';
    sidebar.style.width = '300px';
    sidebar.style.height = '100vh';
    sidebar.style.backgroundColor = '#000000';
    sidebar.style.color = '#ffffff';
    sidebar.style.padding = '20px';
    sidebar.style.zIndex = '999999'; // Increased to ensure it's above all elements
    sidebar.style.fontFamily = 'Arial, sans-serif';
    sidebar.style.boxSizing = 'border-box';
    sidebar.style.display = 'flex';
    sidebar.style.flexDirection = 'column';
    sidebar.style.gap = '10px';
    sidebar.style.transition = 'right 0.3s ease';
    shadowRoot.appendChild(sidebar);

    // CREATE BUTTON HIDE/SHOW SIDEBAR
    const toggleButton = document.createElement('button');
    toggleButton.textContent = '<';
    toggleButton.style.position = 'fixed';
    toggleButton.style.right = '0';
    toggleButton.style.top = '20px';
    toggleButton.style.backgroundColor = '#FFC107'; // Mustard yellow
    toggleButton.style.color = '#000000'; // White text
    toggleButton.style.border = 'none';
    toggleButton.style.padding = '10px';
    toggleButton.style.cursor = 'pointer';
    toggleButton.style.zIndex = '1000000'; // Higher than sidebar
    toggleButton.style.fontSize = '14px';
    toggleButton.style.transition = 'right 0.3s ease';
    shadowRoot.appendChild(toggleButton);

    // CREATE CONTAINER FOR BUTTON AND SELECTOR
    const buttonContainer = document.createElement('div');
    buttonContainer.style.display = 'flex';
    buttonContainer.style.gap = '10px';
    buttonContainer.style.alignItems = 'center';
    sidebar.appendChild(buttonContainer);

    // CREATE SUMMARIZE BUTTON
    const summarizeButton = document.createElement('button');
    summarizeButton.style.backgroundColor = '#333333';
    summarizeButton.style.color = '#ffffff';
    summarizeButton.style.border = 'none';
    summarizeButton.style.padding = '10px 20px';
    summarizeButton.style.cursor = 'pointer';
    summarizeButton.style.fontSize = '14px';
    summarizeButton.style.flex = '1';
    summarizeButton.style.transition = 'background-color 0.3s';
    summarizeButton.onmouseover = () => summarizeButton.style.backgroundColor = '#4d4d4d';
    summarizeButton.onmouseout = () => summarizeButton.style.backgroundColor = '#333333';
    buttonContainer.appendChild(summarizeButton);

    // Create the word number selector
    const wordsSelector = document.createElement('select');
    wordsSelector.style.backgroundColor = '#333333';
    wordsSelector.style.color = '#ffffff';
    wordsSelector.style.border = 'none';
    wordsSelector.style.padding = '10px';
    wordsSelector.style.fontSize = '14px';
    wordsSelector.style.cursor = 'pointer';
    const wordOptions = [50, 100, 200, 300];
    wordOptions.forEach(words => {
        const option = document.createElement('option');
        option.value = words;
        option.textContent = `${words} words`;
        if (words === 50) option.selected = true;
        wordsSelector.appendChild(option);
    });
    buttonContainer.appendChild(wordsSelector);

    // Create the API status display
    const statusDisplay = document.createElement('div');
    statusDisplay.style.fontSize = '12px';
    sidebar.appendChild(statusDisplay);

    // Create the summary container
    const summaryContainer = document.createElement('div');
    summaryContainer.style.fontSize = '14px';
    summaryContainer.style.lineHeight = '1.5';
    summaryContainer.style.display = 'none';
    summaryContainer.style.overflowY = 'auto';
    summaryContainer.style.maxHeight = 'calc(100vh - 130px)';
    sidebar.appendChild(summaryContainer);

    // Variable to track if text is selected
    let isTextSelected = false;

    // Function to update the button text based on text selection and loading state
    function updateButtonText() {
        if (loading) {
            summarizeButton.textContent = 'Loading..';
            return;
        }

        const selectedText = window.getSelection().toString();
        if (selectedText) {
            summarizeButton.textContent = `Summarize [${selectedText.substring(0, 2).trim().replace(/^\n+/, '').trim()}..]`;
            summarizeButton.style.fontSize = "14px";
            isTextSelected = true;
        } else if (isYouTubeVideoPage && ytTranscript !== null) {
            summarizeButton.textContent = 'Summarize YT';
            summarizeButton.style.fontSize = "14px";
            isTextSelected = false;
        } else {
            summarizeButton.textContent = 'Summarize';
            summarizeButton.style.fontSize = "14px";
            isTextSelected = false;
        }
    }

    // Function to update the status display with colored language and response status
    function updateStatusDisplay(statusText, responseColor) {
        statusDisplay.innerHTML = `Status: <span style="color: ${responseColor};">${statusText}</span> | Lang: <span style="color: #00bfff;">${selectedLanguage}</span>`;
    }

    // Function to update the button's disabled state
    function updateButtonState() {
        summarizeButton.disabled = loading;
    }

    // Listen for text selection changes on the document
    document.addEventListener('mouseup', updateButtonText);
    document.addEventListener('mousedown', () => {
        setTimeout(updateButtonText, 100);
    });

    // Handle sidebar visibility
    let isSidebarVisible = false;
    toggleButton.addEventListener('click', function() {
        if (isSidebarVisible) {
            sidebar.style.right = '-300px';
            toggleButton.style.right = '0';
            toggleButton.textContent = '<';
        } else {
            sidebar.style.right = '0';
            toggleButton.style.right = '300px';
            toggleButton.textContent = '>';
        }
        isSidebarVisible = !isSidebarVisible;
    });

    // Listener for the summarize button
    updateStatusDisplay('Idle', '#888888');
    summarizeButton.addEventListener('click', function() {
        if (loading) {
            return; // Do nothing if already loading
        }

        updateButtonText(); // Immediately set button to "Loading.."
        updateButtonState();
        updateStatusDisplay('Requesting..', '#888888');
        summaryContainer.style.display = 'none';
        summaryContainer.style.whiteSpace = 'pre-line';

        let textToSummarize = '';
        if (isYouTubeVideoPage && !isTextSelected && ytTranscript) {
            textToSummarize = ytTranscript;
            console.log("Summarizing YouTube transcript..");
        } else if (isTextSelected) {
            textToSummarize = window.getSelection().toString();
            console.log("Summarizing selected text..");
        } else {
            textToSummarize = document.body.innerText;
            console.log("Summarizing page text..");
        }

        const maxWords = parseInt(wordsSelector.value, 10);
        summarizePage(textToSummarize, selectedLanguage, maxWords, function(error, summaryText, statusCode, statusColor) {
            if (error) {
                summaryContainer.textContent = 'Error: ' + error;
                summaryContainer.style.color = '#ff4444';
                updateStatusDisplay(`Failed${statusCode ? ` (${statusCode})` : ''}`, '#ff4444');
            } else {
                try {
                    const jsonResponse = JSON.parse(summaryText);
                    if (jsonResponse.choices && jsonResponse.choices[0].message && jsonResponse.choices[0].message.content) {
                        summaryContainer.innerHTML = jsonResponse.choices[0].message.content;
                        summaryContainer.style.color = '#ffffff';
                        updateStatusDisplay(`Success (${statusCode})`, statusColor);
                    } else {
                        summaryContainer.textContent = 'Error: Formato della risposta API inatteso.';
                        summaryContainer.style.color = '#ff4444';
                        updateStatusDisplay(`Failed (${statusCode})`, '#ff4444');
                    }
                } catch (parseError) {
                    summaryContainer.textContent = 'Error parsing JSON: ' + parseError;
                    summaryContainer.style.color = '#ff4444';
                    updateStatusDisplay(`Failed (${statusCode})`, '#ff4444');
                }
            }
            summaryContainer.style.display = 'block';
        });
    });

    // Initial button text update
    updateButtonText();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址