微信公众号文章朗读助手

修复了按钮不加载的问题,并优化文本分段逻辑,解决了朗读卡顿、不连贯的问题。

// ==UserScript==
// @name         微信公众号文章朗读助手
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  修复了按钮不加载的问题,并优化文本分段逻辑,解决了朗读卡顿、不连贯的问题。
// @author       Gemini & User
// @match        https://mp.weixin.qq.com/*
// @license      MPL-2.0 License
// @grant        GM_addStyle
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzQyYjM2YSIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0zIDl2Nmg0bDUgNVY0TDcgOWg0em0xMy41IDNjMC0xLjc3LTEuMDItMy4yOS0yLjUtNC4wM3Y4LjA1YzEuNDgtLjczIDIuNS0yLjI1IDIuNS00LjAyek0xNCAzLjk4djIuMDZjMi44OS44NiA1IDEuNzEgNSA1Ljk2czEuMjggMy40NSA0IDQuM3YtMi4xYzAtMy44Ny0yLjQzLTYuNjYtNS03Ljl6Ii8+PC9zdmc+
// ==/UserScript==

(function() {
    'use strict';

    console.log('[朗读助手 v3.1] 脚本启动');

    // --- 配置项 ---
    const TARGET_VOICE_NAME = "Microsoft Yunxi Online (Natural) - Chinese (Mainland)"; // 优先选择更自然的云端语音
    const CHUNK_MAX_LENGTH = 180; // 设置每个朗读片段的最大长度,平衡流畅性与稳定性
    const PLAY_ICON_SVG = `<svg viewBox="0 0 1024 1024" width="14" height="14" style="vertical-align: middle; fill: currentColor;"><path d="M192 128l640 384-640 384z"></path></svg>`;
    const PAUSE_ICON_SVG = `<svg viewBox="0 0 1024 1024" width="14" height="14" style="vertical-align: middle; fill: currentColor;"><path d="M320 128h128v768H320zM576 128h128v768H576z"></path></svg>`;
    const BUTTON_TEXT_PLAY = "朗读";
    const BUTTON_TEXT_PAUSE = "暂停";
    const BUTTON_TEXT_RESUME = "继续";

    let speechState = 'idle'; // 'idle', 'playing', 'paused'
    let speechAPI = window.speechSynthesis;
    let targetVoice = null;
    let speechUtteranceChunks = [];
    let currentChunkIndex = 0;

    /**
     * 使用轮询来等待目标元素出现
     */
    function waitForElement(selector, callback) {
        let interval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                console.log(`[朗读助手 v3.1] 成功找到目标元素: ${selector}`);
                clearInterval(interval);
                callback(element);
            }
        }, 250); // 每250毫秒检查一次

        // 15秒后如果还没找到,就超时放弃
        setTimeout(() => {
            if (interval) {
                clearInterval(interval);
                console.warn(`[朗读助手 v3.1] 超时:未找到元素 ${selector}`);
            }
        }, 15000);
    }

    /**
     * 主初始化函数
     */
    function initializeReader(targetContainer) {
        if (document.getElementById('custom-read-aloud-button')) {
            console.log('[朗读助手 v3.1] 按钮已存在,跳过初始化。');
            return;
        }
        console.log('[朗读助手 v3.1] 开始初始化朗读模块...');

        if (!('speechSynthesis' in window)) {
            console.error("[朗读助手 v3.1] 浏览器不支持 Web Speech API。");
            return;
        }

        // 页面加载时可能语音列表未准备好,需要等待
        populateVoiceList().then(() => {
            if (speechAPI.onvoiceschanged !== undefined) {
                speechAPI.onvoiceschanged = () => populateVoiceList();
            }
            createReadAloudButton(targetContainer);
        });
    }

    function populateVoiceList() {
        return new Promise((resolve) => {
            let voices = speechAPI.getVoices();
            if (voices.length !== 0) {
                targetVoice = voices.find(voice => voice.name === TARGET_VOICE_NAME) || voices.find(voice => /zh|chinese/i.test(voice.lang) && /natural/i.test(voice.name)) || voices.find(voice => /zh|chinese/i.test(voice.lang));
                if (targetVoice) {
                    console.log(`[朗读助手 v3.1] 已选择语音: ${targetVoice.name}`);
                } else {
                    console.warn('[朗读助手 v3.1] 未找到理想的中文语音。');
                }
                resolve();
            } else {
                speechAPI.onvoiceschanged = () => {
                    voices = speechAPI.getVoices();
                    targetVoice = voices.find(voice => voice.name === TARGET_VOICE_NAME) || voices.find(voice => /zh|chinese/i.test(voice.lang) && /natural/i.test(voice.name)) || voices.find(voice => /zh|chinese/i.test(voice.lang));
                    if (targetVoice) {
                         console.log(`[朗读助手 v3.1] 已选择语音 (onvoiceschanged): ${targetVoice.name}`);
                    } else {
                        console.warn('[朗读助手 v3.1] 未找到理想的中文语音。');
                    }
                    resolve();
                };
            }
        });
    }

    function createReadAloudButton(container) {
        const readButton = document.createElement('span');
        readButton.id = 'custom-read-aloud-button';

        GM_addStyle(`
            #custom-read-aloud-button {
                display: inline-flex; align-items: center; gap: 4px;
                margin-left: 16px; padding: 2px 8px; border-radius: 12px;
                background-color: #f0f0f0; color: #555; cursor: pointer;
                font-size: 14px; transition: all 0.2s ease-in-out; user-select: none;
            }
            #custom-read-aloud-button:hover { background-color: #e0e0e0; transform: scale(1.05); }
            #custom-read-aloud-button.speaking { background-color: #d4edda; color: #155724; }
            #custom-read-aloud-button.paused { background-color: #fff3cd; color: #856404; }
        `);

        readButton.addEventListener('click', mainControl);
        // 【修复】恢复到原版可靠的按钮注入方式
        container.insertAdjacentElement('afterend', readButton);
        console.log('[朗读助手 v3.1] 按钮DOM已注入。');
        updateButtonState(speechState);
    }

    function mainControl() {
        switch (speechState) {
            case 'idle':
                startReading();
                break;
            case 'playing':
                pauseReading();
                break;
            case 'paused':
                resumeReading();
                break;
        }
    }

    /**
     * 【核心优化】将文本分割成更长、更连贯的片段
     */
    function splitTextIntoChunks(text, maxLength) {
        const sentences = text.split(/([。!??!\n])/); // 按句末标点分割,并保留标点
        const chunks = [];
        let currentChunk = '';

        for (let i = 0; i < sentences.length; i += 2) {
            const sentence = sentences[i];
            const punctuation = sentences[i + 1] || '';
            const combined = sentence + punctuation;

            if (currentChunk.length + combined.length > maxLength && currentChunk.length > 0) {
                chunks.push(currentChunk.trim());
                currentChunk = '';
            }
            currentChunk += combined;
        }

        if (currentChunk.length > 0) {
            chunks.push(currentChunk.trim());
        }

        console.log(`[朗读助手 v3.1] 文本被分割成 ${chunks.length} 个片段。`);
        return chunks.filter(c => c); // 过滤空片段
    }


    function startReading() {
        const contentElement = document.getElementById('js_content');
        if (!contentElement || !contentElement.innerText.trim()) {
            alert("文章内容为空或无法找到。");
            return;
        }

        // 停止任何正在进行的朗读
        speechAPI.cancel();

        const textToRead = contentElement.innerText;
        // 【核心优化】使用新的文本分割函数
        const chunks = splitTextIntoChunks(textToRead, CHUNK_MAX_LENGTH);

        if (chunks.length === 0) {
            console.warn("[朗读助手 v3.1] 未能从文本中提取任何有效朗读片段。");
            return;
        }

        speechUtteranceChunks = chunks.map(chunk => {
            const utterance = new SpeechSynthesisUtterance(chunk);
            utterance.voice = targetVoice;
            utterance.lang = targetVoice ? targetVoice.lang : 'zh-CN';
            utterance.rate = 1.0; // 语速,可根据喜好调整
            return utterance;
        });

        currentChunkIndex = 0;
        speechState = 'playing';
        updateButtonState(speechState);
        playNextChunk();
    }

    function pauseReading() {
        speechAPI.pause();
        speechState = 'paused';
        updateButtonState(speechState);
    }

    function resumeReading() {
        speechAPI.resume();
        speechState = 'playing';
        updateButtonState(speechState);
    }

    function stopReading() {
        speechAPI.cancel();
        speechState = 'idle';
        currentChunkIndex = 0;
        speechUtteranceChunks = [];
        updateButtonState(speechState);
    }

    function playNextChunk() {
        if (currentChunkIndex >= speechUtteranceChunks.length) {
            console.log("[朗读助手 v3.1] 朗读完成。");
            stopReading();
            return;
        }

        // 当状态不是播放时(例如用户点击了暂停),则不继续下一段
        if (speechState !== 'playing') {
            return;
        }

        const utterance = speechUtteranceChunks[currentChunkIndex];

        utterance.onend = () => {
            // 确保是在播放状态下自然结束才进入下一段
            if (speechState === 'playing') {
                currentChunkIndex++;
                playNextChunk();
            }
        };

        utterance.onerror = (event) => {
            console.error('[朗读助手 v3.1] 朗读错误:', event.error);
            // 发生错误时停止,而不是继续尝试
            stopReading();
        };

        speechAPI.speak(utterance);
    }

    function updateButtonState(state) {
        const button = document.getElementById('custom-read-aloud-button');
        if (!button) {
            console.warn('[朗读助手 v3.1] 更新状态时未找到按钮。');
            return;
        }

        console.log(`[朗读助手 v3.1] 更新按钮状态为: ${state}`);
        button.classList.remove('speaking', 'paused');

        switch (state) {
            case 'playing':
                button.innerHTML = `${PAUSE_ICON_SVG} <span class="button-text">${BUTTON_TEXT_PAUSE}</span>`;
                button.classList.add('speaking');
                break;
            case 'paused':
                button.innerHTML = `${PLAY_ICON_SVG} <span class="button-text">${BUTTON_TEXT_RESUME}</span>`;
                button.classList.add('paused');
                break;
            case 'idle':
            default:
                button.innerHTML = `${PLAY_ICON_SVG} <span class="button-text">${BUTTON_TEXT_PLAY}</span>`;
                break;
        }
    }

    // --- 脚本执行入口 ---
    // 【修复】恢复到原版可靠的目标元素
    waitForElement('#meta_content_hide_info', initializeReader);

    // 增加一个监听器,当页面URL变化时(例如在公众号内跳转),重新初始化
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            // 停止之前的朗读
            if(speechState !== 'idle') {
                stopReading();
            }
            // 延迟一点等待新页面加载
            setTimeout(() => {
                 // 【修复】同样使用恢复后的可靠目标元素
                 waitForElement('#meta_content_hide_info', initializeReader);
            }, 500);
        }
    }).observe(document.body, { subtree: true, childList: true });

})();

QingJ © 2025

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