您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a download button for voice audio files
当前为
// ==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或关注我们的公众号极客氢云获取最新地址