Text-to-Speech Reader脚本新增海螺语音(有周董)支持

背景

感谢 @eggacheb 佬友提供非常详细的海螺语音部署和使用教程。在浏览器端使用Text-to-Speech Reader脚本调用海螺语音需要各位佬友去修改一些代码。为了方便各位佬友在浏览器端使用海螺语音,Text-to-Speech Reader脚本已更新支持海螺全部语音,无需各位佬友去改代码。

前文

主要更新

  1. 新增模型选择功能,支持openai tts-1和海螺tts-hailuo
  2. 新增全部海螺语音支持,支持的语音如下所示。
male-botong 思远 [兼容 tts-1 alloy]
Podcast_girl 心悦 [兼容 tts-1 echo]
boyan_new_hailuo 子轩 [兼容 tts-1 fable]
female-shaonv 灵儿 [兼容 tts-1 onyx]
YaeMiko_hailuo 语嫣 [兼容 tts-1 nova]
xiaoyi_mix_hailuo 少泽 [兼容 tts-1 shimmer]
xiaomo_sft 芷溪 [兼容 tts-1-hd alloy]
cove_test2_hailuo 浩翔(英文)
scarlett_hailuo 雅涵(英文)
Leishen2_hailuo 模仿雷电将军 [兼容 tts-1-hd echo]
Zhongli_hailuo 模仿钟离 [兼容 tts-1-hd fable]
Paimeng_hailuo 模仿派蒙 [兼容 tts-1-hd onyx]
keli_hailuo 模仿可莉 [兼容 tts-1-hd nova]
Hutao_hailuo 模仿胡桃 [兼容 tts-1-hd shimmer]
Xionger_hailuo 模仿熊二
Haimian_hailuo 模仿海绵宝宝
Robot_hunter_hailuo 模仿变形金刚
Linzhiling_hailuo 小玲玲
huafei_hailuo 拽妃
lingfeng_hailuo 东北er
male_dongbei_hailuo 老铁
Beijing_hailuo 北京er
JayChou_hailuo JayJay
Daniel_hailuo 潇然
Bingjiao_zongcai_hailuo 沉韵
female-yaoyao-hd 瑶瑶
murong_sft 晨曦
shangshen_sft 沐珊
kongchen_sft 祁辰
shenteng2_hailuo 夏洛特
Guodegang_hailuo 郭嘚嘚
yueyue_hailuo 小月月

代码

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

脚本外部地址

配置和使用

  1. 按照eggacheb佬友的教程搭建好海螺语音服务
  2. 在newapi/oneapi,新增自定义模型tts-hailuo,添加模型重定向
{
  "tts-hailuo": "tts-1-hd nova"
}

具体如图所示:

  1. 配置baseurl,apikey等
    image

  2. 选择浏览器任意文字,弹出read aloud按钮,点击等待语音请求完成并自动播放。

15 Likes

感谢!

2 Likes

感谢佬友提供的详细教程,让我用上了周董的语音包,嘿嘿 ~:tieba_022:

3 Likes

诶哟,不错哦

2 Likes

哎哟,好帅哦

1 Like

哎呦 可以哦

2 Likes

哎哟,有眼光 :tieba_002:

1 Like

不错哦

2 Likes

不错哦!

2 Likes

:tieba_003: :tieba_003:

1 Like

:tieba_002: :tieba_002:

1 Like

哎哟,不错哦,又更新了

2 Likes

哎哟,被天哥发现啦! :tieba_022:这也是发现了好东西跟进一下

1 Like

佬,请教一下,我的hailuo-free-api服务在serv00跑起来了,new api也在serv00跑起来了,在hailuoai获取了token,添加渠道之后,使用这个油猴脚本没有声音。数据看板里也看不到调用过的痕迹是什么情况呀,我的渠道配置如下:

2 Likes

添加一个自定义模型tts-hailuo点击填入

大佬,添加了。还是不行。

获取Token:

配置渠道:

1 Like

可以了可以了!!!大佬牛逼!

1 Like

:+1: :+1: :+1:是啥原因啊

这个海螺的免费使用量是多少呀?

免费使用量没有看到有具体说明,就我目前使用情况来看,使用特别频繁的话会有429错误,不过一会儿就又可以用了 :smiley: