OpenAI TTS API Converter

调用第三方站点api将选中的文本或输入的文本转换为语音并下载

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         OpenAI TTS API Converter
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  调用第三方站点api将选中的文本或输入的文本转换为语音并下载
// @author       finch
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 加载 Font Awesome CSS
    const fontAwesomeLink = document.createElement('link');
    fontAwesomeLink.rel = 'stylesheet';
    fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css';
    document.head.appendChild(fontAwesomeLink);

    // 获取用户配置
    function getUserConfig() {
        return {
            bearerToken: localStorage.getItem('tts_bearerToken') || '',
            postUrl: localStorage.getItem('tts_postUrl') || '',
            model: localStorage.getItem('tts_model') || '',
            voice: localStorage.getItem('tts_voice') || ''
        };
    }

    // 设置用户配置
    function setUserConfig(config) {
        localStorage.setItem('tts_bearerToken', config.bearerToken);
        localStorage.setItem('tts_postUrl', config.postUrl);
        localStorage.setItem('tts_model', config.model);
        localStorage.setItem('tts_voice', config.voice);
    }

    // 创建设置对话框
    function createSettingsDialog() {
        const dialog = document.createElement('div');
        dialog.style.position = 'fixed';
        dialog.style.top = '50%';
        dialog.style.left = '50%';
        dialog.style.transform = 'translate(-50%, -50%)';
        dialog.style.backgroundColor = 'white';
        dialog.style.padding = '20px';
        dialog.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3)';
        dialog.style.zIndex = 10000;
        dialog.style.display = 'none';
        dialog.style.borderRadius = '8px';
        dialog.style.maxWidth = '400px';
        dialog.style.width = '100%';

        const config = getUserConfig();

        const title = document.createElement('h2');
        title.textContent = '设置 TTS API 配置';
        title.style.marginTop = '0';
        title.style.marginBottom = '20px';
        title.style.textAlign = 'center';
        dialog.appendChild(title);

        const form = document.createElement('form');
        form.style.display = 'flex';
        form.style.flexDirection = 'column';
        form.style.gap = '10px';

        const createInputField = (labelText, inputValue) => {
            const label = document.createElement('label');
            label.textContent = labelText;
            label.style.fontWeight = 'bold';
            const input = document.createElement('input');
            input.type = 'text';
            input.value = inputValue;
            input.style.width = '100%';
            input.style.padding = '8px';
            input.style.border = '1px solid #ccc';
            input.style.borderRadius = '4px';
            label.appendChild(input);
            return { label, input };
        };

        const bearerTokenField = createInputField('Bearer 令牌:', config.bearerToken);
        const postUrlField = createInputField('POST URL:', config.postUrl);
        const modelField = createInputField('模型名称:', config.model);
        const voiceField = createInputField('声音名称:', config.voice);

        form.appendChild(bearerTokenField.label);
        form.appendChild(postUrlField.label);
        form.appendChild(modelField.label);
        form.appendChild(voiceField.label);

        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.justifyContent = 'space-between';
        buttonContainer.style.marginTop = '20px';

        const saveButton = document.createElement('button');
        saveButton.textContent = '保存';
        saveButton.style.backgroundColor = '#4CAF50';
        saveButton.style.color = 'white';
        saveButton.style.border = 'none';
        saveButton.style.padding = '10px 20px';
        saveButton.style.borderRadius = '4px';
        saveButton.style.cursor = 'pointer';
        saveButton.addEventListener('click', (e) => {
            e.preventDefault();
            setUserConfig({
                bearerToken: bearerTokenField.input.value,
                postUrl: postUrlField.input.value,
                model: modelField.input.value,
                voice: voiceField.input.value
            });
            dialog.style.display = 'none';
        });

        const cancelButton = document.createElement('button');
        cancelButton.textContent = '取消';
        cancelButton.style.backgroundColor = '#f44336';
        cancelButton.style.color = 'white';
        cancelButton.style.border = 'none';
        cancelButton.style.padding = '10px 20px';
        cancelButton.style.borderRadius = '4px';
        cancelButton.style.cursor = 'pointer';
        cancelButton.addEventListener('click', (e) => {
            e.preventDefault();
            dialog.style.display = 'none';
        });

        buttonContainer.appendChild(saveButton);
        buttonContainer.appendChild(cancelButton);
        form.appendChild(buttonContainer);

        // 添加测试按钮
        const testButton = document.createElement('button');
        testButton.textContent = '点此测试服务';
        testButton.style.marginTop = '10px';
        testButton.style.backgroundColor = '#2196F3';
        testButton.style.color = 'white';
        testButton.style.border = 'none';
        testButton.style.padding = '10px 20px';
        testButton.style.borderRadius = '4px';
        testButton.style.cursor = 'pointer';
        testButton.addEventListener('click', (e) => {
            e.preventDefault();
            testTTSAPI();
        });

        form.appendChild(testButton);

        dialog.appendChild(form);
        document.body.appendChild(dialog);
        return dialog;
    }

    const settingsDialog = createSettingsDialog();

    // 配置菜单
    GM_registerMenuCommand('设置 TTS API 配置', () => {
        settingsDialog.style.display = 'block';
    });

    // 测试 TTS API
    function testTTSAPI() {
        const config = getUserConfig();
        const data = {
            model: config.model,
            input: 'Hello, world',
            voice: config.voice
        };

        GM_xmlhttpRequest({
            method: 'POST',
            url: config.postUrl,
            headers: {
                'Authorization': `Bearer ${config.bearerToken}`,
                'Content-Type': 'application/json'
            },
            data: JSON.stringify(data),
            responseType: 'blob',
            onload: function(response) {
                if (response.status === 200) {
                    alert('TTS API 连通性测试成功!');
                } else {
                    alert('TTS API 连通性测试失败,请检查配置。');
                }
            }
        });
    }

    // 将文本转换为语音并下载
    function convertTextToSpeech(text, download = false) {
        const config = getUserConfig();
        const data = {
            model: config.model,
            input: text,
            voice: config.voice
        };

        GM_xmlhttpRequest({
            method: 'POST',
            url: config.postUrl,
            headers: {
                'Authorization': `Bearer ${config.bearerToken}`,
                'Content-Type': 'application/json'
            },
            data: JSON.stringify(data),
            responseType: 'blob',
            onload: function(response) {
                if (response.status === 200) {
                    const url = URL.createObjectURL(response.response);
                    if (download) {
                        const downloadLink = document.createElement('a');
                        downloadLink.href = url;
                        downloadLink.download = 'tts_audio.mp3'; // 默认文件名
                        downloadLink.click();
                    } else {
                        const audio = new Audio(url);
                        audio.play();
                        // 显示下载按钮
                        downloadButton.style.display = 'block';
                        downloadButton.onclick = () => {
                            const downloadLink = document.createElement('a');
                            downloadLink.href = url;
                            downloadLink.download = 'tts_audio.mp3';
                            downloadLink.click();
                        };
                    }
                } else {
                    console.error(`请求失败,状态码:${response.status}`);
                }
            }
        });
    }

    // 创建按钮容器
    const buttonContainer = document.createElement('div');
    buttonContainer.style.position = 'absolute';
    buttonContainer.style.zIndex = 1000;
    buttonContainer.style.display = 'flex';
    buttonContainer.style.gap = '8px'; // 按钮间距
    buttonContainer.style.display = 'none'; // 初始隐藏
    document.body.appendChild(buttonContainer);

    // 按钮样式
    const buttonStyle = {
        width: '40px',
        height: '40px',
        borderRadius: '50%',
        border: 'none',
        cursor: 'pointer',
        fontSize: '1.5em',
        color: 'white'
    };

    // 创建转换语音按钮
    const playButton = document.createElement('button');
    playButton.innerHTML = '<i class="fas fa-play"></i>'; // 使用 Font Awesome 播放图标
    Object.assign(playButton.style, buttonStyle, { backgroundColor: '#4CAF50' });
    playButton.addEventListener('click', () => {
        const selection = window.getSelection();
        const text = selection.toString();
        if (text) {
            convertTextToSpeech(text);
        } else {
            alert('没有选中文本');
        }
    });
    buttonContainer.appendChild(playButton);

    // 创建下载按钮
    const downloadButton = document.createElement('button');
    downloadButton.innerHTML = '<i class="fas fa-download"></i>'; // 使用 Font Awesome 下载图标
    Object.assign(downloadButton.style, buttonStyle, { backgroundColor: '#2196F3' });
    downloadButton.style.display = 'none'; // 初始隐藏
    buttonContainer.appendChild(downloadButton);

    // 创建输入文本按钮
    const inputButton = document.createElement('button');
    inputButton.innerHTML = '<i class="fas fa-file-alt"></i>'; // 使用 Font Awesome 纸张图标
    Object.assign(inputButton.style, buttonStyle, { backgroundColor: '#FF9800' });
    inputButton.addEventListener('click', () => {
        inputDialog.style.display = 'block';
    });
    buttonContainer.appendChild(inputButton);

    // 创建文本输入框页面
    const inputDialog = document.createElement('div');
    inputDialog.style.position = 'fixed';
    inputDialog.style.top = '50%';
    inputDialog.style.left = '50%';
    inputDialog.style.transform = 'translate(-50%, -50%)';
    inputDialog.style.backgroundColor = 'white';
    inputDialog.style.padding = '20px';
    inputDialog.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3)';
    inputDialog.style.zIndex = 10000;
    inputDialog.style.display = 'none';
    inputDialog.style.borderRadius = '8px';
    inputDialog.style.maxWidth = '400px';
    inputDialog.style.width = '100%';

    const inputTitle = document.createElement('h2');
    inputTitle.textContent = '输入文本进行转换';
    inputTitle.style.marginTop = '0';
    inputTitle.style.marginBottom = '20px';
    inputTitle.style.textAlign = 'center';
    inputDialog.appendChild(inputTitle);

    const inputText = document.createElement('input');
    inputText.type = 'text';
    inputText.placeholder = '输入文本';
    inputText.style.padding = '10px';
    inputText.style.border = '1px solid #ccc';
    inputText.style.borderRadius = '4px';
    inputText.style.width = '100%';
    inputText.style.marginBottom = '20px';
    inputDialog.appendChild(inputText);

    const inputButtonContainer = document.createElement('div');
    inputButtonContainer.style.display = 'flex';
    inputButtonContainer.style.justifyContent = 'space-between';

    const playInputButton = document.createElement('button');
    playInputButton.textContent = '播放';
    playInputButton.style.backgroundColor = '#4CAF50';
    playInputButton.style.color = 'white';
    playInputButton.style.border = 'none';
    playInputButton.style.padding = '10px 20px';
    playInputButton.style.borderRadius = '4px';
    playInputButton.style.cursor = 'pointer';
    playInputButton.addEventListener('click', () => {
        const text = inputText.value.trim();
        if (text) {
            convertTextToSpeech(text);
        } else {
            alert('请输入文本');
        }
    });

    const inputDownloadButton = document.createElement('button');
    inputDownloadButton.textContent = '下载';
    inputDownloadButton.style.backgroundColor = '#4CAF50';
    inputDownloadButton.style.color = 'white';
    inputDownloadButton.style.border = 'none';
    inputDownloadButton.style.padding = '10px 20px';
    inputDownloadButton.style.borderRadius = '4px';
    inputDownloadButton.style.cursor = 'pointer';
    inputDownloadButton.addEventListener('click', () => {
        const text = inputText.value.trim();
        if (text) {
            convertTextToSpeech(text, true);
        } else {
            alert('请输入文本');
        }
    });

    const cancelInputButton = document.createElement('button');
    cancelInputButton.textContent = '取消';
    cancelInputButton.style.backgroundColor = '#f44336';
    cancelInputButton.style.color = 'white';
    cancelInputButton.style.border = 'none';
    cancelInputButton.style.padding = '10px 20px';
    cancelInputButton.style.borderRadius = '4px';
    cancelInputButton.style.cursor = 'pointer';
    cancelInputButton.addEventListener('click', () => {
        inputDialog.style.display = 'none';
    });

    inputButtonContainer.appendChild(playInputButton);
    inputButtonContainer.appendChild(inputDownloadButton);
    inputButtonContainer.appendChild(cancelInputButton);
    inputDialog.appendChild(inputButtonContainer);
    document.body.appendChild(inputDialog);

    // 监听文本选择事件
    document.addEventListener('selectionchange', () => {
        const selection = window.getSelection();
        if (selection.rangeCount > 0) {
            const range = selection.getRangeAt(0);
            const rect = range.getBoundingClientRect();
            if (rect.width > 0 && rect.height > 0) {
                buttonContainer.style.top = `${rect.bottom + window.scrollY}px`;
                buttonContainer.style.left = `${rect.right + window.scrollX}px`;
                buttonContainer.style.display = 'flex';
            } else {
                buttonContainer.style.display = 'none';
                downloadButton.style.display = 'none'; // 隐藏下载按钮
            }
        } else {
            buttonContainer.style.display = 'none';
            downloadButton.style.display = 'none'; // 隐藏下载按钮
        }
    });
})();