Greasy Fork is available in English.

OpenAI TTS Text Reader

Read selected text with OpenAI's TTS API and adjustable volume and speed

21.11.2023 itibariyledir. En son verisyonu görün.

  1. // ==UserScript==
  2. // @name OpenAI TTS Text Reader
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.6
  5. // @description Read selected text with OpenAI's TTS API and adjustable volume and speed
  6. // @include *
  7. // @author wkf16
  8. // @license MIT
  9. // @grant GM_xmlhttpRequest
  10. // @connect api.openai.com
  11. // ==/UserScript==
  12. var YOUR_API_KEY = "sk-"; // 使用您的API密钥
  13. (function() {
  14. 'use strict';
  15. var currentSource = null;
  16. var isPlaying = false;
  17. var audioContext = new AudioContext();
  18. var gainNode = audioContext.createGain();
  19. gainNode.connect(audioContext.destination);
  20. var playbackRate = 1;
  21.  
  22. // 创建按钮
  23. var readButton = document.createElement("button");
  24. styleButton(readButton);
  25. document.body.appendChild(readButton);
  26.  
  27. // 创建并添加按钮文本
  28. var buttonText = document.createElement("span");
  29. buttonText.textContent = ">";
  30. styleButtonText(buttonText);
  31. readButton.appendChild(buttonText);
  32.  
  33. // 创建控制面板
  34. var controlPanel = document.createElement("div");
  35. styleControlPanel(controlPanel);
  36. document.body.appendChild(controlPanel);
  37.  
  38. // 创建并添加音量和速度滑块到控制面板
  39.  
  40. var volumeControl = createSlider("Volume", 0, 1, 0.5, 0.01, function(value) {
  41. gainNode.gain.value = value;
  42. });
  43. controlPanel.appendChild(volumeControl.wrapper);
  44. volumeControl.slider.value = 0.5; // 设置音量滑块的初始值为中间位置
  45. var speedControl = createSlider("Speed\u00A0\u00A0", 0.5, 1.5, 1, 0.05, function(value) { playbackRate = value; });
  46. controlPanel.appendChild(speedControl.wrapper);
  47. speedControl.slider.value = 1; // 设置音量滑块的初始值为中间位置
  48. // 按钮点击事件
  49. readButton.addEventListener('click', function() {
  50. var selectedText = window.getSelection().toString();
  51. console.log("Setting gainNode.gain.value to: ", gainNode.gain.value);
  52. if (isPlaying) {
  53. currentSource.stop(); // 停止当前播放的音频
  54. HideSpinner(buttonText);
  55. } else{
  56. if (selectedText) {
  57. textToSpeech(selectedText);
  58. } else {
  59. alert("请先选择一些文本。");
  60. }
  61. }
  62. });
  63.  
  64. // 创建和样式化控制面板和滑块
  65. function createSlider(labelText, min, max, value, step, onChange) {
  66. // 添加CSS样式到<head>
  67. var wrapper = document.createElement("div");
  68. var label = document.createElement("label");
  69. label.textContent = labelText;
  70. label.style.color = "white";
  71. label.style.textAlign = "left"; // 保持文字左对齐
  72. label.style.flex = "1"; // label会填充除了slider外的空间
  73.  
  74. var slider = document.createElement("input");
  75. slider.type = "range";
  76. slider.min = min;
  77. slider.max = max;
  78. slider.step = step;
  79.  
  80. // 设置wrapper使用Flexbox布局
  81. wrapper.style.display = 'flex';
  82. wrapper.style.alignItems = 'center'; // 垂直居中,但不影响文字
  83. wrapper.style.padding = '8px'; // 根据需要调整,为控件组添加内边距
  84.  
  85. var styleSheet = document.createElement("style");
  86. styleSheet.type = "text/css";
  87. styleSheet.innerText = `
  88. input[type='range'] {
  89. -webkit-appearance: none;
  90. appearance: none;
  91. width: 90%; // 可以根据需要调整滑块的宽度
  92. height: 8px; /* 调整轨道高度 */
  93. border-radius: 8px; /* 轨道边角圆滑 */
  94. background: rgba(255, 255, 255, 0.2); /* 轨道颜色 */
  95. outline: none;
  96. margin-left: 10px; // 为了与label对齐,可以根据需要调整
  97. }
  98.  
  99. input[type='range']::-webkit-slider-thumb {
  100. -webkit-appearance: none;
  101. appearance: none;
  102. width: 16px; /* 把手宽度 */
  103. height: 16px; /* 把手高度 */
  104. border-radius: 50%; /* 把手为圆形 */
  105. background: #4CAF50; /* 把手颜色 */
  106. cursor: pointer;
  107. box-shadow: 0 0 2px #888; /* 把手阴影 */
  108. }
  109.  
  110. input[type='range']:focus::-webkit-slider-thumb {
  111. background: #ccc; /* 把手聚焦时的颜色 */
  112. }
  113. `;
  114. document.head.appendChild(styleSheet);
  115.  
  116. // 创建滑块元素
  117. slider.oninput = function() {
  118. onChange(this.value);
  119. };
  120.  
  121. wrapper.appendChild(label);
  122. wrapper.appendChild(slider);
  123.  
  124. console.log("Setting volume to: ", value);
  125. return { wrapper: wrapper, slider: slider };
  126. }
  127. // 设置控制面板样式
  128. function styleControlPanel(panel) {
  129. panel.style.position = 'fixed';
  130. panel.style.bottom = '20px'; // 与按钮底部对齐
  131. panel.style.right = '80px';
  132. panel.style.width = '200px';
  133. panel.style.background = 'rgba(0, 0, 0, 0.7)';
  134. panel.style.borderRadius = '10px';
  135. panel.style.padding = '10px';
  136. panel.style.boxSizing = 'border-box';
  137. panel.style.visibility = 'hidden';
  138. panel.style.opacity = 0;
  139. panel.style.transition = 'opacity 0.5s, visibility 0.5s';
  140. panel.style.display = 'flex'; // 使用flex布局
  141. panel.style.flexDirection = 'column'; // 确保子元素垂直排列
  142. panel.style.zIndex = '10000';
  143. }
  144.  
  145. // 设置按钮样式
  146. function styleButton(button) {
  147. button.style.position = 'fixed';
  148. button.style.bottom = '20px';
  149. button.style.right = '20px';
  150. button.style.zIndex = '1000';
  151. button.style.width = '40px'; // 按钮宽度
  152. button.style.height = '40px'; // 按钮高度
  153. button.style.borderRadius = '50%'; // 圆形按钮
  154. button.style.backgroundColor = '#4CAF50';
  155. button.style.border = 'none'; // 确保没有边界
  156. button.style.outline = 'none'; // 确保没有轮廓
  157. button.style.cursor = 'pointer';
  158. button.style.transition = 'background-color 0.3s, opacity 0.4s ease';
  159. }
  160.  
  161. function styleButtonText(text) {
  162. text.style.transition = 'opacity 0.4s ease';
  163. text.style.opacity = '1';
  164. text.style.fontSize = "20px";
  165. text.style.textAlign = "center"; // 文本居中
  166. text.style.lineHeight = "40px"; // 设置行高以垂直居中文本
  167. }
  168.  
  169. function createVoiceSelect() {
  170. var selectWrapper = document.createElement("div");
  171. var select = document.createElement("select");
  172. var voices = [ "onyx", "alloy", "echo", "fable", "nova", "shimmer"];
  173.  
  174. for (var i = 0; i < voices.length; i++) {
  175. var option = document.createElement("option");
  176. option.value = voices[i];
  177. option.textContent = voices[i].charAt(0).toUpperCase() + voices[i].slice(1);
  178. select.appendChild(option);
  179. }
  180.  
  181. selectWrapper.appendChild(select);
  182. styleSelect(selectWrapper, select);
  183. return { wrapper: selectWrapper, select: select };
  184. }
  185.  
  186. // 样式化下拉菜单
  187. function styleSelect(wrapper, select) {
  188. wrapper.style.padding = '5px';
  189. wrapper.style.marginBottom = '10px';
  190.  
  191. select.style.width = '100%';
  192. select.style.padding = '8px 10px';
  193. select.style.borderRadius = '8px';
  194. select.style.background = 'rgba(0, 0, 0, 0.7)'; // 调整背景为稍微透明的黑色
  195. select.style.border = '2px solid #4CAF50'; // 添加绿色边框
  196. select.style.color = 'white'; // 白色字体
  197. select.style.fontFamily = 'Arial, sans-serif';
  198. select.style.fontSize = '14px';
  199.  
  200. // 悬停效果
  201. select.onmouseover = function() {
  202. this.style.backgroundColor = 'rgba(50, 50, 50, 50.5)';
  203. };
  204.  
  205. // 鼠标离开效果
  206. select.onmouseout = function() {
  207. this.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  208. };
  209.  
  210. // 聚焦效果
  211. select.onfocus = function() {
  212. this.style.outline = 'none';
  213. this.style.boxShadow = '0 0 5px rgba(81, 203, 238, 1)';
  214. };
  215. var styleSheet = document.createElement("style");
  216. styleSheet.type = "text/css";
  217. styleSheet.innerText = `
  218. select {
  219. /* 为 select 元素本身设置样式 */
  220. }
  221.  
  222. select option {
  223. background: rgba(0, 0, 0, 0.7); /* 选项背景设置为半透明黑色 */
  224. color: white; /* 文字颜色设置为白色 */
  225. }
  226.  
  227. select option:hover {
  228. background: rgba(0, 0, 0, 0.7); /* 悬浮时为半透明白色 */
  229. }
  230. `;
  231. document.head.appendChild(styleSheet);
  232. }
  233.  
  234. // 将音色选择下拉菜单添加到控制面板
  235. var voiceSelect = createVoiceSelect();
  236. controlPanel.appendChild(voiceSelect.wrapper);
  237. function textToSpeech(s) {
  238. var sModelId = "tts-1-hd";
  239. var sVoiceId = voiceSelect.select.value;
  240. var API_KEY = YOUR_API_KEY
  241.  
  242. ShowSpinner(buttonText); // 显示加载指示器
  243.  
  244. GM_xmlhttpRequest({
  245. method: "POST",
  246. url: "https://api.openai.com/v1/audio/speech",
  247. headers: {
  248. "Accept": "audio/mpeg",
  249. "Content-Type": "application/json",
  250. "Authorization": "Bearer " + API_KEY
  251. },
  252. data: JSON.stringify({
  253. model: sModelId,
  254. input: s,
  255. voice: sVoiceId,
  256. speed: playbackRate // 添加speed属性,使用全局变量playbackRate的值
  257. }),
  258. responseType: "arraybuffer",
  259.  
  260. onload: function(response) {
  261. if (response.status === 200) {
  262. HideSpinner(buttonText);
  263. audioContext.decodeAudioData(response.response, function(buffer) {
  264. var source = audioContext.createBufferSource();
  265. source.buffer = buffer;
  266. source.connect(gainNode);
  267. source.start(0);
  268. currentSource = source; // 保存新的音频源
  269. isPlaying = true;
  270. StopSpinner(buttonText); // 更新按钮文本
  271.  
  272. // 监听音频结束事件
  273. source.onended = function() {
  274. isPlaying = false;
  275. //currentSource = null;
  276. HideSpinner(buttonText);
  277. } // 更新按钮文本
  278. }, function(e) {
  279. console.error("Error decoding audio data: ", e);
  280. });
  281. } else {
  282. HideSpinner(buttonText);
  283. console.error("Error loading TTS: ", response.status);
  284. }
  285. },
  286. onerror: function(error) {
  287. HideSpinner(buttonText);
  288. console.error("GM_xmlhttpRequest error: ", error);
  289. }
  290. });
  291. }
  292.  
  293.  
  294.  
  295. // 设置延迟显示和隐藏控制面板的时间(以毫秒为单位)
  296. var panelDisplayDelay = 700; // 700毫秒
  297. var panelHideDelay = 500; // 隐藏延迟时间
  298. var showPanelTimeout, hidePanelTimeout;
  299.  
  300. // 鼠标悬停在按钮上时延迟显示控制面板
  301. readButton.addEventListener('mouseenter', function() {
  302. readButton.style.backgroundColor = '#45a049';
  303. clearTimeout(hidePanelTimeout); // 取消之前的隐藏计时器(如果有)
  304. showPanelTimeout = setTimeout(function() {
  305. controlPanel.style.visibility = 'visible';
  306. controlPanel.style.opacity = 1;
  307. }, panelDisplayDelay);
  308. });
  309.  
  310. // 鼠标离开按钮时延迟隐藏控制面板
  311. readButton.addEventListener('mouseleave', function() {
  312. readButton.style.backgroundColor = '#4CAF50';
  313. clearTimeout(showPanelTimeout); // 取消之前的显示计时器(如果有)
  314. hidePanelTimeout = setTimeout(function() {
  315. controlPanel.style.visibility = 'hidden';
  316. controlPanel.style.opacity = 0;
  317. }, panelHideDelay);
  318. });
  319.  
  320. // 鼠标在控制面板上时保持显示状态
  321. controlPanel.addEventListener('mouseenter', function() {
  322. clearTimeout(hidePanelTimeout); // 取消隐藏计时器
  323. controlPanel.style.visibility = 'visible';
  324. controlPanel.style.opacity = 1;
  325. });
  326.  
  327. // 鼠标离开控制面板时延迟隐藏
  328. controlPanel.addEventListener('mouseleave', function() {
  329. hidePanelTimeout = setTimeout(function() {
  330. controlPanel.style.visibility = 'hidden';
  331. controlPanel.style.opacity = 0;
  332. }, panelHideDelay);
  333. });
  334. speedControl.slider.addEventListener('input', function() {
  335. playbackRate = this.value;
  336. });
  337. function ShowSpinner(text) {
  338. text.style.opacity = '0';
  339. setTimeout(function() {
  340. text.textContent = "...";
  341. text.style.opacity = '1';
  342. }, 400); // 等待与 transition 时间一致
  343. readButton.disabled = true; // 禁用按钮以防止重复点击
  344. }
  345.  
  346. function HideSpinner(text) {
  347. text.style.opacity = '0';
  348. setTimeout(function() {
  349. text.textContent = ">";
  350. text.style.opacity = '1';
  351. }, 400); // 等待与 transition 时间一致
  352. readButton.disabled = false; //
  353. }
  354. function StopSpinner(text) {
  355. text.style.opacity = '0';
  356. setTimeout(function() {
  357. text.textContent = "│▌";
  358. text.style.opacity = '1';
  359. }, 400); // 等待与 transition 时间一致
  360. //readButton.disabled = false; //
  361. }
  362. })();