Download ChatGPT Voice Audio

Adds a download button for voice audio files

当前为 2024-10-01 提交的版本,查看 最新版本

// ==UserScript==
// @name         Download ChatGPT Voice Audio
// @namespace    ViolentMonkeyScript
// @match       *://chat.openai.com/*
// @match       *://chatgpt.com/*
// @version      3.2
// @description  Adds a download button for voice audio files
// @grant        none
// @run-at       document-start
// @inject-into  page
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('Script is running at document-start');

    let shouldStopAudioPlayback = false;
    let shouldDownloadSynthesizedAudio = false;
    let currentDownloadButton = null;

    // Save the original fetch function
    const originalFetch = window.fetch;

    // Override the fetch function
    window.fetch = function(...args) {
        const resource = args[0];
        const config = args[1];

        // Get the URL from the resource
        let url = resource instanceof Request ? resource.url : resource;

        // Check if the request URL includes 'backend-api/synthesize'
        if (typeof url === 'string' && url.includes('/backend-api/synthesize')) {
            console.log('Intercepted fetch:', url);

            if (shouldDownloadSynthesizedAudio) {
                shouldDownloadSynthesizedAudio = false; // Reset the flag

                // Extract message_id from the URL parameters
                const urlParams = new URL(url, window.location.origin);
                const messageId = urlParams.searchParams.get('message_id');

                // Generate filename
                let fileName = 'response.aac'; // Default filename
                if (messageId) {
                    const prefix = messageId.split('-')[0];
                    fileName = `${prefix}.aac`;
                }

                return originalFetch(...args)
                    .then(response => {
                        // Clone the response
                        const responseClone = response.clone();

                        // Convert response to Blob and download
                        responseClone.blob().then(blob => {
                            const objectURL = URL.createObjectURL(blob);
                            console.log('Object URL:', objectURL);

                            const a = document.createElement('a');
                            a.href = objectURL;
                            a.download = fileName;
                            document.body.appendChild(a);
                            a.click();
                            document.body.removeChild(a);

                            // Revoke the object URL after download
                            URL.revokeObjectURL(objectURL);

                            // Restore the button after download
                            if (currentDownloadButton) {
                                restoreDownloadButton(currentDownloadButton);
                                currentDownloadButton = null;
                            }
                        }).catch(error => {
                            console.error('Error processing the blob:', error);
                            // Restore the button on error
                            if (currentDownloadButton) {
                                restoreDownloadButton(currentDownloadButton);
                                currentDownloadButton = null;
                            }
                        });

                        // Return the original response
                        return response;
                    })
                    .catch(error => {
                        console.error('Error fetching the response:', error);
                        // Restore the button on error
                        if (currentDownloadButton) {
                            restoreDownloadButton(currentDownloadButton);
                            currentDownloadButton = null;
                        }
                        throw error;
                    });
            } else {
                // Proceed with the fetch normally
                return originalFetch(...args);
            }
        } else {
            // For other fetch requests
            return originalFetch(...args);
        }
    };

    // Wait until the DOM is ready
    document.addEventListener('DOMContentLoaded', function() {
        waitForElements();
    });

    // Monitor 'play' events on audio elements
    document.addEventListener('play', function(e) {
        const audioElement = e.target;
        if (audioElement.tagName === 'AUDIO') {
            if (shouldStopAudioPlayback) {
                audioElement.pause();
                audioElement.currentTime = 0;
                shouldStopAudioPlayback = false;
                console.log('Audio playback stopped');
            }
        }
    }, true); // Use capture phase to catch events from all elements

    // Function to wait for the elements to be available
    function waitForElements() {
        const originalButtons = document.querySelectorAll('button[data-testid="voice-play-turn-action-button"]');
        if (originalButtons && originalButtons.length > 0) {
            console.log('Original buttons are now available');
            injectNewButtons();
            observeDOM(); // Start observing after initial buttons are injected
        } else {
            console.log('Original buttons not yet available, retrying in 500ms...');
            setTimeout(waitForElements, 500);
        }
    }

    // Function to inject new buttons next to each 'Replay' button
    function injectNewButtons() {
        // Get all the 'Replay' buttons
        const originalButtons = document.querySelectorAll('button[data-testid="voice-play-turn-action-button"]');

        console.log('Number of original buttons found:', originalButtons.length);

        originalButtons.forEach(originalButton => {
            // Find the parent element to inject the new button into
            let parentElement = originalButton.closest('.flex.items-center');

            if (!parentElement) {
                console.error('Parent element not found for an original button!');
                return;
            }

            // Check if the button is already injected to avoid duplicates
            if (parentElement.querySelector('.download-audio-button')) {
                return;
            }

            // Create the new button element
            const newButton = document.createElement('button');
            newButton.classList.add('rounded-lg', 'text-token-text-secondary', 'hover:bg-token-main-surface-secondary', 'download-audio-button');
            newButton.setAttribute('aria-label', 'Download Audio');
            newButton.setAttribute('data-testid', 'download-audio-button');

            // Create the span inside the button
            const span = document.createElement('span');
            span.classList.add('flex', 'h-[30px]', 'w-[30px]', 'items-center', 'justify-center');

            // Create the SVG element (download icon)
            const svgNS = 'http://www.w3.org/2000/svg';
            const svg = document.createElementNS(svgNS, 'svg');
            svg.setAttribute('width', '24');
            svg.setAttribute('height', '24');
            svg.setAttribute('viewBox', '0 0 24 24');
            svg.classList.add('icon-md-heavy');

            // Create the path element
            const path = document.createElementNS(svgNS, 'path');
            path.setAttribute('fill-rule', 'evenodd');
            path.setAttribute('clip-rule', 'evenodd');
            path.setAttribute('d', 'M5 20H19V18H5M19 9H15V3H9V9H5L12 16L19 9Z'); // Download icon path
            path.setAttribute('fill', 'currentColor');

            // Append the path to the SVG
            svg.appendChild(path);

            // Append the SVG to the span
            span.appendChild(svg);

            // Append the span to the button
            newButton.appendChild(span);

            // Append the new button to the parent element
            parentElement.appendChild(newButton);

            // Determine if this is an old or new conversation
            // const isOldConversation = detectOldConversation(originalButton);

            addClickHandler(newButton, originalButton);

            // // Add click event listener to the new button
            // if (isOldConversation) {
            //     addClickHandlerToOldButton(newButton, originalButton);
            // } else {
            //     addClickHandlerToNewButton(newButton, originalButton);
            // }
        });
    }

    // Function to detect if the conversation is old
    function detectOldConversation(originalButton) {
        // Get the 'svg' element within the button
        const svgElement = originalButton.querySelector('svg');

        if (!svgElement) {
            console.warn('SVG element not found within the Replay button.');
            return false;
        }

        // Find the 'path' element within the 'svg'
        const pathElement = svgElement.querySelector('path');

        if (!pathElement) {
            console.warn('Path element not found within the SVG.');
            return false;
        }

        // Get the 'd' attribute of the 'path' element
        const dAttribute = pathElement.getAttribute('d');

        // SVG 'd' attribute for the old conversation Replay button
        const oldReplayButtonDAttribute = 'M11 4.9099C11 4.47485 10.4828 4.24734 10.1621 4.54132L6.67572 7.7372C6.49129 7.90626 6.25019 8.00005 6 8.00005H4C3.44772 8.00005 3 8.44776 3 9.00005V15C3 15.5523 3.44772 16 4 16H6C6.25019 16 6.49129 16.0938 6.67572 16.2629L10.1621 19.4588C10.4828 19.7527 11 19.5252 11 19.0902V4.9099ZM8.81069 3.06701C10.4142 1.59714 13 2.73463 13 4.9099V19.0902C13 21.2655 10.4142 22.403 8.81069 20.9331L5.61102 18H4C2.34315 18 1 16.6569 1 15V9.00005C1 7.34319 2.34315 6.00005 4 6.00005H5.61102L8.81069 3.06701ZM20.3166 6.35665C20.8019 6.09313 21.409 6.27296 21.6725 6.75833C22.5191 8.3176 22.9996 10.1042 22.9996 12.0001C22.9996 13.8507 22.5418 15.5974 21.7323 17.1302C21.4744 17.6185 20.8695 17.8054 20.3811 17.5475C19.8927 17.2896 19.7059 16.6846 19.9638 16.1962C20.6249 14.9444 20.9996 13.5175 20.9996 12.0001C20.9996 10.4458 20.6064 8.98627 19.9149 7.71262C19.6514 7.22726 19.8312 6.62017 20.3166 6.35665ZM15.7994 7.90049C16.241 7.5688 16.8679 7.65789 17.1995 8.09947C18.0156 9.18593 18.4996 10.5379 18.4996 12.0001C18.4996 13.3127 18.1094 14.5372 17.4385 15.5604C17.1357 16.0222 16.5158 16.1511 16.0539 15.8483C15.5921 15.5455 15.4632 14.9255 15.766 14.4637C16.2298 13.7564 16.4996 12.9113 16.4996 12.0001C16.4996 10.9859 16.1653 10.0526 15.6004 9.30063C15.2687 8.85905 15.3578 8.23218 15.7994 7.90049Z';

        // Compare the 'd' attribute to the known value for the old conversation icon
        if (dAttribute === oldReplayButtonDAttribute) {
            return true; // It's an old conversation
        } else {
            return false; // It's a new conversation
        }
    }

      function addClickHandler(newButton, originalButton) {
        newButton.addEventListener('click', function() {
            console.log('Download button clicked');
            // Set the flag to stop audio playback
            shouldStopAudioPlayback = true;

            // Save reference to the current button
            currentDownloadButton = newButton;

            // Change the button to loading state
            setButtonLoadingState(newButton);

            // Record the current timestamp
            const startTime = performance.now();

            // Set the flag to download synthesized audio
            shouldDownloadSynthesizedAudio = true;

            // Simulate click on the original 'Replay' button
            originalButton.click();

            // Attempt to capture the audio URL
            waitForAudioURL(startTime).then(function(audioURL) {
                if (audioURL) {
                    // New conversation handling
                    console.log('Captured audio URL via Performance API:', audioURL);
                    downloadAudio(audioURL);
                } else {
                    // Old conversation handling
                    console.log('Attempting to download synthesized audio');
                    // The fetch override should handle the download
                    // If not, restore the button
                    setTimeout(() => {
                        if (currentDownloadButton) {
                            restoreDownloadButton(currentDownloadButton);
                            currentDownloadButton = null;
                        }
                    }, 5000); // Adjust timeout as needed
                }
            });
        });
    }


    // Function to handle the click event for old conversations
    function addClickHandlerToOldButton(newButton, originalButton) {
        newButton.addEventListener('click', function() {
            console.log('Simulated click on the original button (old conversation)');
            // Set the flag to stop audio playback
            shouldStopAudioPlayback = true;

            // Set the flag to download synthesized audio
            shouldDownloadSynthesizedAudio = true;

            // Save reference to the current button
            currentDownloadButton = newButton;

            // Change the button to loading state
            setButtonLoadingState(newButton);

            // Simulate click on the original 'Replay' button
            originalButton.click();
        });
    }

    // Function to handle the click event for new conversations
    function addClickHandlerToNewButton(newButton, originalButton) {
        newButton.addEventListener('click', function() {
            console.log('Simulated click on the original button (new conversation)');
            // Set the flag to stop audio playback
            shouldStopAudioPlayback = true;

            // Record the current timestamp
            const startTime = performance.now();

            // Save reference to the current button
            currentDownloadButton = newButton;

            // Change the button to loading state
            setButtonLoadingState(newButton);

            // Simulate click on the original 'Replay' button
            originalButton.click();

            // Wait for the audio URL to be captured
            waitForAudioURL(startTime).then(function(audioURL) {
                if (audioURL) {
                    // Initiate download
                    downloadAudio(audioURL);
                } else {
                    console.error('Failed to capture the audio URL.');
                    restoreDownloadButton(currentDownloadButton);
                    currentDownloadButton = null;
                }
            });
        });
    }

    function setButtonLoadingState(button) {
        // Disable the button
        button.disabled = true;
        button.setAttribute('aria-label', 'Loading');
        button.setAttribute('data-testid', 'loading-download-button');

        // Clear existing content
        button.innerHTML = '';

        // Create the span inside the button
        const span = document.createElement('span');
        span.classList.add('flex', 'h-[30px]', 'w-[30px]', 'items-center', 'justify-center');

        // Create the loading spinner SVG
        const svgNS = 'http://www.w3.org/2000/svg';
        const svg = document.createElementNS(svgNS, 'svg');
        svg.setAttribute('stroke', 'currentColor');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('stroke-width', '2');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('stroke-linecap', 'round');
        svg.setAttribute('stroke-linejoin', 'round');
        svg.classList.add('animate-spin', 'text-center', 'icon-md-heavy');
        svg.setAttribute('height', '1em');
        svg.setAttribute('width', '1em');

        // Add lines to the spinner SVG
        const lines = [
            { x1: 12, y1: 2, x2: 12, y2: 6 },
            { x1: 12, y1: 18, x2: 12, y2: 22 },
            { x1: 4.93, y1: 4.93, x2: 7.76, y2: 7.76 },
            { x1: 16.24, y1: 16.24, x2: 19.07, y2: 19.07 },
            { x1: 2, y1: 12, x2: 6, y2: 12 },
            { x1: 18, y1: 12, x2: 22, y2: 12 },
            { x1: 4.93, y1: 19.07, x2: 7.76, y2: 16.24 },
            { x1: 16.24, y1: 7.76, x2: 19.07, y2: 4.93 },
        ];

        lines.forEach(lineData => {
            const line = document.createElementNS(svgNS, 'line');
            line.setAttribute('x1', lineData.x1);
            line.setAttribute('y1', lineData.y1);
            line.setAttribute('x2', lineData.x2);
            line.setAttribute('y2', lineData.y2);
            svg.appendChild(line);
        });

        // Append the SVG to the span
        span.appendChild(svg);

        // Append the span to the button
        button.appendChild(span);
    }

    function restoreDownloadButton(button) {
        // Enable the button
        button.disabled = false;
        button.setAttribute('aria-label', 'Download Audio');
        button.setAttribute('data-testid', 'download-audio-button');

        // Clear existing content
        button.innerHTML = '';

        // Create the span inside the button
        const span = document.createElement('span');
        span.classList.add('flex', 'h-[30px]', 'w-[30px]', 'items-center', 'justify-center');

        // Create the SVG element (download icon)
        const svgNS = 'http://www.w3.org/2000/svg';
        const svg = document.createElementNS(svgNS, 'svg');
        svg.setAttribute('width', '24');
        svg.setAttribute('height', '24');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.classList.add('icon-md-heavy');

        // Create the path element
        const path = document.createElementNS(svgNS, 'path');
        path.setAttribute('fill-rule', 'evenodd');
        path.setAttribute('clip-rule', 'evenodd');
        path.setAttribute('d', 'M5 20h14v-2H5v2zm7-18L5 9h4v4h6V9h4L12 2z'); // Download icon path
        path.setAttribute('fill', 'currentColor');

        // Append the path to the SVG
        svg.appendChild(path);

        // Append the SVG to the span
        span.appendChild(svg);

        // Append the span to the button
        button.appendChild(span);
    }

    function downloadAudio(url) {
        fetch(url)
            .then(response => response.blob())
            .then(blob => {
                const objectURL = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = objectURL;
                a.download = 'audio.wav'; // Customize the filename if needed
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(objectURL);
                console.log('Audio download completed');
                if (currentDownloadButton) {
                    restoreDownloadButton(currentDownloadButton);
                    currentDownloadButton = null;
                }
            })
            .catch(error => {
                console.error('Audio download failed', error);
                if (currentDownloadButton) {
                    restoreDownloadButton(currentDownloadButton);
                    currentDownloadButton = null;
                }
            });
    }

    function waitForAudioURL(startTime, timeout = 5000) {
        return new Promise(function(resolve, reject) {
            const interval = 100;
            let elapsedTime = 0;

            const checkURL = setInterval(function() {
                const audioURL = getLatestAudioRequestURL(startTime);
                if (audioURL) {
                    clearInterval(checkURL);
                    resolve(audioURL);
                } else if (elapsedTime >= timeout) {
                    clearInterval(checkURL);
                    resolve(null);
                } else {
                    elapsedTime += interval;
                }
            }, interval);
        });
    }

    function getLatestAudioRequestURL(startTime) {
        const entries = performance.getEntriesByType('resource');
        for (let i = entries.length - 1; i >= 0; i--) {
            const entry = entries[i];
            if (entry.startTime < startTime) {
                break;
            }
            if (entry.name.includes('sdmntprwestus.oaiusercontent.com')) {
                console.log('Captured audio URL via Performance API:', entry.name);
                return entry.name;
            }
        }
        return null;
    }

    function observeDOM() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                // Check if added nodes contain new 'Replay' buttons
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches('button[data-testid="voice-play-turn-action-button"]') ||
                            node.querySelector('button[data-testid="voice-play-turn-action-button"]')) {
                            console.log('New Replay button detected, injecting Download buttons...');
                            injectNewButtons();
                        }
                    }
                });
            });
        });

        const threadsContainer = document.body; // Adjust this selector if necessary
        if (threadsContainer) {
            observer.observe(threadsContainer, {
                childList: true,
                subtree: true
            });
            console.log('MutationObserver is now observing the DOM for changes.');
        } else {
            console.error('Threads container not found for MutationObserver!');
        }
    }

})();


QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址