OpenAI TTS Text Reader (gpt-4o-mini-tts)

Read selected text with OpenAI's TTS API (gpt-4o-mini-tts model) and adjustable volume and speed. Please enter the apikey before using.

  1. // ==UserScript==
  2. // @name OpenAI TTS Text Reader (gpt-4o-mini-tts)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.6.6 // Version incremented due to model change
  5. // @description Read selected text with OpenAI's TTS API (gpt-4o-mini-tts model) and adjustable volume and speed. Please enter the apikey before using.
  6. // @description:ar قراءة النص المحدد باستخدام واجهة برمجة تطبيقات تحويل النص إلى كلام من OpenAI (نموذج gpt-4o-mini-tts) مع إمكانية ضبط مستوى الصوت والسرعة. يرجى إدخال مفتاح الواجهة البرمجية (apikey) قبل الاستخدام.
  7. // @include *
  8. // @author wkf16 (Modified by AI assistant for Bahattab)
  9. // @license MIT
  10. // @grant GM_xmlhttpRequest
  11. // @connect api.openai.com
  12. // @antifeature cross-domain This script makes cross-domain API calls to OpenAI's TTS service, which may have implications for data security and privacy.
  13. // ==/UserScript==
  14. var YOUR_API_KEY = "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"; // استخدم مفتاح الواجهة البرمجية الخاص بك
  15. (function() {
  16. 'use strict';
  17. var currentSource = null;
  18. var isPlaying = false;
  19. var audioContext = new AudioContext();
  20. var gainNode = audioContext.createGain();
  21. gainNode.connect(audioContext.destination);
  22. var playbackRate = 1;
  23.  
  24. // إنشاء الزر
  25. var readButton = document.createElement("button");
  26. styleButton(readButton);
  27. document.body.appendChild(readButton);
  28.  
  29. // إنشاء وإضافة نص الزر
  30. var buttonText = document.createElement("span");
  31. buttonText.textContent = ">";
  32. styleButtonText(buttonText);
  33. readButton.appendChild(buttonText);
  34.  
  35. // إنشاء لوحة التحكم
  36. var controlPanel = document.createElement("div");
  37. styleControlPanel(controlPanel);
  38. document.body.appendChild(controlPanel);
  39.  
  40. // إنشاء وإضافة منزلقات مستوى الصوت والسرعة إلى لوحة التحكم
  41. var volumeControl = createSlider("الصوت", 0, 1, 0.5, 0.01, function(value) {
  42. gainNode.gain.value = value;
  43. });
  44. controlPanel.appendChild(volumeControl.wrapper);
  45. volumeControl.slider.value = 0.5;
  46. var speedControl = createSlider("السرعة", 0.5, 1.5, 1, 0.05, function(value) { playbackRate = value; });
  47. controlPanel.appendChild(speedControl.wrapper);
  48. speedControl.slider.value = 1;
  49.  
  50. // حدث النقر على الزر
  51. readButton.addEventListener('click', function() {
  52. var selectedText = window.getSelection().toString();
  53. console.log("Setting gainNode.gain.value to: ", gainNode.gain.value);
  54. if (isPlaying) {
  55. if (currentSource) {
  56. currentSource.stop(); // إيقاف الصوت قيد التشغيل حاليًا
  57. }
  58. HideSpinner(buttonText); // Reset button to play state
  59. isPlaying = false; // Ensure state is updated
  60. } else{
  61. if (selectedText) {
  62. textToSpeech(selectedText);
  63. } else {
  64. alert("رجاءً، قم بتحديد بعض النص أولاً.");
  65. }
  66. }
  67. });
  68.  
  69. // إنشاء وتنسيق لوحة التحكم والمنزلقات
  70. function createSlider(labelText, min, max, value, step, onChange) {
  71. var wrapper = document.createElement("div");
  72. var label = document.createElement("label");
  73. label.textContent = labelText;
  74. label.style.color = "white";
  75. label.style.textAlign = "right";
  76. label.style.flex = "1";
  77.  
  78. var slider = document.createElement("input");
  79. slider.type = "range";
  80. slider.min = min;
  81. slider.max = max;
  82. slider.step = step;
  83.  
  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; appearance: none;
  93. width: 90%; height: 8px; border-radius: 8px;
  94. background: rgba(255, 255, 255, 0.2); outline: none;
  95. margin-right: 10px; margin-left: 0;
  96. }
  97. input[type='range']::-webkit-slider-thumb {
  98. -webkit-appearance: none; appearance: none;
  99. width: 16px; height: 16px; border-radius: 50%;
  100. background: #4CAF50; cursor: pointer; box-shadow: 0 0 2px #888;
  101. }
  102. input[type='range']:focus::-webkit-slider-thumb { background: #ccc; }
  103. `;
  104. document.head.appendChild(styleSheet);
  105.  
  106. slider.oninput = function() { onChange(this.value); };
  107. wrapper.appendChild(label);
  108. wrapper.appendChild(slider);
  109. return { wrapper: wrapper, slider: slider };
  110. }
  111. // تعيين نمط لوحة التحكم
  112. function styleControlPanel(panel) {
  113. panel.style.position = 'fixed'; panel.style.bottom = '20px';
  114. panel.style.right = '80px'; panel.style.width = '200px';
  115. panel.style.background = 'rgba(0, 0, 0, 0.7)'; panel.style.borderRadius = '10px';
  116. panel.style.padding = '10px'; panel.style.boxSizing = 'border-box';
  117. panel.style.visibility = 'hidden'; panel.style.opacity = 0;
  118. panel.style.transition = 'opacity 0.5s, visibility 0.5s';
  119. panel.style.display = 'flex'; panel.style.flexDirection = 'column';
  120. panel.style.zIndex = '10000';
  121. }
  122.  
  123. // تعيين نمط الزر
  124. function styleButton(button) {
  125. button.style.position = 'fixed'; button.style.bottom = '80px';
  126. button.style.right = '20px'; button.style.zIndex = '1000';
  127. button.style.width = '40px'; button.style.height = '40px';
  128. button.style.borderRadius = '50%'; button.style.backgroundColor = '#4CAF50';
  129. button.style.border = 'none'; button.style.outline = 'none';
  130. button.style.cursor = 'pointer';
  131. button.style.transition = 'background-color 0.3s, opacity 0.4s ease';
  132. }
  133.  
  134. function styleButtonText(text) {
  135. text.style.transition = 'opacity 0.4s ease'; text.style.opacity = '1';
  136. text.style.fontSize = "20px"; text.style.textAlign = "center";
  137. text.style.lineHeight = "40px";
  138. }
  139.  
  140. function createVoiceSelect() {
  141. var selectWrapper = document.createElement("div");
  142. var select = document.createElement("select");
  143. var voices = ["nova", "onyx", "alloy", "echo", "fable", "shimmer"];
  144. var voiceLabel = document.createElement("label");
  145. voiceLabel.textContent = "الصوت:";
  146. voiceLabel.style.color = "white"; voiceLabel.style.marginRight = "5px";
  147. selectWrapper.appendChild(voiceLabel);
  148.  
  149. for (var i = 0; i < voices.length; i++) {
  150. var option = document.createElement("option");
  151. option.value = voices[i];
  152. option.textContent = voices[i].charAt(0).toUpperCase() + voices[i].slice(1);
  153. select.appendChild(option);
  154. }
  155. selectWrapper.appendChild(select);
  156. styleSelect(selectWrapper, select);
  157. return { wrapper: selectWrapper, select: select };
  158. }
  159.  
  160. // تنسيق القائمة المنسدلة
  161. function styleSelect(wrapper, select) {
  162. wrapper.style.padding = '5px'; wrapper.style.marginBottom = '10px';
  163. wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center';
  164.  
  165. select.style.flexGrow = '1'; select.style.padding = '8px 10px';
  166. select.style.borderRadius = '8px'; select.style.background = 'rgba(0, 0, 0, 0.7)';
  167. select.style.border = '2px solid #4CAF50'; select.style.color = 'white';
  168. select.style.fontFamily = 'Arial, sans-serif'; select.style.fontSize = '14px';
  169. select.style.direction = 'ltr';
  170.  
  171. select.onmouseover = function() { this.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'; };
  172. select.onmouseout = function() { this.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; };
  173. select.onfocus = function() { this.style.outline = 'none'; this.style.boxShadow = '0 0 5px rgba(81, 203, 238, 1)'; };
  174.  
  175. var styleSheet = document.createElement("style");
  176. styleSheet.type = "text/css";
  177. styleSheet.innerText = `
  178. select option { background: rgba(0, 0, 0, 0.9); color: white; }
  179. select option:checked { background: #4CAF50; color: white; }
  180. select option:hover { background: rgba(50, 50, 50, 0.8); color: white; }
  181. `;
  182. document.head.appendChild(styleSheet);
  183. }
  184.  
  185. // إضافة القائمة المنسدلة لاختيار الصوت إلى لوحة التحكم
  186. var voiceSelect = createVoiceSelect();
  187. controlPanel.appendChild(voiceSelect.wrapper);
  188.  
  189. function textToSpeech(s) {
  190. // ----- السطر الذي تم تعديله -----
  191. var sModelId = "gpt-4o-mini-tts"; // Changed from "tts-1" to "gpt-4o-mini-tts" for higher quality
  192. // ----- نهاية السطر المعدل -----
  193. var sVoiceId = voiceSelect.select.value;
  194. var API_KEY = YOUR_API_KEY; // Make sure this is set correctly at the top
  195.  
  196. // Ensure API Key is present
  197. if (!API_KEY || API_KEY === "YOUR_API_KEY_HERE" || API_KEY.length < 10) {
  198. alert("يرجى إدخال مفتاح واجهة برمجة تطبيقات OpenAI صالح في بداية البرنامج النصي.");
  199. HideSpinner(buttonText);
  200. return;
  201. }
  202.  
  203.  
  204. ShowSpinner(buttonText); // إظهار مؤشر التحميل
  205.  
  206. GM_xmlhttpRequest({
  207. method: "POST",
  208. url: "https://api.openai.com/v1/audio/speech",
  209. headers: {
  210. "Accept": "audio/mpeg",
  211. "Content-Type": "application/json",
  212. "Authorization": "Bearer " + API_KEY
  213. },
  214. data: JSON.stringify({
  215. model: sModelId, // Now uses "gpt-4o-mini-tts"
  216. input: s,
  217. voice: sVoiceId,
  218. speed: playbackRate
  219. }),
  220. responseType: "arraybuffer",
  221.  
  222. onload: function(response) {
  223. if (response.status === 200) {
  224. // Hide spinner isn't needed here, StopSpinner handles the transition
  225. audioContext.decodeAudioData(response.response, function(buffer) {
  226. var source = audioContext.createBufferSource();
  227. source.buffer = buffer;
  228. source.connect(gainNode);
  229. source.start(0);
  230. currentSource = source; // حفظ مصدر الصوت الجديد
  231. isPlaying = true;
  232. StopSpinner(buttonText); // تحديث نص الزر إلى حالة الإيقاف المؤقت
  233.  
  234. // الاستماع لحدث انتهاء الصوت
  235. source.onended = function() {
  236. isPlaying = false;
  237. currentSource = null; // Clear the source
  238. HideSpinner(buttonText); // تحديث نص الزر إلى حالة التشغيل
  239. }
  240. }, function(e) {
  241. console.error("Error decoding audio data: ", e);
  242. HideSpinner(buttonText); // Ensure spinner hides on decode error
  243. alert("حدث خطأ أثناء معالجة الصوت.");
  244. });
  245. } else {
  246. HideSpinner(buttonText);
  247. console.error("Error loading TTS: ", response.status, response.statusText, response.response);
  248. try {
  249. var errorResponse = JSON.parse(new TextDecoder("utf-8").decode(response.response));
  250. console.error("OpenAI Error:", errorResponse);
  251. // Check for specific common errors
  252. if (response.status === 401) {
  253. alert("خطأ في المصادقة (401). يرجى التحقق من مفتاح الواجهة البرمجية (API Key).");
  254. } else if (errorResponse.error?.message) {
  255. alert("خطأ من OpenAI: " + errorResponse.error.message);
  256. } else {
  257. alert("حدث خطأ أثناء الاتصال بخدمة تحويل النص إلى كلام. الرمز: " + response.status);
  258. }
  259. } catch (e) {
  260. alert("حدث خطأ أثناء الاتصال بخدمة تحويل النص إلى كلام. الرمز: " + response.status);
  261. }
  262. }
  263. },
  264. onerror: function(error) {
  265. HideSpinner(buttonText);
  266. console.error("GM_xmlhttpRequest error: ", error);
  267. alert("حدث خطأ في الشبكة أو في طلب الواجهة البرمجية.");
  268. }
  269. });
  270. }
  271.  
  272. // تأخير عرض وإخفاء لوحة التحكم
  273. var panelDisplayDelay = 700;
  274. var panelHideDelay = 500;
  275. var showPanelTimeout, hidePanelTimeout;
  276.  
  277. readButton.addEventListener('mouseenter', function() {
  278. readButton.style.backgroundColor = '#45a049';
  279. clearTimeout(hidePanelTimeout);
  280. showPanelTimeout = setTimeout(function() {
  281. controlPanel.style.visibility = 'visible';
  282. controlPanel.style.opacity = 1;
  283. }, panelDisplayDelay);
  284. });
  285.  
  286. readButton.addEventListener('mouseleave', function() {
  287. readButton.style.backgroundColor = '#4CAF50';
  288. clearTimeout(showPanelTimeout);
  289. hidePanelTimeout = setTimeout(function() {
  290. if (!controlPanel.matches(':hover')) {
  291. controlPanel.style.visibility = 'hidden';
  292. controlPanel.style.opacity = 0;
  293. }
  294. }, panelHideDelay);
  295. });
  296.  
  297. controlPanel.addEventListener('mouseenter', function() {
  298. clearTimeout(hidePanelTimeout);
  299. controlPanel.style.visibility = 'visible';
  300. controlPanel.style.opacity = 1;
  301. });
  302.  
  303. controlPanel.addEventListener('mouseleave', function() {
  304. hidePanelTimeout = setTimeout(function() {
  305. controlPanel.style.visibility = 'hidden';
  306. controlPanel.style.opacity = 0;
  307. }, panelHideDelay);
  308. });
  309. speedControl.slider.addEventListener('input', function() {
  310. playbackRate = this.value;
  311. });
  312.  
  313. function ShowSpinner(text) {
  314. text.style.opacity = '0';
  315. setTimeout(function() {
  316. text.textContent = "...";
  317. text.style.opacity = '1';
  318. }, 400);
  319. readButton.disabled = true;
  320. }
  321.  
  322. function HideSpinner(text) { // Resets button to 'Play' state
  323. text.style.opacity = '0';
  324. setTimeout(function() {
  325. text.textContent = ">";
  326. text.style.opacity = '1';
  327. }, 400);
  328. readButton.disabled = false;
  329. }
  330. function StopSpinner(text) { // Sets button to 'Stop' state
  331. text.style.opacity = '0';
  332. setTimeout(function() {
  333. text.textContent = "❚❚";
  334. text.style.opacity = '1';
  335. }, 400);
  336. readButton.disabled = false; // Keep button enabled to allow stopping
  337. }
  338. })();