Say, Pi

Speak to Pi with OpenAI's Whisper

目前為 2023-07-25 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Say, Pi
// @namespace    http://www.saypi.ai/
// @version      1.0.1
// @description  Speak to Pi with OpenAI's Whisper
// @author       Ross Cadogan
// @match        https://pi.ai/*
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const localConfig = {
        webServerUrl: "http://localhost:3000",
        apiServerUrl: "http://localhost:5000",
        // Add other configuration properties as needed
    };

    // Define a global configuration property
    const config = {
        webServerUrl: "https://www.saypi.ai",
        apiServerUrl: "https://api.saypi.ai",
        // Add other configuration properties as needed
    };

    // Create a MutationObserver to listen for changes to the DOM
    var observer = new MutationObserver(function (mutations) {
        // Check each mutation
        for (var i = 0; i < mutations.length; i++) {
            var mutation = mutations[i];

            // If nodes were added, check each one
            if (mutation.addedNodes.length > 0) {
                for (var j = 0; j < mutation.addedNodes.length; j++) {
                    var node = mutation.addedNodes[j];

                    // If the node is the appropriate container element, add the button and stop observing
                    if (node.nodeName.toLowerCase() === 'div' && node.classList.contains('fixed') && node.classList.contains('bottom-16')) {
                        var footer = node;
                        var buttonContainer = footer.querySelector('.relative.flex.flex-col');
                        if (buttonContainer) {
                            addAudioButton(buttonContainer);
                        } else {
                            console.log('No button container found in footer');
                        }
                        observer.disconnect();
                        return;
                    }
                }
            }
        }
    });

    function injectScript(callback) {
        return injectScriptRemote(callback);
    }

    function injectScriptRemote(callback) {
        // Get the URL of the remote script
        var remoteScriptUrl = config.webServerUrl + '/static/js/literal.js';
        GM_xmlhttpRequest({
            method: "GET",
            url: remoteScriptUrl,
            onload: function (response) {
                var scriptElement = document.createElement("script");
                scriptElement.type = "text/javascript";
                scriptElement.id = 'saypi-script';
                const configText = 'var config = ' + JSON.stringify(config) + ';';
                scriptElement.textContent = configText + response.responseText;
                document.body.appendChild(scriptElement);

                // Call the callback function after the script is added
                if (callback) {
                    callback();
                }
            }
        });
    }

    function injectScriptLocal(callback) {
        var scriptElement = document.createElement("script");
        scriptElement.type = "text/javascript";
        scriptElement.id = 'saypi-script';
        const scriptText = `
        // Paste the contents of static/js/literal.js here to avoid CORS issues
        `
        const configText = 'var config = ' + JSON.stringify(config) + ';';
        scriptElement.textContent = configText + scriptText;
        document.body.appendChild(scriptElement);

        // Call the callback function after the script is added
        if (callback) {
            callback();
        }
    }

    function addAudioButton(container) {
        var button = document.createElement('button');
        button.id = 'talkButton';
        button.type = 'button';
        button.className = 'relative flex mt-1 mb-1 rounded-full px-2 py-3 text-center bg-cream-550 hover:bg-cream-650 hover:text-brand-green-700 text-muted';
        // Set ARIA label and tooltip
        button.setAttribute('aria-label', 'Talk (Press Control + Space to use hotkey)');
        button.setAttribute('title', 'Talk (Press Control + Space to use hotkey)');
        container.appendChild(button);
        addAudioButtonStyles();
        addAudioIcon(button);

        // Call the function to inject the script after the button has been added
        injectScript(registerAudioButtonEvents);
    }

    function addAudioIcon(button) {
        var iconHtml = `
        <svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 56.25 30" class="waveform">
        <defs>
          <clipPath id="a">
            <path d="M.54 12H3v5H.54Zm0 0"/>
          </clipPath>
          <clipPath id="b">
            <path d="M25 2.2h2v24.68h-2Zm0 0"/>
          </clipPath>
          <clipPath id="c">
            <path d="M53 12h1.98v5H53Zm0 0"/>
          </clipPath>
        </defs>
        <g clip-path="url(#a)">
          <path fill="#776d6d" d="M1.48 12.71c-.5 0-.9.4-.9.9v1.85a.9.9 0 0 0 1.8 0v-1.84c0-.5-.4-.9-.9-.9Zm0 0"/>
        </g>
        <path fill="#776d6d" d="M4.98 6.63c-.5 0-.9.4-.9.9v14.01a.9.9 0 0 0 1.81 0v-14c0-.5-.4-.92-.9-.92Zm3.51 3.1a.9.9 0 0 0-.9.91v7.79a.9.9 0 0 0 1.8 0v-7.79c0-.5-.4-.9-.9-.9ZM12 3.83a.9.9 0 0 0-.91.9v19.6a.9.9 0 0 0 1.8 0V4.74c0-.5-.4-.9-.9-.9Zm3.5 8.29a.9.9 0 0 0-.91.9v3.03a.9.9 0 0 0 1.81 0v-3.03c0-.5-.4-.9-.9-.9ZM19 6.8c-.5 0-.9.4-.9.9v13.68a.9.9 0 0 0 1.8 0V7.7c0-.5-.4-.9-.9-.9Zm3.58-2.97h-.01c-.5 0-.9.4-.9.9l-.13 19.6c0 .5.4.9.9.91.5 0 .9-.4.9-.9l.14-19.6a.9.9 0 0 0-.9-.9Zm0 0"/>
        <g clip-path="url(#b)">
          <path fill="#776d6d" d="M26 2.2c-.5 0-.9.4-.9.9v22.86a.9.9 0 1 0 1.81 0V3.11a.9.9 0 0 0-.9-.91Zm0 0"/>
        </g>
        <path fill="#776d6d" d="M29.52 7.71a.9.9 0 0 0-.91.9v11.85a.9.9 0 0 0 1.81 0V8.62c0-.5-.4-.9-.9-.9Zm3.5 2.93a.9.9 0 0 0-.9.91v5.97a.9.9 0 0 0 1.8 0v-5.97c0-.5-.4-.9-.9-.9Zm3.5-5.78c-.5 0-.9.4-.9.9v17.55a.9.9 0 0 0 1.81 0V5.76c0-.5-.4-.9-.9-.9Zm3.51 3.34c-.5 0-.9.4-.9.9v10.87a.9.9 0 0 0 1.8 0V9.1a.9.9 0 0 0-.9-.91Zm3.5 3.08c-.5 0-.9.4-.9.91v4.7a.9.9 0 1 0 1.8 0v-4.7a.9.9 0 0 0-.9-.9Zm3.51-7.45a.9.9 0 0 0-.91.9v19.6a.9.9 0 0 0 1.81 0V4.74c0-.5-.4-.9-.9-.9Zm3.5 5.57a.9.9 0 0 0-.9.91v8.45a.9.9 0 0 0 1.8 0v-8.45c0-.5-.4-.9-.9-.9Zm0 0"/>
        <g clip-path="url(#c)">
          <path fill="#776d6d" d="M54.04 12.96a.9.9 0 0 0-.9.91v1.33a.9.9 0 1 0 1.8 0v-1.32a.9.9 0 0 0-.9-.92Zm0 0"/>
        </g>
      </svg>
        `;
        var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        button.appendChild(icon);
        icon.outerHTML = iconHtml;
    }

    function addStyles(css) {
        const style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(css));
        document.head.appendChild(style);
    }

    function addAudioButtonStyles() {
        // Get the button and register for mousedown and mouseup events
        var button = document.getElementById('talkButton');
        button.style.marginTop = '0.25rem';
        button.style.borderRadius = '18px';
        button.style.width = '120px';
        // button animation
        addStyles(`
            @keyframes pulse {
                0% {
                    transform: scale(1);
                }
                50% {
                    transform: scale(0.9);
                }
                100% {
                    transform: scale(1);
                }
            }

            #talkButton:active .waveform, #talkButton.active .waveform {
                animation: pulse 1s infinite;
            }
        `);

    }

    function registerAudioButtonEvents() {
        var button = document.getElementById('talkButton');

        button.addEventListener('mousedown', function () {
            idPromptTextArea();
            unsafeWindow.startRecording();
        });
        button.addEventListener('mouseup', function () {
            unsafeWindow.stopRecording();
        });
        registerHotkey();

        // "warm up" the microphone by acquiring it before the user presses the button
        document.getElementById('talkButton').addEventListener('mouseenter', setupRecording);
        document.getElementById('talkButton').addEventListener('mouseleave', tearDownRecording);
        window.addEventListener('beforeunload', tearDownRecording);
    }

    function registerHotkey() {
        // Register a hotkey for the button
        let ctrlDown = false;

        document.addEventListener('keydown', function (event) {
            if (event.ctrlKey && event.code === 'Space' && !ctrlDown) {
                ctrlDown = true;
                // Simulate mousedown event
                let mouseDownEvent = new Event('mousedown');
                document.getElementById('talkButton').dispatchEvent(mouseDownEvent);
                talkButton.classList.add('active'); // Add the active class
            }
        });

        document.addEventListener('keyup', function (event) {
            if (ctrlDown && event.code === 'Space') {
                ctrlDown = false;
                // Simulate mouseup event
                let mouseUpEvent = new Event('mouseup');
                document.getElementById('talkButton').dispatchEvent(mouseUpEvent);
                talkButton.classList.remove('active');
            }
        });
    }


    function idPromptTextArea() {
        var textarea = document.getElementById('prompt');
        if (!textarea) {
            // Find the first <textarea> element and give it an id
            var textareaElement = document.querySelector('textarea');
            if (textareaElement) {
                textareaElement.id = 'prompt';
            } else {
                console.log('No <textarea> element found');
            }
        }
    }

    // Start observing the entire document for changes to child nodes and subtree
    observer.observe(document, { childList: true, subtree: true });
})();