微信公众号文章朗读助手

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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 });

})();