OpenAI TTS API Converter

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

// ==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'; // 隐藏下载按钮
        }
    });
})();