您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Read selected text using OpenAI TTS API
// ==UserScript== // @name Text-to-Speech Reader // @namespace http://tampermonkey.net/ // @version 1.6 // @description Read selected text using OpenAI TTS API // @author https://linux.do/u/snaily,https://linux.do/u/joegodwanggod // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function () { "use strict"; // 创建按钮 const button = document.createElement("button"); button.innerText = "TTS"; button.style.position = "absolute"; button.style.width = "auto"; button.style.zIndex = "1000"; button.style.display = "none"; // 初始隐藏 button.style.backgroundColor = "#007BFF"; // 蓝色背景 button.style.color = "#FFFFFF"; // 白色文字 button.style.border = "none"; button.style.borderRadius = "3px"; // 调整圆角 button.style.padding = "5px 10px"; // 减少内边距 button.style.boxShadow = "0 2px 5px rgba(0, 0, 0, 0.2)"; button.style.cursor = "pointer"; button.style.fontSize = "12px"; button.style.fontFamily = "Arial, sans-serif"; document.body.appendChild(button); // 获取选中的文本 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); // 调试用 return text; } // 判断文本是否为有效内容 (非空白) function isTextValid(text) { return text.trim().length > 0; } // 调用 OpenAI TTS API function callOpenAITTS(text, baseUrl, apiKey, voice, model) { const cachedAudioUrl = getCachedAudio(text); if (cachedAudioUrl) { console.log("使用缓存的音频"); playAudio(cachedAudioUrl); resetButton(); return; } const url = `${baseUrl}/v1/audio/speech`; console.log("调用 OpenAI TTS API,文本:", 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 调用成功"); // 调试用 const audioBlob = new Blob([response.response], { type: "audio/mpeg", }); const audioUrl = URL.createObjectURL(audioBlob); playAudio(audioUrl); cacheAudio(text, audioUrl); } else { console.error("错误:", response.statusText); showCustomAlert( `TTS API 错误:${response.status} ${response.statusText}` ); } // 请求完成后重置按钮 resetButton(); }, onerror: function (error) { console.error("请求失败", error); showCustomAlert("TTS API 请求失败。"); // 请求失败后重置按钮 resetButton(); }, }); } // 播放音频 function playAudio(url) { const audio = new Audio(url); audio.play(); } // 使用浏览器内建 TTS function speakText(text) { const utterance = new SpeechSynthesisUtterance(text); speechSynthesis.speak(utterance); } // 设置按钮为加载状态 function setLoadingState() { button.disabled = true; button.innerText = "Loading"; button.style.backgroundColor = "#6c757d"; // 灰色背景 button.style.cursor = "not-allowed"; } // 重置按钮到原始状态 function resetButton() { button.disabled = false; button.innerText = "TTS"; button.style.backgroundColor = "#007BFF"; // 蓝色背景 button.style.cursor = "pointer"; } // 获取缓存的音频 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; // 一周的毫秒数 if (now - item.timestamp < weekInMillis) { return item.audioUrl; } else { delete cache[text]; // 删除过期的缓存 GM_setValue("cache", cache); } } return null; } // 缓存音频 URL function cacheAudio(text, audioUrl) { const cache = GM_getValue("cache", {}); cache[text] = { audioUrl: audioUrl, timestamp: new Date().getTime(), }; GM_setValue("cache", cache); } // 清除缓存 function clearCache() { GM_setValue("cache", {}); showCustomAlert("缓存已成功清除。"); } // 按钮点击事件 button.addEventListener("click", (event) => { event.stopPropagation(); // 防止点击按钮时触发全局点击事件 const selectedText = getSelectedText(); if (selectedText && isTextValid(selectedText)) { // 添加有效性检查 let apiKey = GM_getValue("apiKey", null); let baseUrl = GM_getValue("baseUrl", null); let voice = GM_getValue("voice", "onyx"); // 默认为 'onyx' let model = GM_getValue("model", "tts-1"); // 默认为 'tts-1' if (!baseUrl) { showCustomAlert("请在 Tampermonkey 菜单中设置 TTS API 的基础 URL。"); return; } if (!apiKey) { showCustomAlert("请在 Tampermonkey 菜单中设置 TTS API 的 API 密钥。"); return; } setLoadingState(); // 设置按钮为加载状态 if (window.location.hostname === "github.com") { speakText(selectedText); resetButton(); // 使用内建 TTS 后立即重置按钮 } else { callOpenAITTS(selectedText, baseUrl, apiKey, voice, model); } } else { showCustomAlert("请选择一些有效的文本以朗读。"); } }); // 在选中文本附近显示按钮 document.addEventListener("mouseup", (event) => { // 设置一个短暂的延迟,确保选区状态已更新 setTimeout(() => { // 检查 mouseup 事件是否由按钮本身触发 if (event.target === button) { return; } const selectedText = getSelectedText(); if (selectedText && isTextValid(selectedText)) { // 添加有效性检查 const mouseX = event.pageX; const mouseY = event.pageY; button.style.left = `${mouseX + 30}px`; // 调整按钮位置 button.style.top = `${mouseY - 10}px`; button.style.display = "block"; } else { button.style.display = "none"; } }, 10); // 10毫秒延迟 }); // 监听点击页面其他部分以隐藏按钮 document.addEventListener("click", (event) => { if (event.target !== button) { const selectedText = getSelectedText(); if (!selectedText || !isTextValid(selectedText)) { button.style.display = "none"; } } }); // 初始化配置模态框 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>配置 TTS 设置</h2> <label for="baseUrl">基础 URL:</label> <input type="text" id="baseUrl" value="${GM_getValue( "baseUrl", "https://api.openai.com" )}" style="width: 100%;"> <label for="apiKey">API 密钥:</label> <input type="text" id="apiKey" value="${GM_getValue( "apiKey", "" )}" style="width: 100%;"> <label for="model">模型:</label> <select id="model" style="width: 100%;"> <option value="tts-1">tts-1</option> <option value="tts-hailuo">tts-hailuo</option> <option value="tts-1-hd">tts-1-hd</option> <option vlaue="tts-audio-fish">tts-audio-fish</option> </select> <label for="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: 5px; background-color: #007BFF; color: white; border: none; border-radius: 3px;">保存</button> <button id="cancelConfig" style="margin-top: 5px; width: 100%; padding: 5px; background-color: grey; color: white; border: none; border-radius: 3px;">取消</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() { const modelSelect = document.getElementById("model"); const voiceSelect = document.getElementById("voice"); if (modelSelect.value === "tts-hailuo") { 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 if (modelSelect.value === "tts-1-hd") { 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> `; } else if (modelSelect.value === "tts-audio-fish") { voiceSelect.innerHTML = ` <option value="54a5170264694bfc8e9ad98df7bd89c3">丁真</option> <option value="7f92f8afb8ec43bf81429cc1c9199cb1">AD学姐</option> <option value="0eb38bc974e1459facca38b359e13511">赛马娘</option> <option value="e4642e5edccd4d9ab61a69e82d4f8a14">蔡徐坤</option> <option value="332941d1360c48949f1b4e0cabf912cd">丁真(锐刻五代版)</option> <option value="f7561ff309bd4040a59f1e600f4f4338">黑手</option> <option value="e80ea225770f42f79d50aa98be3cedfc">孙笑川258</option> <option value="1aacaeb1b840436391b835fd5513f4c4">芙宁娜</option> <option value="59cb5986671546eaa6ca8ae6f29f6d22">央视配音</option> <option value="3b55b3d84d2f453a98d8ca9bb24182d6">邓紫琪</option> <option value="738d0cc1a3e9430a9de2b544a466a7fc">雷军</option> <option value="e1cfccf59a1c4492b5f51c7c62a8abd2">永雏塔菲</option> <option value="7af4d620be1c4c6686132f21940d51c5">东雪莲</option> <option value="7c66db6e457c4d53b1fe428a8c547953">郭德纲</option> <option value="e488ebeadd83496b97a3cd472dcd04ab">爱丽丝(中配)</option> <option value="b1ce0a88c79f4e3180217a7fe2c72969">飞凡高启强</option> <option value="57a14f36492d4d0eb207b9fe9d335f95">国恒</option> <option value="787159b6d13542afbaff4f933689bab6">伯邑考</option> <option value="f4913edba8844da9827c28210ff5f884">机智张</option> <option value="c1fc72257200410587a557758b320700">彭海兵</option> <option value="8a112f7f56694daaa3c7a55c08f6e5a0">申公豹</option> <option value="af450a74e5f94095bbf009e2c7b6b0e7">赵德汉</option> <option value="b1602dc301a84093aabe97da41e59ee7">神魔暗信</option> <option value="de5e904b61214ed5bad3e4757cd5aed9">诸葛</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.trim(); const model = document.getElementById("model").value; const apiKey = document.getElementById("apiKey").value.trim(); const voice = document.getElementById("voice").value; if (!baseUrl) { showCustomAlert("基础 URL 不能为空。"); return; } if (!apiKey) { showCustomAlert("API 密钥不能为空。"); return; } GM_setValue("baseUrl", baseUrl); GM_setValue("model", model); GM_setValue("apiKey", apiKey); GM_setValue("voice", voice); showCustomAlert("设置已成功保存。"); closeModal(); } // 关闭模态框 function closeModal() { if (document.getElementById("configModal")) { document.getElementById("configModal").style.display = "none"; } } // 打开模态框 function openModal() { if (!document.getElementById("configModal")) { initModal(); } document.getElementById("configModal").style.display = "flex"; // 设置当前值 document.getElementById("baseUrl").value = GM_getValue( "baseUrl", "https://api.openai.com" ); document.getElementById("apiKey").value = GM_getValue("apiKey", ""); document.getElementById("model").value = GM_getValue("model", "tts-1"); updateVoiceOptions(); // 根据模型更新语音选项 document.getElementById("voice").value = GM_getValue("voice", "onyx"); } // 创建自定义弹窗 function createCustomAlert() { const alertBox = document.createElement("div"); alertBox.id = "customAlertBox"; alertBox.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 2147483647; // 使用最高的 z-index 值 display: none; color: #333; // 设置默认文字颜色 font-family: Arial, sans-serif; // 设置字体 max-width: 80%; width: 300px; text-align: center; `; const message = document.createElement("p"); message.id = "alertMessage"; message.style.cssText = ` margin-bottom: 15px; color: #333; // 确保消息文本颜色 word-wrap: break-word; `; const closeButton = document.createElement("button"); closeButton.textContent = "确定"; closeButton.style.cssText = ` padding: 5px 10px; background-color: #007BFF; color: white; border: none; border-radius: 3px; cursor: pointer; font-family: inherit; // 继承父元素的字体 `; closeButton.onclick = () => { alertBox.style.opacity = "0"; setTimeout(() => (alertBox.style.display = "none"), 300); }; alertBox.appendChild(message); alertBox.appendChild(closeButton); document.body.appendChild(alertBox); // 添加淡入淡出效果 alertBox.style.transition = "opacity 0.3s ease-in-out"; } // 显示自定义弹窗 function showCustomAlert(text) { const alertBox = document.getElementById("customAlertBox") || createCustomAlert(); document.getElementById("alertMessage").textContent = text; alertBox.style.display = "block"; alertBox.style.opacity = "0"; setTimeout(() => (alertBox.style.opacity = "1"), 10); // 短暂延迟以确保过渡效果生效 } // 注册(不可用)菜单命令以打开配置 GM_registerMenuCommand("配置 TTS 设置", openModal); // 注册(不可用)菜单命令以清除缓存 GM_registerMenuCommand("清除 TTS 缓存", clearCache); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址