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.3
  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) {
  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: 'tts-1',
  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. if (!baseUrl) {
  165. alert('Please set the base URL for the TTS API in the Tampermonkey menu.');
  166. return;
  167. }
  168. if (!apiKey) {
  169. alert('Please set the API key for the TTS API in the Tampermonkey menu.');
  170. return;
  171. }
  172. setLoadingState(); // Set button to loading state
  173. if (window.location.hostname === 'github.com') {
  174. speakText(selectedText);
  175. resetButton(); // Reset button immediately for built-in TTS
  176. }else {
  177. callOpenAITTS(selectedText, baseUrl, apiKey, voice);
  178. }
  179. } else {
  180. alert('Please select some text to read aloud.');
  181. }
  182. });
  183.  
  184. // Show the button near the selected text
  185. document.addEventListener('mouseup', (event) => {
  186. // Check if the mouseup event is triggered by the button itself
  187. if (event.target === button) {
  188. return;
  189. }
  190.  
  191. const selectedText = getSelectedText();
  192. if (selectedText) {
  193. const mouseX = event.pageX;
  194. const mouseY = event.pageY;
  195. button.style.left = `${mouseX + 10}px`;
  196. button.style.top = `${mouseY + 10}px`;
  197. button.style.display = 'block';
  198. } else {
  199. button.style.display = 'none';
  200. }
  201. });
  202.  
  203. // Initialize UI components
  204. function initModal() {
  205. const modalHTML = `
  206. <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;">
  207. <div style="background: white; padding: 20px; border-radius: 10px; width: 300px;">
  208. <h2>Configure TTS Settings</h2>
  209. <label for="baseUrl">Base URL:</label>
  210. <input type="text" id="baseUrl" value="${GM_getValue('baseUrl', 'https://api.openai.com')}" style="width: 100%;">
  211. <label for="apiKey">API Key:</label>
  212. <input type="text" id="apiKey" value="${GM_getValue('apiKey', '')}" style="width: 100%;">
  213. <label for="voice">Voice:</label>
  214. <select id="voice" style="width: 100%;">
  215. <option value="alloy">Alloy</option>
  216. <option value="echo">Echo</option>
  217. <option value="fable">Fable</option>
  218. <option value="onyx">Onyx</option>
  219. <option value="nova">Nova</option>
  220. <option value="shimmer">Shimmer</option>
  221. </select>
  222. <button id="saveConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: #007BFF; color: white; border: none; border-radius: 5px;">Save</button>
  223. <button id="cancelConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: grey; color: white; border: none; border-radius: 5px;">Cancel</button>
  224. </div>
  225. </div>
  226. `;
  227. document.body.insertAdjacentHTML('beforeend', modalHTML);
  228.  
  229. document.getElementById('saveConfig').addEventListener('click', saveConfig);
  230. document.getElementById('cancelConfig').addEventListener('click', closeModal);
  231. }
  232.  
  233. function saveConfig() {
  234. const baseUrl = document.getElementById('baseUrl').value;
  235. const apiKey = document.getElementById('apiKey').value;
  236. const voice = document.getElementById('voice').value;
  237. GM_setValue('baseUrl', baseUrl);
  238. GM_setValue('apiKey', apiKey);
  239. GM_setValue('voice', voice);
  240. alert('Settings saved successfully.');
  241. closeModal();
  242. }
  243.  
  244. function closeModal() {
  245. document.getElementById('configModal').style.display = 'none';
  246. }
  247.  
  248. function openModal() {
  249. if (!document.getElementById('configModal')) {
  250. initModal();
  251. }
  252. document.getElementById('configModal').style.display = 'flex';
  253. }
  254.  
  255. GM_registerMenuCommand('Configure TTS Settings', openModal);
  256.  
  257. // Register menu command to clear cache
  258. GM_registerMenuCommand('Clear TTS Cache', clearCache);
  259. })();

QingJ © 2025

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