// ==UserScript== // @name Text-to-Speech Reader // @namespace http://tampermonkey.net/ // @version 1.5 // @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 // @downloadURL none // ==/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 = ` `; 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 = ` `; } else if (modelSelect.value === "tts-1-hd") { voiceSelect.innerHTML = ` `; } else if (modelSelect.value === "tts-audio-fish") { voiceSelect.innerHTML = ` `; } else { // 恢复默认选项 voiceSelect.innerHTML = ` `; } } // 保存配置 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: 10001; display: none; color: #333; // 设置默认文字颜色 font-family: Arial, sans-serif; // 设置字体 `; const message = document.createElement("p"); message.id = "alertMessage"; message.style.cssText = ` margin-bottom: 15px; color: #333; // 确保消息文本颜色 `; 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.display = "none"); alertBox.appendChild(message); alertBox.appendChild(closeButton); document.body.appendChild(alertBox); } // 显示自定义弹窗 function showCustomAlert(text) { const alertBox = document.getElementById("customAlertBox") || createCustomAlert(); document.getElementById("alertMessage").textContent = text; alertBox.style.display = "block"; } // 注册菜单命令以打开配置 GM_registerMenuCommand("配置 TTS 设置", openModal); // 注册菜单命令以清除缓存 GM_registerMenuCommand("清除 TTS 缓存", clearCache); })();