Greasy Fork

来自缓存

Greasy Fork is available in English.

网页划词朗读

使用阿里云TTS朗读网页选定文本。支持自定义发音人、一次性设置Appkey/Token、按住Ctrl临时禁用、网站黑名单功能。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         网页划词朗读
// @name:en      Web Selection Reader
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  使用阿里云TTS朗读网页选定文本。支持自定义发音人、一次性设置Appkey/Token、按住Ctrl临时禁用、网站黑名单功能。
// @description:en Read selected text on any webpage using Aliyun TTS. Supports custom voice, one-time Appkey/Token setup, holding Ctrl to disable, and a site blacklist feature.
// @author       Gemini & YourName
// @license      CC BY-NC-SA 4.0
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      nls-gateway-cn-shanghai.aliyuncs.com
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置区域 ---
    const DEFAULT_VOICE_FALLBACK = 'tomoka'; // 默认的发音人 (当用户未设置时)
    const DEFAULT_FORMAT = 'mp3';
    const DEFAULT_SAMPLE_RATE = 16000;
    const MAX_TEXT_LENGTH = 500; // 限制朗读的最大字符数

    // --- 油猴存储键名 ---
    const KEY_APPKEY = 'aliyun_tts_appkey';
    const KEY_TOKEN = 'aliyun_tts_token';
    const KEY_BLACKLIST = 'tts_blacklist';
    const KEY_VOICE = 'aliyun_tts_voice'; // 新增:用于存储发音人

    // --- 变量和状态 ---
    let audio = null;
    let isPlaying = false;

    // --- 从油猴存储中读取配置 ---
    let appkey = GM_getValue(KEY_APPKEY, '');
    let token = GM_getValue(KEY_TOKEN, '');
    let voice = GM_getValue(KEY_VOICE, DEFAULT_VOICE_FALLBACK);
    let blacklist = JSON.parse(GM_getValue(KEY_BLACKLIST, '[]'));

    // --- 核心功能:语音合成 ---

    /**
     * @description 调用阿里云TTS API进行语音合成并播放
     * @param {string} text - 需要朗读的文本
     */
    function speak(text) {
        if (isPlaying) {
            audio.pause();
            isPlaying = false;
        }

        if (!appkey || !token) {
            alert('尚未配置Appkey或Token。请点击油猴扩展图标,在菜单中进行设置。');
            if (confirm('是否现在就去设置?')) {
                setupCredentials();
            }
            return;
        }

        const params = new URLSearchParams({
            appkey: appkey,
            token: token,
            text: text,
            format: DEFAULT_FORMAT,
            sample_rate: DEFAULT_SAMPLE_RATE,
            voice: voice, // 使用可配置的voice变量
        });

        const url = `https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts?${params.toString().replace(/\+/g, '%20')}`;

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            onload: function(response) {
                if (response.status === 200) {
                    const audioBlob = response.response;
                    const audioUrl = URL.createObjectURL(audioBlob);
                    audio = new Audio(audioUrl);
                    audio.play();
                    isPlaying = true;
                    audio.onended = () => {
                        isPlaying = false;
                        URL.revokeObjectURL(audioUrl);
                    };
                } else {
                    response.response.text().then(errorText => {
                        console.error('阿里云TTS请求失败:', errorText);
                        try {
                            const errorJson = JSON.parse(errorText);
                            alert(`语音合成失败: ${errorJson.message}\n\n这通常意味着Token已过期或Appkey/发音人名称不正确。`);
                        } catch (e) {
                            alert(`语音合成失败,无法解析错误信息: ${errorText}`);
                        }
                    });
                }
            },
            onerror: function(error) {
                console.error('网络请求错误:', error);
                alert('网络请求失败,请检查网络连接或浏览器控制台。');
            }
        });
    }


    // --- 配置与菜单功能 ---

    /**
     * @description 弹窗引导用户一次性设置Appkey和Token
     */
    function setupCredentials() {
        const placeholder = "请按格式粘贴“Appkey,Token” (用英文逗号分隔)\n\n示例:\nLTAI5t...,450343c7...";
        const currentValues = appkey && token ? `${appkey},${token}` : '';
        const input = prompt("⚙️ 设置阿里云 Appkey 和 Token", currentValues || placeholder);

        if (input === null) {
            alert('操作已取消。');
            return;
        }

        const parts = input.split(',').map(s => s.trim());

        if (parts.length !== 2 || !parts[0] || !parts[1]) {
            alert('格式错误!\n\n请输入由一个英文逗号分隔的Appkey和Token。');
            return;
        }

        appkey = parts[0];
        token = parts[1];
        GM_setValue(KEY_APPKEY, appkey);
        GM_setValue(KEY_TOKEN, token);

        alert('Appkey 和 Token 已成功更新!');
    }

    /**
     * @description 设置TTS发音人
     */
    function setupVoice() {
        const voiceList = "常用日语女声: airi, haruka, nanako, shiori, tomoka";
        const input = prompt(`🎤 请输入要使用的发音人名称。\n当前为: ${voice}\n\n${voiceList}\n(您也可以输入其他任何有效的阿里云TTS发音人名称)`, voice);

        if (input === null) {
            alert('操作已取消。');
            return;
        }

        const newVoice = input.trim();
        if (newVoice) {
            voice = newVoice;
            GM_setValue(KEY_VOICE, voice);
            alert(`发音人已更新为: ${voice}`);
        } else {
            alert('发音人名称不能为空!');
        }
    }

    /**
     * @description 将当前网站域名添加到黑名单 (立即生效)
     */
    function addCurrentSiteToBlacklist() {
        const hostname = window.location.hostname;
        if (!blacklist.includes(hostname)) {
            blacklist.push(hostname); // **关键修正: 直接更新内存中的变量**
            GM_setValue(KEY_BLACKLIST, JSON.stringify(blacklist));
            alert(`【${hostname}】\n\n已加入朗读黑名单,在本页面立即生效。\n菜单选项将在刷新后更新。`);
        } else {
            alert(`【${hostname}】\n\n已在黑名单中,无需重复添加。`);
        }
    }

    /**
     * @description 从黑名单中移除当前网站域名 (立即生效)
     */
    function removeCurrentSiteFromBlacklist() {
        const hostname = window.location.hostname;
        const index = blacklist.indexOf(hostname);
        if (index > -1) {
            blacklist.splice(index, 1); // **关键修正: 直接更新内存中的变量**
            GM_setValue(KEY_BLACKLIST, JSON.stringify(blacklist));
            alert(`【${hostname}】\n\n已从朗读黑名单中移除,在本页面立即生效。\n菜单选项将在刷新后更新。`);
        } else {
            alert(`【${hostname}】\n\n未在黑名单中。`);
        }
    }

    /**
     * @description 检查当前网站是否在黑名单中
     * @returns {boolean}
     */
    function isSiteBlacklisted() {
        return blacklist.includes(window.location.hostname);
    }

    // --- 注册油猴菜单命令 ---
    GM_registerMenuCommand('⚙️ 设置 Appkey 和 Token', setupCredentials);
    GM_registerMenuCommand('🎤 设置发音人 (Voice)', setupVoice);

    if (isSiteBlacklisted()) {
        GM_registerMenuCommand('✅ 在此网站上启用朗读', removeCurrentSiteFromBlacklist);
    } else {
        GM_registerMenuCommand('❌ 在此网站上禁用朗读', addCurrentSiteToBlacklist);
    }


    // --- 事件监听器 ---
    document.addEventListener('mouseup', function(event) {
        if (isSiteBlacklisted()) {
            return;
        }

        if (event.ctrlKey) {
            return;
        }

        setTimeout(() => {
            const selectedText = window.getSelection().toString().trim();
            if (selectedText.length > 0 && selectedText.length < MAX_TEXT_LENGTH) {
                speak(selectedText);
            }
        }, 100);
    });

})();