PI.ai Voice

Adds voice option in PI.ai responses

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         PI.ai Voice
// @namespace    https://github.com/HellFiveOsborn
// @version      2024-05-29
// @description  Adds voice option in PI.ai responses
// @author       HellFive Osborn
// @match        https://pi.ai/threads
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pi.ai
// @compatible          chrome
// @compatible          firefox
// @compatible          edge
// @compatible          opera
// @compatible          brave
// @compatible          vivaldi
// @compatible          waterfox
// @compatible          librewolf
// @compatible          ghost
// @compatible          qq
// @compatible          whale
// @compatible          kiwi
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .animate-spin {
            animation: spin 1s linear infinite;
        }
    `;
    document.head.appendChild(style);

    const originalFetch = window.fetch;
    let audio = null;
    let preferredVoice;

    // set the default value for preferredVoice in localStorage
    if (!localStorage.getItem('preferredVoice')) {
        localStorage.setItem('preferredVoice', 'voice1');
    }

    window.fetch = function(...args) {
        const url = args[0];
        const options = args[1] || {};

        // retrieve the preferredVoice value from localStorage
        preferredVoice = localStorage.getItem('preferredVoice');

        if (typeof url === 'string' && url.startsWith("/api/chat/start") || url.startsWith("/api/user/settings") && (!options.method || options.method.toUpperCase() === 'POST')) {
            return originalFetch.apply(this, arguments).then(response => {
                const clonedResponse = response.clone();
                clonedResponse.text().then(text => {
                    // parse the response text as JSON and update the preferredVoice value if necessary
                    const data = JSON.parse(!url.startsWith("/api/user/settings") ? text : options.body);
                    if (data.preferredVoice && data.preferredVoice !== preferredVoice) {
                        preferredVoice = data.preferredVoice;
                        localStorage.setItem('preferredVoice', preferredVoice);
                    }
                });
                return response;
            });
        }

        if (typeof url === 'string' && url.startsWith("/api/chat/history?conversation=") && (!options.method || options.method.toUpperCase() === 'GET')) {
            return originalFetch.apply(this, arguments).then(response => {
                const clonedResponse = response.clone();
                clonedResponse.text().then(text => {
                    let { messages } = JSON.parse(text);
                    let AIMessages = messages.filter(item => item.direction === "outbound");
                    window.AIMessages = AIMessages.reverse();
                    processNewElements();
                });
                return response;
            });
        }
        return originalFetch.apply(this, arguments);
    };

    function createButton(sid) {
        let button = `<button id="voiceButton" class="w-8 h-8 p-2 flex z-10 items-center text-neutral-800 inset-0 bg-neutral-200 mt-4" style="border-radius:9999px;" type="button" data-sid="${sid}"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5 text-primary-700 lg:h-6 lg:w-6" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" data-darkreader-inline-fill="" data-darkreader-inline-stroke="" style="--darkreader-inline-fill: currentColor; --darkreader-inline-stroke: currentColor;"><path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 001.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06zM18.584 5.106a.75.75 0 011.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 11-1.06-1.06 8.25 8.25 0 000-11.668.75.75 0 010-1.06z"></path><path d="M15.932 7.757a.75.75 0 011.061 0 6 6 0 010 8.486.75.75 0 01-1.06-1.061 4.5 4.5 0 000-6.364.75.75 0 010-1.06z"></path></svg></button>`;
        let btn = document.createElement('div');
        btn.innerHTML = button;
        let buttonElement = btn.firstElementChild;
        buttonElement.addEventListener('click', () => handleButtonClick(buttonElement, sid));
        return buttonElement;
    }

    function handleButtonClick(button, sid) {
        if (audio && !audio.paused) {
            audio.pause();
            audio = null;
            button.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5 text-primary-700 lg:h-6 lg:w-6" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" data-darkreader-inline-fill="" data-darkreader-inline-stroke="" style="--darkreader-inline-fill: currentColor; --darkreader-inline-stroke: currentColor;"><path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 001.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06zM18.584 5.106a.75.75 0 011.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 11-1.06-1.06 8.25 8.25 0 000-11.668.75.75 0 010-1.06z"></path><path d="M15.932 7.757a.75.75 0 011.061 0 6 6 0 010 8.486.75.75 0 01-1.06-1.061 4.5 4.5 0 000-6.364.75.75 0 010-1.06z"></path></svg>`;
            return;
        }

        button.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024" aria-hidden="true" class="h-5 w-5 animate-spin lg:h-6 lg:w-6" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" data-darkreader-inline-fill="" data-darkreader-inline-stroke="" style="--darkreader-inline-fill: currentColor; --darkreader-inline-stroke: currentColor;"><path d="M512 64C265.6 64 64 265.6 64 512s201.6 448 448 448 448-201.6 448-448S758.4 64 512 64zm0 820.8C308.8 884.8 139.2 715.2 139.2 512S308.8 139.2 512 139.2 884.8 308.8 884.8 512 715.2 884.8 512 884.8z" /><path d="M512 139.2V64c-247.2 0-448 200.8-448 448s200.8 448 448 448v-75.2c-204 0-372.8-168.8-372.8-372.8S308 139.2 512 139.2z" /></svg>`;

        fetch(`https://pi.ai/api/chat/voice?mode=eager&voice=${preferredVoice}&messageSid=${sid}`, {
            method: 'GET'
        })
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.blob();
        })
        .then(blob => {
            const url = URL.createObjectURL(blob);
            audio = new Audio(url);
            audio.play();

            button.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5 lg:h-6 lg:w-6" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" data-darkreader-inline-fill="" data-darkreader-inline-stroke="" style="--darkreader-inline-fill: currentColor; --darkreader-inline-stroke: currentColor;"><path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 001.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06zM17.78 9.22a.75.75 0 10-1.06 1.06L18.44 12l-1.72 1.72a.75.75 0 001.06 1.06l1.72-1.72 1.72 1.72a.75.75 0 101.06-1.06L20.56 12l1.72-1.72a.75.75 0 00-1.06-1.06l-1.72 1.72-1.72-1.72z"></path></svg>`;
            audio.onended = () => {
                button.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5 text-primary-700 lg:h-6 lg:w-6" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" data-darkreader-inline-fill="" data-darkreader-inline-stroke="" style="--darkreader-inline-fill: currentColor; --darkreader-inline-stroke: currentColor;"><path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 001.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06zM18.584 5.106a.75.75 0 011.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 11-1.06-1.06 8.25 8.25 0 000-11.668.75.75 0 010-1.06z"></path><path d="M15.932 7.757a.75.75 0 011.061 0 6 6 0 010 8.486.75.75 0 01-1.06-1.061 4.5 4.5 0 000-6.364.75.75 0 010-1.06z"></path></svg>`;
            };
        })
        .catch(error => {
            console.error('There was a problem with the fetch operation:', error);
        });
    }

    function processNewElements() {
        if (!window.AIMessages) return;
        let AIContainers = window.document.querySelectorAll('.break-anywhere:not([class*=" "])');
        AIContainers.forEach((element, index) => {
            if (!element.querySelector('#voiceButton') && window.AIMessages[index]) {
                let sid = window.AIMessages[index].sid;
                let button = createButton(sid);
                let contentMenu = document.createElement('div');
                contentMenu.classList.add('flex', 'items-center', 'gap-2'); // add Tailwind CSS classes

                // move data-projection element to the new div
                let dataProjection = element.querySelector("div[data-projection-id]");
                if (dataProjection) {
                    contentMenu.appendChild(dataProjection);
                }

                // append button and contentMenu to the element
                contentMenu.appendChild(button);
                element.appendChild(contentMenu);
            }
        });
    }

    const observer = new MutationObserver((mutationsList) => {
        for (let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                processNewElements();
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    function checkForUpdate() {
        var scriptUrl = 'https://update.greasyfork.org/scripts/496302/PIai%20Voice.user.js';

        GM_xmlhttpRequest({
            method: 'GET',
            url: scriptUrl,
            headers: {
                'Accept': 'text/plain'
            },
            onload: function(response) {
                var lastModified = new Date(response.responseHeaders.get('Last-Modified')).getTime();

                var lastChecked = localStorage.getItem('PIaiVoiceLastChecked');

                if (!lastChecked || lastModified > lastChecked) {
                    localStorage.setItem('PIaiVoiceLastChecked', lastModified);
                    GM_notification({
                        text: 'An update to the PI.ai Voice script is available on GreasyFork. Visit <https://greasyfork.org/scripts/496302-PIai-Voice> for more information.',
                        title: 'PI.ai Voice: Update available 🚀',
                        timeout: 10000
                    });
                }
            },
            onerror: function() {
                console.error('Failed to check for updates to the PI.ai Voice script.');
            }
        });
    }

    // Calls checkForUpdate() function every 24 hours
    setInterval(checkForUpdate, 24 * 60 * 60 * 1000);

    console.log("Fetch request interceptor and MutationObserver have been set up.");
})();