OpenAI TTS Text Reader

使用openai的tts-1阅读选定的文本。使用前请填入apikey

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

QingJ © 2025

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