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.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name             OpenAI TTS Text Reader (gpt-4o-mini-tts)
// @namespace        http://tampermonkey.net/
// @version          2.6.6 // Version incremented due to model change
// @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.
// @description:ar   قراءة النص المحدد باستخدام واجهة برمجة تطبيقات تحويل النص إلى كلام من OpenAI (نموذج gpt-4o-mini-tts) مع إمكانية ضبط مستوى الصوت والسرعة. يرجى إدخال مفتاح الواجهة البرمجية (apikey) قبل الاستخدام.
// @include          *
// @author           wkf16 (Modified by AI assistant for Bahattab)
// @license          MIT
// @grant            GM_xmlhttpRequest
// @connect          api.openai.com
// @antifeature cross-domain This script makes cross-domain API calls to OpenAI's TTS service, which may have implications for data security and privacy.
// ==/UserScript==
var YOUR_API_KEY = "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"; // استخدم مفتاح الواجهة البرمجية الخاص بك
(function() {
    'use strict';
    var currentSource = null;
    var isPlaying = false;
    var audioContext = new AudioContext();
    var gainNode = audioContext.createGain();
    gainNode.connect(audioContext.destination);
    var playbackRate = 1;

    // إنشاء الزر
    var readButton = document.createElement("button");
    styleButton(readButton);
    document.body.appendChild(readButton);

    // إنشاء وإضافة نص الزر
    var buttonText = document.createElement("span");
    buttonText.textContent = ">";
    styleButtonText(buttonText);
    readButton.appendChild(buttonText);

    // إنشاء لوحة التحكم
    var controlPanel = document.createElement("div");
    styleControlPanel(controlPanel);
    document.body.appendChild(controlPanel);

    // إنشاء وإضافة منزلقات مستوى الصوت والسرعة إلى لوحة التحكم
    var volumeControl = createSlider("الصوت", 0, 1, 0.5, 0.01, function(value) {
        gainNode.gain.value = value;
    });
    controlPanel.appendChild(volumeControl.wrapper);
    volumeControl.slider.value = 0.5;
    var speedControl = createSlider("السرعة", 0.5, 1.5, 1, 0.05, function(value) { playbackRate = value; });
    controlPanel.appendChild(speedControl.wrapper);
    speedControl.slider.value = 1;

    // حدث النقر على الزر
    readButton.addEventListener('click', function() {
        var selectedText = window.getSelection().toString();
        console.log("Setting gainNode.gain.value to: ", gainNode.gain.value);
        if (isPlaying) {
            if (currentSource) {
                currentSource.stop(); // إيقاف الصوت قيد التشغيل حاليًا
            }
            HideSpinner(buttonText); // Reset button to play state
            isPlaying = false; // Ensure state is updated
        } else{
            if (selectedText) {
                textToSpeech(selectedText);
            } else {
                alert("رجاءً، قم بتحديد بعض النص أولاً.");
            }
       }
    });

    // إنشاء وتنسيق لوحة التحكم والمنزلقات
    function createSlider(labelText, min, max, value, step, onChange) {
        var wrapper = document.createElement("div");
        var label = document.createElement("label");
        label.textContent = labelText;
        label.style.color = "white";
        label.style.textAlign = "right";
        label.style.flex = "1";

        var slider = document.createElement("input");
        slider.type = "range";
        slider.min = min;
        slider.max = max;
        slider.step = step;

        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.padding = '8px';

        var styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = `
        input[type='range'] {
            -webkit-appearance: none; appearance: none;
            width: 90%; height: 8px; border-radius: 8px;
            background: rgba(255, 255, 255, 0.2); outline: none;
            margin-right: 10px; margin-left: 0;
        }
        input[type='range']::-webkit-slider-thumb {
            -webkit-appearance: none; appearance: none;
            width: 16px; height: 16px; border-radius: 50%;
            background: #4CAF50; cursor: pointer; box-shadow: 0 0 2px #888;
        }
        input[type='range']:focus::-webkit-slider-thumb { background: #ccc; }
        `;
        document.head.appendChild(styleSheet);

        slider.oninput = function() { onChange(this.value); };
        wrapper.appendChild(label);
        wrapper.appendChild(slider);
        return { wrapper: wrapper, slider: slider };
    }
    // تعيين نمط لوحة التحكم
    function styleControlPanel(panel) {
        panel.style.position = 'fixed'; panel.style.bottom = '20px';
        panel.style.right = '80px'; panel.style.width = '200px';
        panel.style.background = 'rgba(0, 0, 0, 0.7)'; panel.style.borderRadius = '10px';
        panel.style.padding = '10px'; panel.style.boxSizing = 'border-box';
        panel.style.visibility = 'hidden'; panel.style.opacity = 0;
        panel.style.transition = 'opacity 0.5s, visibility 0.5s';
        panel.style.display = 'flex'; panel.style.flexDirection = 'column';
        panel.style.zIndex = '10000';
    }

    // تعيين نمط الزر
    function styleButton(button) {
        button.style.position = 'fixed'; button.style.bottom = '80px';
        button.style.right = '20px'; button.style.zIndex = '1000';
        button.style.width = '40px'; button.style.height = '40px';
        button.style.borderRadius = '50%'; button.style.backgroundColor = '#4CAF50';
        button.style.border = 'none'; button.style.outline = 'none';
        button.style.cursor = 'pointer';
        button.style.transition = 'background-color 0.3s, opacity 0.4s ease';
    }

    function styleButtonText(text) {
        text.style.transition = 'opacity 0.4s ease'; text.style.opacity = '1';
        text.style.fontSize = "20px"; text.style.textAlign = "center";
        text.style.lineHeight = "40px";
    }

    function createVoiceSelect() {
        var selectWrapper = document.createElement("div");
        var select = document.createElement("select");
        var voices = ["nova", "onyx", "alloy", "echo", "fable", "shimmer"];
        var voiceLabel = document.createElement("label");
        voiceLabel.textContent = "الصوت:";
        voiceLabel.style.color = "white"; voiceLabel.style.marginRight = "5px";
        selectWrapper.appendChild(voiceLabel);

        for (var i = 0; i < voices.length; i++) {
            var option = document.createElement("option");
            option.value = voices[i];
            option.textContent = voices[i].charAt(0).toUpperCase() + voices[i].slice(1);
            select.appendChild(option);
        }
        selectWrapper.appendChild(select);
        styleSelect(selectWrapper, select);
        return { wrapper: selectWrapper, select: select };
    }

    // تنسيق القائمة المنسدلة
    function styleSelect(wrapper, select) {
        wrapper.style.padding = '5px'; wrapper.style.marginBottom = '10px';
        wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center';

        select.style.flexGrow = '1'; select.style.padding = '8px 10px';
        select.style.borderRadius = '8px'; select.style.background = 'rgba(0, 0, 0, 0.7)';
        select.style.border = '2px solid #4CAF50'; select.style.color = 'white';
        select.style.fontFamily = 'Arial, sans-serif'; select.style.fontSize = '14px';
        select.style.direction = 'ltr';

        select.onmouseover = function() { this.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'; };
        select.onmouseout = function() { this.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; };
        select.onfocus = function() { this.style.outline = 'none'; this.style.boxShadow = '0 0 5px rgba(81, 203, 238, 1)'; };

        var styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = `
        select option { background: rgba(0, 0, 0, 0.9); color: white; }
        select option:checked { background: #4CAF50; color: white; }
        select option:hover { background: rgba(50, 50, 50, 0.8); color: white; }
        `;
        document.head.appendChild(styleSheet);
    }

    // إضافة القائمة المنسدلة لاختيار الصوت إلى لوحة التحكم
    var voiceSelect = createVoiceSelect();
    controlPanel.appendChild(voiceSelect.wrapper);

    function textToSpeech(s) {
        // ----- السطر الذي تم تعديله -----
        var sModelId = "gpt-4o-mini-tts"; // Changed from "tts-1" to "gpt-4o-mini-tts" for higher quality
        // ----- نهاية السطر المعدل -----
        var sVoiceId = voiceSelect.select.value;
        var API_KEY = YOUR_API_KEY; // Make sure this is set correctly at the top

        // Ensure API Key is present
         if (!API_KEY || API_KEY === "YOUR_API_KEY_HERE" || API_KEY.length < 10) {
            alert("يرجى إدخال مفتاح واجهة برمجة تطبيقات OpenAI صالح في بداية البرنامج النصي.");
            HideSpinner(buttonText);
            return;
        }


        ShowSpinner(buttonText); // إظهار مؤشر التحميل

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.openai.com/v1/audio/speech",
            headers: {
                "Accept": "audio/mpeg",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + API_KEY
            },
            data: JSON.stringify({
                model: sModelId, // Now uses "gpt-4o-mini-tts"
                input: s,
                voice: sVoiceId,
                speed: playbackRate
            }),
            responseType: "arraybuffer",

            onload: function(response) {
                if (response.status === 200) {
                    // Hide spinner isn't needed here, StopSpinner handles the transition
                    audioContext.decodeAudioData(response.response, function(buffer) {
                        var source = audioContext.createBufferSource();
                        source.buffer = buffer;
                        source.connect(gainNode);
                        source.start(0);
                        currentSource = source; // حفظ مصدر الصوت الجديد
                        isPlaying = true;
                        StopSpinner(buttonText); // تحديث نص الزر إلى حالة الإيقاف المؤقت

                        // الاستماع لحدث انتهاء الصوت
                        source.onended = function() {
                            isPlaying = false;
                            currentSource = null; // Clear the source
                            HideSpinner(buttonText); // تحديث نص الزر إلى حالة التشغيل
                        }
                    }, function(e) {
                        console.error("Error decoding audio data: ", e);
                        HideSpinner(buttonText); // Ensure spinner hides on decode error
                        alert("حدث خطأ أثناء معالجة الصوت.");
                    });
                } else {
                    HideSpinner(buttonText);
                    console.error("Error loading TTS: ", response.status, response.statusText, response.response);
                    try {
                        var errorResponse = JSON.parse(new TextDecoder("utf-8").decode(response.response));
                        console.error("OpenAI Error:", errorResponse);
                         // Check for specific common errors
                        if (response.status === 401) {
                             alert("خطأ في المصادقة (401). يرجى التحقق من مفتاح الواجهة البرمجية (API Key).");
                        } else if (errorResponse.error?.message) {
                             alert("خطأ من OpenAI: " + errorResponse.error.message);
                        } else {
                             alert("حدث خطأ أثناء الاتصال بخدمة تحويل النص إلى كلام. الرمز: " + response.status);
                        }
                    } catch (e) {
                         alert("حدث خطأ أثناء الاتصال بخدمة تحويل النص إلى كلام. الرمز: " + response.status);
                    }
                }
            },
            onerror: function(error) {
                HideSpinner(buttonText);
                console.error("GM_xmlhttpRequest error: ", error);
                alert("حدث خطأ في الشبكة أو في طلب الواجهة البرمجية.");
            }
        });
    }

    // تأخير عرض وإخفاء لوحة التحكم
    var panelDisplayDelay = 700;
    var panelHideDelay = 500;
    var showPanelTimeout, hidePanelTimeout;

    readButton.addEventListener('mouseenter', function() {
        readButton.style.backgroundColor = '#45a049';
        clearTimeout(hidePanelTimeout);
        showPanelTimeout = setTimeout(function() {
            controlPanel.style.visibility = 'visible';
            controlPanel.style.opacity = 1;
        }, panelDisplayDelay);
    });

    readButton.addEventListener('mouseleave', function() {
        readButton.style.backgroundColor = '#4CAF50';
        clearTimeout(showPanelTimeout);
        hidePanelTimeout = setTimeout(function() {
            if (!controlPanel.matches(':hover')) {
                 controlPanel.style.visibility = 'hidden';
                 controlPanel.style.opacity = 0;
            }
        }, panelHideDelay);
    });

    controlPanel.addEventListener('mouseenter', function() {
        clearTimeout(hidePanelTimeout);
        controlPanel.style.visibility = 'visible';
        controlPanel.style.opacity = 1;
    });

    controlPanel.addEventListener('mouseleave', function() {
        hidePanelTimeout = setTimeout(function() {
            controlPanel.style.visibility = 'hidden';
            controlPanel.style.opacity = 0;
        }, panelHideDelay);
    });
    speedControl.slider.addEventListener('input', function() {
        playbackRate = this.value;
    });

    function ShowSpinner(text) {
        text.style.opacity = '0';
        setTimeout(function() {
            text.textContent = "...";
            text.style.opacity = '1';
        }, 400);
        readButton.disabled = true;
    }

    function HideSpinner(text) { // Resets button to 'Play' state
        text.style.opacity = '0';
        setTimeout(function() {
            text.textContent = ">";
            text.style.opacity = '1';
        }, 400);
        readButton.disabled = false;
    }
    function StopSpinner(text) { // Sets button to 'Stop' state
        text.style.opacity = '0';
        setTimeout(function() {
            text.textContent = "❚❚";
            text.style.opacity = '1';
        }, 400);
        readButton.disabled = false; // Keep button enabled to allow stopping
    }
})();