vits-simple-api for SillyTavern

Add a button to each chat message to play it using TTS API

// ==UserScript==
// @name         vits-simple-api for SillyTavern
// @namespace    https://github.com/yujianke100/vits-simple-api-for-SillyTavern
// @version      1.0.3
// @license MIT
// @description  Add a button to each chat message to play it using TTS API
// @author       yujianke100
// @match        http://*:8080*
// @match http://localhost:8080/*
// @match http://127.0.0.1:8080/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    let currentAudio = null;
    let readQuotesOnly = false;
    let toggleSwitches = [];
    let apiDomain = localStorage.getItem('ttsApiDomain') || 'http://127.0.0.1:23456';

    // Function to add TTS/Stop button and a Toggle button to each chat message
    function addTTSButton(message) {
        if (!message.querySelector('.mytts-button')) {
            const ttsButton = document.createElement('button');
            ttsButton.innerText = 'TTS';
            ttsButton.classList.add('mytts-button', 'action-button');
            updateTTSButtonStyle(ttsButton, false);

            const toggleButton = document.createElement('button');
            toggleButton.innerText = readQuotesOnly ? 'Quotes' : 'All';
            toggleButton.classList.add('toggle-button', 'action-button');
            updateToggleButtonStyle(toggleButton);

            const handleToggleChange = () => {
                readQuotesOnly = !readQuotesOnly;
                console.log('Read Quotes Only:', readQuotesOnly);
                toggleSwitches.forEach(btn => {
                    btn.innerText = readQuotesOnly ? 'Quotes' : 'All';
                    updateToggleButtonStyle(btn);
                });
            };

            toggleButton.addEventListener('click', handleToggleChange);

            toggleSwitches.push(toggleButton);

            ttsButton.addEventListener('click', () => {
                if (currentAudio && !currentAudio.paused) {
                    currentAudio.pause();
                    currentAudio.currentTime = 0;
                    console.log('Audio stopped');
                    updateTTSButtonStyle(ttsButton, false);
                } else {
                    console.log('TTS button clicked');
                    let messageText = message.querySelector('.mes_text').innerText;
                    if (readQuotesOnly) {
                        const quotes = messageText.match(/“([^”]*)”/g);
                        messageText = quotes ? quotes.map(quote => quote.replace(/“|”/g, '')).join(' ') : '';
                    }
                    console.log('TTS for:', messageText);
                    playTTS(messageText, ttsButton);
                }
            });

            const buttonsContainer = message.querySelector('.mes_buttons');
            if (buttonsContainer) {
                buttonsContainer.appendChild(toggleButton);
                buttonsContainer.appendChild(ttsButton);
                console.log('TTS/Stop button and Toggle button added');
            }
        }
    }

    // Function to update the toggle button style
    function updateToggleButtonStyle(button) {
        if (readQuotesOnly) {
            button.style.backgroundColor = 'green';
            button.style.color = 'white';
        } else {
            button.style.backgroundColor = 'white';
            button.style.color = 'black';
        }
    }

    // Function to update the TTS button style
    function updateTTSButtonStyle(button, isPlaying) {
        if (isPlaying) {
            button.innerText = 'Stop';
            button.style.backgroundColor = 'green';
            button.style.color = 'white';
        } else {
            button.innerText = 'TTS';
            button.style.backgroundColor = 'white';
            button.style.color = 'black';
        }
    }

    // Function to send text to TTS API and play response
    function playTTS(text, ttsButton) {
        console.log('Sending text to TTS API:', text);
        const apiUrl = `${apiDomain}/voice/bert-vits2?text=${encodeURIComponent(text)}&id=0&noise=0.5&noisew=0.5`;

        GM_xmlhttpRequest({
            method: 'GET',
            url: apiUrl,
            responseType: 'blob', // Expecting a binary response
            onload: function(response) {
                // Convert the response Blob to a URL
                const audioUrl = URL.createObjectURL(response.response);
                if (currentAudio) {
                    currentAudio.pause();
                    currentAudio.currentTime = 0;
                }
                currentAudio = new Audio(audioUrl);
                currentAudio.play().catch(e => console.error('Audio play error:', e));
                currentAudio.onplay = () => updateTTSButtonStyle(ttsButton, true);
                currentAudio.onpause = () => updateTTSButtonStyle(ttsButton, false);
                currentAudio.onended = () => updateTTSButtonStyle(ttsButton, false);
            },
            onerror: function(error) {
                console.error('TTS API Error:', error);
            }
        });
    }

    // Function to add the settings button to the chat container
    function addSettingsButton() {
        const chatContainer = document.querySelector('#chat');
        if (chatContainer && !chatContainer.querySelector('.settings-button')) {
            const settingsButton = document.createElement('button');
            settingsButton.innerText = 'TTS API Setting';
            settingsButton.classList.add('settings-button', 'action-button');
            settingsButton.addEventListener('click', () => {
                const newDomain = prompt('Enter TTS API Domain:', apiDomain);
                if (newDomain) {
                    apiDomain = newDomain;
                    localStorage.setItem('ttsApiDomain', apiDomain);
                    console.log('API Domain updated to:', apiDomain);
                }
            });
            chatContainer.appendChild(settingsButton);
        }
    }

    // Adding CSS styles for the buttons
    const style = document.createElement('style');
    style.innerHTML = `
        .action-button {
            background-color: gray;
            color: white;
            border: none;
            cursor: pointer;
        }

        .action-button:hover {
            background-color: lightgray;
        }

        .mytts-button.playing {
            background-color: green;
        }

        .settings-button {
            margin-bottom: 10px;
        }
    `;
    document.head.appendChild(style);

    // Using MutationObserver to dynamically add buttons to new messages and the settings button
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes) {
                mutation.addedNodes.forEach(node => {
                    if (node.classList && node.classList.contains('mes')) {
                        addTTSButton(node);
                    }
                    if (node.id === 'chat') {
                        addSettingsButton();
                    }
                });
            }
        });
    });

    // Start observing
    const config = { childList: true, subtree: true };
    observer.observe(document.body, config);

    // Initial settings button addition
    addSettingsButton();
})();