T3 Chat STT with Whisper API (Configurable API Key)

Adds speech-to-text functionality to t3.chat using OpenAI's Whisper API with configurable API key via UI.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         T3 Chat STT with Whisper API (Configurable API Key)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds speech-to-text functionality to t3.chat using OpenAI's Whisper API with configurable API key via UI.
// @author       wearifulpoet
// @match        https://t3.chat/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ********** API Key Configuration **********
    // Use localStorage to store the API key.
    // Initially, if no API key is stored, an empty string is returned.
    const getApiKey = () => localStorage.getItem("whisper_api_key") || "";
    const setApiKey = (key) => localStorage.setItem("whisper_api_key", key);
    // *********************************************

    // Create the STT button.
    const sttButton = document.createElement('button');
    sttButton.innerHTML = "🎙️ Start STT";
    sttButton.style.position = "fixed";
    sttButton.style.bottom = "20px";
    sttButton.style.right = "20px";
    sttButton.style.zIndex = "9999";
    sttButton.style.padding = "10px";
    sttButton.style.fontSize = "16px";
    document.body.appendChild(sttButton);

    // Create the API Key configuration button.
    const apiKeyButton = document.createElement('button');
    apiKeyButton.innerHTML = "API Key";
    apiKeyButton.style.position = "fixed";
    apiKeyButton.style.bottom = "20px";
    apiKeyButton.style.right = "140px";
    apiKeyButton.style.zIndex = "9999";
    apiKeyButton.style.padding = "10px";
    apiKeyButton.style.fontSize = "16px";
    document.body.appendChild(apiKeyButton);

    // Event listener to set/update the API key.
    apiKeyButton.addEventListener("click", () => {
        const currentKey = getApiKey();
        const newKey = prompt("Enter your Whisper API key:", currentKey);
        if (newKey !== null) {
            setApiKey(newKey.trim());
            alert("API key updated!");
        }
    });

    let mediaRecorder = null;
    let audioChunks = [];
    let isRecording = false;

    sttButton.addEventListener("click", async () => {
        if (!isRecording) {
            // Check if the API key has been set.
            const storedKey = getApiKey();
            if (!storedKey) {
                alert("Please set your Whisper API key using the API Key button.");
                return;
            }
            // Start recording
            sttButton.innerHTML = "Stop Recording";
            isRecording = true;
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                mediaRecorder = new MediaRecorder(stream);
                audioChunks = [];

                mediaRecorder.addEventListener("dataavailable", event => {
                    if (event.data.size > 0) {
                        audioChunks.push(event.data);
                    }
                });

                mediaRecorder.addEventListener("stop", async () => {
                    const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
                    // Prepare the file to send to Whisper API.
                    const audioFile = new File([audioBlob], "audio.wav", { type: 'audio/wav' });
                    const formData = new FormData();
                    formData.append("file", audioFile);
                    formData.append("model", "whisper-1");

                    try {
                        const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
                            method: "POST",
                            headers: {
                                "Authorization": `Bearer ${getApiKey()}`
                            },
                            body: formData
                        });
                        if (!response.ok) {
                            const errorText = await response.text();
                            console.error("Whisper API error:", errorText);
                            alert("Error with Whisper API:\n" + errorText);
                            return;
                        }
                        const result = await response.json();
                        const transcription = result.text;
                        // Find the chat input textarea and insert the transcription.
                        const chatInput = document.querySelector("textarea");
                        if (chatInput) {
                            chatInput.value = transcription;
                            // Dispatch an input event if the site relies on it.
                            chatInput.dispatchEvent(new Event('input', { bubbles: true }));
                        } else {
                            alert("Chat input not found!");
                        }
                    } catch (error) {
                        console.error("Error calling Whisper API:", error);
                        alert("Error calling Whisper API: " + error.message);
                    }
                });

                mediaRecorder.start();
            } catch (error) {
                console.error("Error accessing microphone:", error);
                alert("Error accessing microphone: " + error.message);
                sttButton.innerHTML = "🎙️ Start STT";
                isRecording = false;
            }
        } else {
            // Stop recording
            sttButton.innerHTML = "🎙️ Start STT";
            isRecording = false;
            if (mediaRecorder && mediaRecorder.state === "recording") {
                mediaRecorder.stop();
            }
        }
    });
})();