Text-to-Speech Reader

Read selected text using OpenAI TTS API

目前為 2024-06-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Text-to-Speech Reader
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description    Read selected text using OpenAI TTS API
// @author       https://linux.do/u/snaily
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license       MIT
// ==/UserScript==

(function() {
    'use strict';

    // Add a button to the page for reading selected text
    const button = document.createElement('button');
    button.innerText = 'Read Aloud';
    button.style.position = 'absolute';
    button.style.width = 'auto';
    button.style.zIndex = '1000';
    button.style.display = 'none'; // Initially hidden
    button.style.backgroundColor = '#007BFF'; // Blue background
    button.style.color = '#FFFFFF'; // White text
    button.style.border = 'none';
    button.style.borderRadius = '5px';
    button.style.padding = '10px 20px';
    button.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
    button.style.cursor = 'pointer';
    button.style.fontSize = '14px';
    button.style.fontFamily = 'Arial, sans-serif';
    document.body.appendChild(button);

    // Function to get selected text
    function getSelectedText() {
        let text = '';
        if (window.getSelection) {
            text = window.getSelection().toString();
        } else if (document.selection && document.selection.type != 'Control') {
            text = document.selection.createRange().text;
        }
        console.log('Selected Text:', text); // Debugging line
        return text;
    }

    // Function to call OpenAI TTS API
    function callOpenAITTS(text, baseUrl, apiKey, voice, model) {
        const cachedAudioUrl = getCachedAudio(text);
        if (cachedAudioUrl) {
            console.log('Using cached audio');
            playAudio(cachedAudioUrl);
            resetButton();
            return;
        }

        const url = `${baseUrl}/v1/audio/speech`;
        console.log('Calling OpenAI TTS API with text:', text);
        GM_xmlhttpRequest({
            method: 'POST',
            url: url,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            data: JSON.stringify({
                model: model,
                input: text,
                voice: voice
            }),
            responseType: 'arraybuffer',
            onload: function(response) {
                if (response.status === 200) {
                    console.log('API call successful'); // Debugging line
                    const audioBlob = new Blob([response.response], { type: 'audio/mpeg' });
                    const audioUrl = URL.createObjectURL(audioBlob);
                    playAudio(audioUrl);
                    cacheAudio(text, audioUrl);
                } else {
                    console.error('Error:', response.statusText);
                }
                // Reset button after request is complete
                resetButton();
            },
            onerror: function(error) {
                console.error('Request failed', error);
                // Reset button after request is complete
                resetButton();
            }
        });
    }

    // Function to play audio
    function playAudio(url) {
        const audio = new Audio(url);
        audio.play();
    }

    // Function to use browser's built-in TTS
    function speakText(text) {
        const utterance = new SpeechSynthesisUtterance(text);
        speechSynthesis.speak(utterance);
    }

    // Function to set button to loading state
    function setLoadingState() {
        button.disabled = true;
        button.innerText = 'Loading...';
        button.style.backgroundColor = '#6c757d'; // Grey background
        button.style.cursor = 'not-allowed';
    }

    // Function to reset button to original state
    function resetButton() {
        button.disabled = false;
        button.innerText = 'Read Aloud';
        button.style.backgroundColor = '#007BFF'; // Blue background
        button.style.cursor = 'pointer';
    }

    // Helper function to get cached audio URL
    function getCachedAudio(text) {
        const cache = GM_getValue('cache', {});
        const item = cache[text];
        if (item) {
            const now = new Date().getTime();
            const weekInMillis = 7 * 24 * 60 * 60 * 1000; // One day in milliseconds
            if (now - item.timestamp < weekInMillis) {
                return item.audioUrl;
            } else {
                delete cache[text]; // Remove expired cache item
                GM_setValue('cache', cache);
            }
        }
        return null;
    }

    // Helper function to cache audio URL
    function cacheAudio(text, audioUrl) {
        const cache = GM_getValue('cache', {});
        cache[text] = {
            audioUrl: audioUrl,
            timestamp: new Date().getTime()
        };
        GM_setValue('cache', cache);
    }

    // Function to clear cache
    function clearCache() {
        GM_setValue('cache', {});
        alert('Cache cleared successfully.');
    }


    // Event listener for button click
    button.addEventListener('click', () => {
        const selectedText = getSelectedText();
        if (selectedText) {
            let apiKey = GM_getValue('apiKey', null);
            let baseUrl = GM_getValue('baseUrl', null);
            let voice = GM_getValue('voice', 'onyx'); // Default to 'onyx'
            let model = GM_getValue('model', 'tts-1'); // Default to 'tts-1'
            if (!baseUrl) {
                alert('Please set the base URL for the TTS API in the Tampermonkey menu.');
                return;
            }
            if (!apiKey) {
                alert('Please set the API key for the TTS API in the Tampermonkey menu.');
                return;
            }
            setLoadingState(); // Set button to loading state
            if (window.location.hostname === 'github.com') {
                speakText(selectedText);
                resetButton(); // Reset button immediately for built-in TTS
            }else {
                callOpenAITTS(selectedText, baseUrl, apiKey, voice, model);
            }
        } else {
            alert('Please select some text to read aloud.');
        }
    });

     // Show the button near the selected text
    document.addEventListener('mouseup', (event) => {
        // Check if the mouseup event is triggered by the button itself
        if (event.target === button) {
            return;
        }

        const selectedText = getSelectedText();
        if (selectedText) {
            const mouseX = event.pageX;
            const mouseY = event.pageY;
            button.style.left = `${mouseX + 10}px`;
            button.style.top = `${mouseY + 10}px`;
            button.style.display = 'block';
        } else {
            button.style.display = 'none';
        }
    });



    // Initialize UI components
    function initModal() {
        const modalHTML = `
            <div id="configModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: none; justify-content: center; align-items: center; z-index: 10000;">
                <div style="background: white; padding: 20px; border-radius: 10px; width: 300px;">
                    <h2>Configure TTS Settings</h2>
                    <label for="baseUrl">Base URL:</label>
                    <input type="text" id="baseUrl" value="${GM_getValue('baseUrl', 'https://api.openai.com')}" style="width: 100%;">
                    <label for="apiKey">API Key:</label>
                    <input type="text" id="apiKey" value="${GM_getValue('apiKey', '')}" style="width: 100%;">
                    <label for="model">Model:</label>
                    <select id="model" style="width: 100%;">
                        <option value="tts-1">tts-1</option>
                        <option value="tts-hailuo">tts-hailuo</option>
                    </select>
                    <label for="voice">Voice:</label>
                    <select id="voice" style="width: 100%;">
                        <option value="alloy">Alloy</option>
                        <option value="echo">Echo</option>
                        <option value="fable">Fable</option>
                        <option value="onyx">Onyx</option>
                        <option value="nova">Nova</option>
                        <option value="shimmer">Shimmer</option>
                    </select>
                    <button id="saveConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: #007BFF; color: white; border: none; border-radius: 5px;">Save</button>
                    <button id="cancelConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: grey; color: white; border: none; border-radius: 5px;">Cancel</button>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', modalHTML);
        document.getElementById('saveConfig').addEventListener('click', saveConfig);
        document.getElementById('cancelConfig').addEventListener('click', closeModal);
        document.getElementById('model').addEventListener('change', updateVoiceOptions);
    }
    function updateVoiceOptions() {
        // 获取select元素
        var modelSelect = document.getElementById('model');
        var voiceSelect = document.getElementById('voice');

        if (modelSelect.value === 'tts-hailuo') {
            // 清空voiceSelect
            voiceSelect.innerHTML = `
                <option value="male-botong">思远</option>
                <option value="Podcast_girl">心悦</option>
                <option value="boyan_new_hailuo">子轩</option>
                <option value="female-shaonv">灵儿</option>
                <option value="YaeMiko_hailuo">语嫣</option>
                <option value="xiaoyi_mix_hailuo">少泽</option>
                <option value="xiaomo_sft">芷溪</option>
                <option value="cove_test2_hailuo">浩翔(英文)</option>
                <option value="scarlett_hailuo">雅涵(英文)</option>
                <option value="Leishen2_hailuo">雷电将军</option>
                <option value="Zhongli_hailuo">钟离</option>
                <option value="Paimeng_hailuo">派蒙</option>
                <option value="keli_hailuo">可莉</option>
                <option value="Hutao_hailuo">胡桃</option>
                <option value="Xionger_hailuo">熊二</option>
                <option value="Haimian_hailuo">海绵宝宝</option>
                <option value="Robot_hunter_hailuo">变形金刚</option>
                <option value="Linzhiling_hailuo">小玲玲</option>
                <option value="huafei_hailuo">拽妃</option>
                <option value="lingfeng_hailuo">东北er</option>
                <option value="male_dongbei_hailuo">老铁</option>
                <option value="Beijing_hailuo">北京er</option>
                <option value="JayChou_hailuo">JayChou</option>
                <option value="Daniel_hailuo">潇然</option>
                <option value="Bingjiao_zongcai_hailuo">沉韵</option>
                <option value="female-yaoyao-hd">瑶瑶</option>
                <option value="murong_sft">晨曦</option>
                <option value="shangshen_sft">沐珊</option>
                <option value="kongchen_sft">祁辰</option>
                <option value="shenteng2_hailuo">夏洛特</option>
                <option value="Guodegang_hailuo">郭嘚嘚</option>
                <option value="yueyue_hailuo">小月月</option>
            `;
        } else {
            // 恢复默认选项
            voiceSelect.innerHTML = `
                <option value="alloy">Alloy</option>
                <option value="echo">Echo</option>
                <option value="fable">Fable</option>
                <option value="onyx">Onyx</option>
                <option value="nova">Nova</option>
                <option value="shimmer">Shimmer</option>
            `;
        }
    }

    function saveConfig() {
        const baseUrl = document.getElementById('baseUrl').value;
        const model = document.getElementById('model').value;
        const apiKey = document.getElementById('apiKey').value;
        const voice = document.getElementById('voice').value;
        GM_setValue('baseUrl', baseUrl);
        GM_setValue('model', model);
        GM_setValue('apiKey', apiKey);
        GM_setValue('voice', voice);
        alert('Settings saved successfully.');
        closeModal();
    }

    function closeModal() {
        document.getElementById('configModal').style.display = 'none';
    }

    function openModal() {
        if (!document.getElementById('configModal')) {
            initModal();
        }
        document.getElementById('configModal').style.display = 'flex';
        // Set the current values from the cache
        document.getElementById('baseUrl').value = GM_getValue('baseUrl', '');
        document.getElementById('apiKey').value = GM_getValue('apiKey', '');
        document.getElementById('model').value = GM_getValue('model', 'tts-1');
        updateVoiceOptions(); // Ensure voice options are updated based on the model
        document.getElementById('voice').value = GM_getValue('voice', 'onyx');
    }

    GM_registerMenuCommand('Configure TTS Settings', openModal);

    // Register menu command to clear cache
    GM_registerMenuCommand('Clear TTS Cache', clearCache);
})();