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