您需要先安装一个扩展,例如 篡改猴、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.5 // @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 create a tooltip element function createTooltip(text) { const tooltip = document.createElement('div'); tooltip.setAttribute('data-radix-popper-content-wrapper', ''); tooltip.style.position = 'fixed'; tooltip.style.left = '0px'; tooltip.style.top = '0px'; tooltip.style.zIndex = '50'; tooltip.style.minWidth = 'max-content'; tooltip.style.willChange = 'transform'; tooltip.style.pointerEvents = 'none'; // Add this line const innerTooltip = document.createElement('div'); innerTooltip.setAttribute('data-side', 'bottom'); innerTooltip.setAttribute('data-align', 'center'); innerTooltip.className = 'relative z-50 shadow-xs transition-opacity px-3 py-2 rounded-lg border-white/10 dark:border bg-gray-950 max-w-xs'; const textSpan = document.createElement('span'); textSpan.className = 'flex items-center whitespace-pre-wrap text-center font-semibold normal-case text-gray-100 text-sm'; textSpan.textContent = text; const arrow = document.createElement('span'); arrow.style.position = 'absolute'; arrow.style.top = '0px'; arrow.style.transformOrigin = 'center 0px'; arrow.style.transform = 'rotate(180deg)'; arrow.style.left = '50.5px'; const arrowDiv = document.createElement('div'); arrowDiv.className = 'relative top-[-4px] h-2 w-2 rotate-45 transform shadow-xs dark:border-r dark:border-b border-white/10 bg-gray-950'; arrow.appendChild(arrowDiv); innerTooltip.appendChild(textSpan); innerTooltip.appendChild(arrow); tooltip.appendChild(innerTooltip); return tooltip; } // Function to add tooltip functionality to a button function addTooltipToButton(button) { let tooltip = null; let showTimeout; button.addEventListener('mouseenter', () => { showTimeout = setTimeout(() => { if (!tooltip) { tooltip = createTooltip('Download Audio'); document.body.appendChild(tooltip); } const buttonRect = button.getBoundingClientRect(); tooltip.style.transform = `translate(${buttonRect.left + buttonRect.width / 2 - 54.5}px, ${buttonRect.bottom + 8}px)`; tooltip.querySelector('[data-side]').setAttribute('data-state', 'delayed-open'); }, 300); }); button.addEventListener('mouseleave', () => { clearTimeout(showTimeout); if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; } }); } // Modified injectNewButtons function function injectNewButtons() { const originalButtons = document.querySelectorAll('button[data-testid="voice-play-turn-action-button"]'); console.log('Number of original buttons found:', originalButtons.length); originalButtons.forEach(originalButton => { // Get the parent span of the original button const originalSpan = originalButton.closest('span[data-state]'); if (!originalSpan) { console.error('Parent span not found for an original button!'); return; } // Check if the download button already exists after the original span if (originalSpan.nextElementSibling && originalSpan.nextElementSibling.querySelector('button.download-audio-button')) { return; } // Create a new span for the new button const newSpan = document.createElement('span'); newSpan.className = originalSpan.className; // Copy class newSpan.setAttribute('data-state', originalSpan.getAttribute('data-state')); // Copy data-state // Create the new button as before 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'); const span = document.createElement('span'); span.classList.add('flex', 'h-[30px]', 'w-[30px]', 'items-center', 'justify-center'); // Create the SVG icon for the button 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'); 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'); svg.appendChild(path); span.appendChild(svg); newButton.appendChild(span); // Append the new button to the new span newSpan.appendChild(newButton); // Insert the new span after the original span originalSpan.insertAdjacentElement('afterend', newSpan); // Add tooltip functionality addTooltipToButton(newButton); addClickHandler(newButton, originalButton); }); } 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 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 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 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; } // Check for any oaiusercontent.com URL if (entry.name.includes('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或关注我们的公众号极客氢云获取最新地址