🤖ChatGPT 朗读助手

在ChatGPT原生网页中添加朗读功能的脚本,可以让你听到聊天记录的文字内容。

目前為 2023-04-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         🤖ChatGPT 朗读助手
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  在ChatGPT原生网页中添加朗读功能的脚本,可以让你听到聊天记录的文字内容。
// @author       OpenAI - ChatGPT
// @match        https://chat.openai.com/*
// @license      GNU GPLv3
// ==/UserScript==

(function () {
    'use strict';
    const buttonStyles = {
        shared: {
            borderColor: 'rgba(86,88,105,var(--tw-border-opacity))',
            fontSize: '.875rem',
            lineHeight: '1.25rem',
            padding: '0.5rem 0.75rem',
            borderRadius: '0.25rem',
            marginLeft: '0.25rem',
            position: 'relative',
            bottom: '5px',
        },
        light: {
            backgroundColor: 'white',
            color: 'black',
        },
        dark: {
            backgroundColor: 'rgba(52,53,65,var(--tw-bg-opacity))',
            color: 'rgba(217,217,227,var(--tw-text-opacity))',
        },
    };

    const selectorStyles = {
        shared: {
            marginLeft: '0.25rem',
            position: 'relative',
            bottom: '5px',
            fontSize: '.875rem',
            padding: '0.1rem 0.5rem',
            borderRadius: '0.25rem',
        },
        light: {
            backgroundColor: 'white',
            color: 'black',
        },
        dark: {
            backgroundColor: 'rgba(52,53,65,var(--tw-bg-opacity))',
            color: 'rgba(217,217,227,var(--tw-text-opacity))',
        },
    };

    function isDarkMode() {
        return document.documentElement.classList.contains('dark');
    }

    function createButton() {
        const button = document.createElement('button');
        button.innerHTML = '朗读';
        button.title = '朗读';
        button.setAttribute('data-chatgpt', '');
        Object.assign(button.style, buttonStyles.shared, isDarkMode() ? buttonStyles.dark : buttonStyles.light);
        return button;
    }

    function createLanguageSelector() {
        const select = document.createElement('select');
        select.setAttribute('data-chatgpt', '');
        Object.assign(select.style, selectorStyles.shared, isDarkMode() ? selectorStyles.dark : selectorStyles.light);

        const voices = window.speechSynthesis.getVoices().filter(voice => voice.name.includes('Microsoft'));

        const voiceGroups = voices.reduce((groups, voice) => {
            const language = voice.lang.split('-')[0];
            if (!groups[language]) {
                groups[language] = [];
            }
            groups[language].push(voice);
            return groups;
        }, {});

        Object.keys(voiceGroups).forEach((language) => {
            const optgroup = document.createElement('optgroup');
            optgroup.label = language;
            voiceGroups[language].forEach((voice) => {
                const option = document.createElement('option');
                option.value = voice.name;
                option.text = voice.name;
                optgroup.appendChild(option);
            });
            select.appendChild(optgroup);
        });

        select.value = 'Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)';

        return select;
    }

    function addButtonAndSelector() {
        const elements = document.querySelectorAll('.markdown.prose');
        elements.forEach((elm) => {
            if (elm.nextElementSibling?.nodeName ==='SELECT') return;

            const button = createButton();
            const languageSelector = createLanguageSelector();

            button.addEventListener('mouseenter', () => {
                button.style.backgroundColor = buttonStyles.hover.backgroundColor;
            });

            button.addEventListener('mouseleave', () => {
                button.style.backgroundColor = buttonStyles.normal.backgroundColor;
            });

            button.addEventListener('click', () => {
                if (button.classList.contains('playing')) {
                    window.speechSynthesis.cancel();
                    button.innerHTML = '朗读';
                    button.classList.remove('playing');
                    button.disabled = false;
                    return;
                }

                button.classList.add('playing');
                button.innerHTML = '生成中请稍等...';
                button.disabled = true;

                const msg = new SpeechSynthesisUtterance(elm.textContent);
                msg.rate = 0.825;

                msg.addEventListener('boundary', (event) => {
                    const currentWord = elm.textContent.slice(event.charIndex, event.charIndex + event.charLength);
                    button.innerHTML = `朗读中: ${currentWord}`;
                    button.disabled = false;
                });

                msg.addEventListener('end', () => {
                    button.innerHTML = '朗读';
                    button.classList.remove('playing');
                    button.disabled = false;
                });

                msg.voice = speechSynthesis.getVoices().find(voice => voice.name === languageSelector.value);

                msg.onerror = (errorEvent) => {
                    if (errorEvent.error === 'interrupted') {
                        return;
                    }
                    const errorMsg = `发生错误: ${errorEvent.error}`;
                    button.innerHTML = `发生错误: ${errorEvent.error}`;
                    button.classList.remove('playing');
                    button.disabled = false;
                };

                window.speechSynthesis.speak(msg);
            });



            elm.parentNode.insertBefore(languageSelector, elm.nextSibling);
            elm.parentNode.insertBefore(button, languageSelector.nextSibling);
        });
    }

    function updateButtonAndSelectorStyles() {
        const buttons = document.querySelectorAll('button[data-chatgpt]');
        const selectors = document.querySelectorAll('select[data-chatgpt]');

        buttons.forEach((button) => {
            Object.assign(button.style, buttonStyles.shared, isDarkMode() ? buttonStyles.dark : buttonStyles.light);
        });

        selectors.forEach((select) => {
            Object.assign(select.style, selectorStyles.shared, isDarkMode() ? selectorStyles.dark : selectorStyles.light);
        });
    }

    window.addEventListener('keydown', (event) => {
        if (event.key === 'Escape') {
            window.speechSynthesis.cancel();
            const buttons = document.querySelectorAll('button.playing');
            buttons.forEach((button) => {
                button.innerHTML = '朗读';
                button.classList.remove('playing');
            });
        }
    });

    window.speechSynthesis.onvoiceschanged = () => {
        addButtonAndSelector();
    };

    setInterval(() => {
        addButtonAndSelector();
    }, 2000);

    const darkModeObserver = new MutationObserver(updateButtonAndSelectorStyles);
    darkModeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
})();

QingJ © 2025

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