Text-to-Speech Reader

Read selected text using OpenAI TTS API

当前为 2024-06-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Text-to-Speech Reader
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4
  5. // @description Read selected text using OpenAI TTS API
  6. // @author https://linux.do/u/snaily
  7. // @match *://*/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // Add a button to the page for reading selected text
  20. const button = document.createElement('button');
  21. button.innerText = 'Read Aloud';
  22. button.style.position = 'absolute';
  23. button.style.width = 'auto';
  24. button.style.zIndex = '1000';
  25. button.style.display = 'none'; // Initially hidden
  26. button.style.backgroundColor = '#007BFF'; // Blue background
  27. button.style.color = '#FFFFFF'; // White text
  28. button.style.border = 'none';
  29. button.style.borderRadius = '5px';
  30. button.style.padding = '10px 20px';
  31. button.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
  32. button.style.cursor = 'pointer';
  33. button.style.fontSize = '14px';
  34. button.style.fontFamily = 'Arial, sans-serif';
  35. document.body.appendChild(button);
  36.  
  37. // Function to get selected text
  38. function getSelectedText() {
  39. let text = '';
  40. if (window.getSelection) {
  41. text = window.getSelection().toString();
  42. } else if (document.selection && document.selection.type != 'Control') {
  43. text = document.selection.createRange().text;
  44. }
  45. console.log('Selected Text:', text); // Debugging line
  46. return text;
  47. }
  48.  
  49. // Function to call OpenAI TTS API
  50. function callOpenAITTS(text, baseUrl, apiKey, voice, model) {
  51. const cachedAudioUrl = getCachedAudio(text);
  52. if (cachedAudioUrl) {
  53. console.log('Using cached audio');
  54. playAudio(cachedAudioUrl);
  55. resetButton();
  56. return;
  57. }
  58.  
  59. const url = `${baseUrl}/v1/audio/speech`;
  60. console.log('Calling OpenAI TTS API with text:', text);
  61. GM_xmlhttpRequest({
  62. method: 'POST',
  63. url: url,
  64. headers: {
  65. 'Content-Type': 'application/json',
  66. 'Authorization': `Bearer ${apiKey}`
  67. },
  68. data: JSON.stringify({
  69. model: model,
  70. input: text,
  71. voice: voice
  72. }),
  73. responseType: 'arraybuffer',
  74. onload: function(response) {
  75. if (response.status === 200) {
  76. console.log('API call successful'); // Debugging line
  77. const audioBlob = new Blob([response.response], { type: 'audio/mpeg' });
  78. const audioUrl = URL.createObjectURL(audioBlob);
  79. playAudio(audioUrl);
  80. cacheAudio(text, audioUrl);
  81. } else {
  82. console.error('Error:', response.statusText);
  83. }
  84. // Reset button after request is complete
  85. resetButton();
  86. },
  87. onerror: function(error) {
  88. console.error('Request failed', error);
  89. // Reset button after request is complete
  90. resetButton();
  91. }
  92. });
  93. }
  94.  
  95. // Function to play audio
  96. function playAudio(url) {
  97. const audio = new Audio(url);
  98. audio.play();
  99. }
  100.  
  101. // Function to use browser's built-in TTS
  102. function speakText(text) {
  103. const utterance = new SpeechSynthesisUtterance(text);
  104. speechSynthesis.speak(utterance);
  105. }
  106.  
  107. // Function to set button to loading state
  108. function setLoadingState() {
  109. button.disabled = true;
  110. button.innerText = 'Loading...';
  111. button.style.backgroundColor = '#6c757d'; // Grey background
  112. button.style.cursor = 'not-allowed';
  113. }
  114.  
  115. // Function to reset button to original state
  116. function resetButton() {
  117. button.disabled = false;
  118. button.innerText = 'Read Aloud';
  119. button.style.backgroundColor = '#007BFF'; // Blue background
  120. button.style.cursor = 'pointer';
  121. }
  122.  
  123. // Helper function to get cached audio URL
  124. function getCachedAudio(text) {
  125. const cache = GM_getValue('cache', {});
  126. const item = cache[text];
  127. if (item) {
  128. const now = new Date().getTime();
  129. const weekInMillis = 7 * 24 * 60 * 60 * 1000; // One day in milliseconds
  130. if (now - item.timestamp < weekInMillis) {
  131. return item.audioUrl;
  132. } else {
  133. delete cache[text]; // Remove expired cache item
  134. GM_setValue('cache', cache);
  135. }
  136. }
  137. return null;
  138. }
  139.  
  140. // Helper function to cache audio URL
  141. function cacheAudio(text, audioUrl) {
  142. const cache = GM_getValue('cache', {});
  143. cache[text] = {
  144. audioUrl: audioUrl,
  145. timestamp: new Date().getTime()
  146. };
  147. GM_setValue('cache', cache);
  148. }
  149.  
  150. // Function to clear cache
  151. function clearCache() {
  152. GM_setValue('cache', {});
  153. alert('Cache cleared successfully.');
  154. }
  155.  
  156.  
  157. // Event listener for button click
  158. button.addEventListener('click', () => {
  159. const selectedText = getSelectedText();
  160. if (selectedText) {
  161. let apiKey = GM_getValue('apiKey', null);
  162. let baseUrl = GM_getValue('baseUrl', null);
  163. let voice = GM_getValue('voice', 'onyx'); // Default to 'onyx'
  164. let model = GM_getValue('model', 'tts-1'); // Default to 'tts-1'
  165. if (!baseUrl) {
  166. alert('Please set the base URL for the TTS API in the Tampermonkey menu.');
  167. return;
  168. }
  169. if (!apiKey) {
  170. alert('Please set the API key for the TTS API in the Tampermonkey menu.');
  171. return;
  172. }
  173. setLoadingState(); // Set button to loading state
  174. if (window.location.hostname === 'github.com') {
  175. speakText(selectedText);
  176. resetButton(); // Reset button immediately for built-in TTS
  177. }else {
  178. callOpenAITTS(selectedText, baseUrl, apiKey, voice, model);
  179. }
  180. } else {
  181. alert('Please select some text to read aloud.');
  182. }
  183. });
  184.  
  185. // Show the button near the selected text
  186. document.addEventListener('mouseup', (event) => {
  187. // Check if the mouseup event is triggered by the button itself
  188. if (event.target === button) {
  189. return;
  190. }
  191.  
  192. const selectedText = getSelectedText();
  193. if (selectedText) {
  194. const mouseX = event.pageX;
  195. const mouseY = event.pageY;
  196. button.style.left = `${mouseX + 10}px`;
  197. button.style.top = `${mouseY + 10}px`;
  198. button.style.display = 'block';
  199. } else {
  200. button.style.display = 'none';
  201. }
  202. });
  203.  
  204.  
  205.  
  206. // Initialize UI components
  207. function initModal() {
  208. const modalHTML = `
  209. <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;">
  210. <div style="background: white; padding: 20px; border-radius: 10px; width: 300px;">
  211. <h2>Configure TTS Settings</h2>
  212. <label for="baseUrl">Base URL:</label>
  213. <input type="text" id="baseUrl" value="${GM_getValue('baseUrl', 'https://api.openai.com')}" style="width: 100%;">
  214. <label for="apiKey">API Key:</label>
  215. <input type="text" id="apiKey" value="${GM_getValue('apiKey', '')}" style="width: 100%;">
  216. <label for="model">Model:</label>
  217. <select id="model" style="width: 100%;">
  218. <option value="tts-1">tts-1</option>
  219. <option value="tts-hailuo">tts-hailuo</option>
  220. </select>
  221. <label for="voice">Voice:</label>
  222. <select id="voice" style="width: 100%;">
  223. <option value="alloy">Alloy</option>
  224. <option value="echo">Echo</option>
  225. <option value="fable">Fable</option>
  226. <option value="onyx">Onyx</option>
  227. <option value="nova">Nova</option>
  228. <option value="shimmer">Shimmer</option>
  229. </select>
  230. <button id="saveConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: #007BFF; color: white; border: none; border-radius: 5px;">Save</button>
  231. <button id="cancelConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: grey; color: white; border: none; border-radius: 5px;">Cancel</button>
  232. </div>
  233. </div>
  234. `;
  235. document.body.insertAdjacentHTML('beforeend', modalHTML);
  236. document.getElementById('saveConfig').addEventListener('click', saveConfig);
  237. document.getElementById('cancelConfig').addEventListener('click', closeModal);
  238. document.getElementById('model').addEventListener('change', updateVoiceOptions);
  239. }
  240. function updateVoiceOptions() {
  241. // 获取select元素
  242. var modelSelect = document.getElementById('model');
  243. var voiceSelect = document.getElementById('voice');
  244.  
  245. if (modelSelect.value === 'tts-hailuo') {
  246. // 清空voiceSelect
  247. voiceSelect.innerHTML = `
  248. <option value="male-botong">思远</option>
  249. <option value="Podcast_girl">心悦</option>
  250. <option value="boyan_new_hailuo">子轩</option>
  251. <option value="female-shaonv">灵儿</option>
  252. <option value="YaeMiko_hailuo">语嫣</option>
  253. <option value="xiaoyi_mix_hailuo">少泽</option>
  254. <option value="xiaomo_sft">芷溪</option>
  255. <option value="cove_test2_hailuo">浩翔(英文)</option>
  256. <option value="scarlett_hailuo">雅涵(英文)</option>
  257. <option value="Leishen2_hailuo">雷电将军</option>
  258. <option value="Zhongli_hailuo">钟离</option>
  259. <option value="Paimeng_hailuo">派蒙</option>
  260. <option value="keli_hailuo">可莉</option>
  261. <option value="Hutao_hailuo">胡桃</option>
  262. <option value="Xionger_hailuo">熊二</option>
  263. <option value="Haimian_hailuo">海绵宝宝</option>
  264. <option value="Robot_hunter_hailuo">变形金刚</option>
  265. <option value="Linzhiling_hailuo">小玲玲</option>
  266. <option value="huafei_hailuo">拽妃</option>
  267. <option value="lingfeng_hailuo">东北er</option>
  268. <option value="male_dongbei_hailuo">老铁</option>
  269. <option value="Beijing_hailuo">北京er</option>
  270. <option value="JayChou_hailuo">JayChou</option>
  271. <option value="Daniel_hailuo">潇然</option>
  272. <option value="Bingjiao_zongcai_hailuo">沉韵</option>
  273. <option value="female-yaoyao-hd">瑶瑶</option>
  274. <option value="murong_sft">晨曦</option>
  275. <option value="shangshen_sft">沐珊</option>
  276. <option value="kongchen_sft">祁辰</option>
  277. <option value="shenteng2_hailuo">夏洛特</option>
  278. <option value="Guodegang_hailuo">郭嘚嘚</option>
  279. <option value="yueyue_hailuo">小月月</option>
  280. `;
  281. } else {
  282. // 恢复默认选项
  283. voiceSelect.innerHTML = `
  284. <option value="alloy">Alloy</option>
  285. <option value="echo">Echo</option>
  286. <option value="fable">Fable</option>
  287. <option value="onyx">Onyx</option>
  288. <option value="nova">Nova</option>
  289. <option value="shimmer">Shimmer</option>
  290. `;
  291. }
  292. }
  293.  
  294. function saveConfig() {
  295. const baseUrl = document.getElementById('baseUrl').value;
  296. const model = document.getElementById('model').value;
  297. const apiKey = document.getElementById('apiKey').value;
  298. const voice = document.getElementById('voice').value;
  299. GM_setValue('baseUrl', baseUrl);
  300. GM_setValue('model', model);
  301. GM_setValue('apiKey', apiKey);
  302. GM_setValue('voice', voice);
  303. alert('Settings saved successfully.');
  304. closeModal();
  305. }
  306.  
  307. function closeModal() {
  308. document.getElementById('configModal').style.display = 'none';
  309. }
  310.  
  311. function openModal() {
  312. if (!document.getElementById('configModal')) {
  313. initModal();
  314. }
  315. document.getElementById('configModal').style.display = 'flex';
  316. // Set the current values from the cache
  317. document.getElementById('baseUrl').value = GM_getValue('baseUrl', '');
  318. document.getElementById('apiKey').value = GM_getValue('apiKey', '');
  319. document.getElementById('model').value = GM_getValue('model', 'tts-1');
  320. updateVoiceOptions(); // Ensure voice options are updated based on the model
  321. document.getElementById('voice').value = GM_getValue('voice', 'onyx');
  322. }
  323.  
  324. GM_registerMenuCommand('Configure TTS Settings', openModal);
  325.  
  326. // Register menu command to clear cache
  327. GM_registerMenuCommand('Clear TTS Cache', clearCache);
  328. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址