您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通过油猴菜单控制ChatGPT朗读速度,支持0.01-100x速度调节
// ==UserScript== // @name ChatGPT朗读速度控制器 // @description 通过油猴菜单控制ChatGPT朗读速度,支持0.01-100x速度调节 // @author schweigen // @version 1.0 // @namespace ChatGPT.SpeedController.Simple // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com // @match https://chatgpt.com // @match https://chatgpt.com/?model=* // @match https://chatgpt.com/?temporary-chat=* // @match https://chatgpt.com/c/* // @match https://chatgpt.com/g/* // @match https://chatgpt.com/share/* // @grant GM.setValue // @grant GM.getValue // @grant GM.registerMenuCommand // @grant GM.unregisterMenuCommand // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 配置常量 const MIN_SPEED = 0.01; const MAX_SPEED = 100; // 全局变量 let currentSpeed = 1; let playingAudio = new Set(); let menuCommands = []; // 加载保存的速度设置 async function loadSettings() { const saved = await GM.getValue('playbackSpeed', 1); currentSpeed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, saved)); console.log(`ChatGPT速度控制器:当前速度 ${currentSpeed}x`); } // 保存速度设置 async function saveSettings() { await GM.setValue('playbackSpeed', currentSpeed); console.log(`ChatGPT速度控制器:速度已保存为 ${currentSpeed}x`); } // 设置音频播放速度 function setAudioSpeed(speed) { // 设置所有当前播放的音频速度 playingAudio.forEach(audio => { if (audio && !audio.paused) { audio.playbackRate = speed; audio.preservesPitch = true; audio.mozPreservesPitch = true; audio.webkitPreservesPitch = true; } }); } // 监听新的音频元素 function setupAudioListener() { // 监听播放事件 document.addEventListener('play', (e) => { const audio = e.target; if (audio instanceof HTMLAudioElement) { audio.playbackRate = currentSpeed; audio.preservesPitch = true; audio.mozPreservesPitch = true; audio.webkitPreservesPitch = true; playingAudio.add(audio); // 音频结束时从集合中移除 const cleanup = () => playingAudio.delete(audio); audio.addEventListener('pause', cleanup, {once: true}); audio.addEventListener('ended', cleanup, {once: true}); } }, true); // 监听速度变化事件,防止被重置 document.addEventListener('ratechange', (e) => { const audio = e.target; if (audio instanceof HTMLAudioElement && Math.abs(audio.playbackRate - currentSpeed) > 0.01) { audio.playbackRate = currentSpeed; } }, true); } // 更改速度 async function changeSpeed(newSpeed) { currentSpeed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, newSpeed)); setAudioSpeed(currentSpeed); await saveSettings(); updateMenus(); // 显示通知 showNotification(`朗读速度已设置为 ${currentSpeed}x`); } // 显示通知 (移除旧的通知函数,只在changeSpeed中使用右上角显示) function showNotification(message) { // 创建右上角通知 const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #4CAF50; color: white; padding: 12px 18px; border-radius: 8px; z-index: 10000; font-size: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 2px solid #45a049; `; notification.textContent = message; document.body.appendChild(notification); // 3秒后自动消失 setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 3000); } // 注册菜单命令 function registerMenus() { // 清除旧菜单 menuCommands.forEach(id => GM.unregisterMenuCommand(id)); menuCommands = []; // 设置面板 menuCommands.push(GM.registerMenuCommand('⚙️ 打开速度设置', () => { showSettingsPanel(); })); // 查看当前速度 menuCommands.push(GM.registerMenuCommand('🎵 查看当前速度', () => { showSpeedDisplay(); })); } // 显示右上角速度提示框 function showSpeedDisplay() { const speedBox = document.createElement('div'); speedBox.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #333; color: white; padding: 12px 18px; border-radius: 8px; z-index: 10000; font-size: 16px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 2px solid #666; `; speedBox.textContent = `当前速度: ${currentSpeed}x`; document.body.appendChild(speedBox); // 3秒后自动消失 setTimeout(() => { if (speedBox.parentNode) { speedBox.parentNode.removeChild(speedBox); } }, 3000); } // 显示设置面板 function showSettingsPanel() { // 如果已经存在设置面板,先移除 const existingPanel = document.getElementById('speed-settings-panel'); if (existingPanel) { existingPanel.remove(); } // 创建背景遮罩 const overlay = document.createElement('div'); overlay.id = 'speed-settings-panel'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); z-index: 10000; display: flex; justify-content: center; align-items: center; animation: fadeIn 0.2s ease-out; `; // 添加动画样式 const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideIn { from { transform: scale(0.9) translateY(-20px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } } `; document.head.appendChild(style); // 创建设置面板 const panel = document.createElement('div'); panel.style.cssText = ` background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1); max-width: 320px; width: 90%; animation: slideIn 0.3s ease-out; position: relative; overflow: hidden; `; // 添加装饰性背景 const bgDecor = document.createElement('div'); bgDecor.style.cssText = ` position: absolute; top: -50%; right: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); pointer-events: none; `; panel.appendChild(bgDecor); const content = document.createElement('div'); content.style.cssText = ` position: relative; z-index: 1; `; content.innerHTML = ` <div style="text-align: center; margin-bottom: 20px;"> <div style=" display: inline-block; width: 48px; height: 48px; background: rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 8px; backdrop-filter: blur(10px); "> <span style="font-size: 24px;">🎵</span> </div> <h3 style="margin: 0; color: white; font-size: 18px; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">朗读速度设置</h3> </div> <div style="margin-bottom: 18px;"> <label style="display: block; margin-bottom: 8px; color: rgba(255,255,255,0.9); font-size: 14px; font-weight: 500;"> 当前速度: <span style="color: #fff; font-weight: 600;">${currentSpeed}x</span> </label> <div style="position: relative;"> <input type="range" id="speedSlider" min="0.25" max="5" step="0.25" value="${Math.min(currentSpeed, 5)}" style=" width: 100%; margin: 8px 0; -webkit-appearance: none; appearance: none; height: 6px; background: rgba(255,255,255,0.3); border-radius: 3px; outline: none; "> <style> #speedSlider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.3); } #speedSlider::-moz-range-thumb { width: 18px; height: 18px; background: white; border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 2px 6px rgba(0,0,0,0.3); } </style> </div> <div style="display: flex; justify-content: space-between; font-size: 11px; color: rgba(255,255,255,0.7); margin-top: 4px;"> <span>0.25x</span> <span>5x</span> </div> </div> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 8px; color: rgba(255,255,255,0.9); font-size: 14px; font-weight: 500;"> 精确值 (${MIN_SPEED} - ${MAX_SPEED}) </label> <input type="number" id="speedInput" min="${MIN_SPEED}" max="${MAX_SPEED}" step="0.01" value="${currentSpeed}" style=" width: 100%; padding: 10px 12px; border: none; border-radius: 8px; font-size: 14px; background: rgba(255,255,255,0.95); color: #333; backdrop-filter: blur(10px); box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: all 0.2s ease; " onfocus="this.style.background='rgba(255,255,255,1)'; this.style.transform='translateY(-1px)'" onblur="this.style.background='rgba(255,255,255,0.95)'; this.style.transform='translateY(0)'"> </div> <div style="display: flex; gap: 10px;"> <button id="applyBtn" style=" padding: 12px 20px; border: none; border-radius: 8px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; cursor: pointer; flex: 1; font-size: 14px; font-weight: 600; box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4); transition: all 0.2s ease; text-shadow: 0 1px 2px rgba(0,0,0,0.2); " onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(79, 172, 254, 0.5)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(79, 172, 254, 0.4)'" >✓ 应用</button> <button id="cancelBtn" style=" padding: 12px 20px; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; background: rgba(255,255,255,0.1); color: white; cursor: pointer; flex: 1; font-size: 14px; font-weight: 500; backdrop-filter: blur(10px); transition: all 0.2s ease; " onmouseover="this.style.background='rgba(255,255,255,0.2)'; this.style.transform='translateY(-1px)'" onmouseout="this.style.background='rgba(255,255,255,0.1)'; this.style.transform='translateY(0)'" >✕ 取消</button> </div> `; panel.appendChild(content); overlay.appendChild(panel); document.body.appendChild(overlay); // 获取元素 const speedSlider = panel.querySelector('#speedSlider'); const speedInput = panel.querySelector('#speedInput'); const applyBtn = panel.querySelector('#applyBtn'); const cancelBtn = panel.querySelector('#cancelBtn'); // 滑块和输入框同步 speedSlider.addEventListener('input', () => { speedInput.value = speedSlider.value; }); speedInput.addEventListener('input', () => { const value = parseFloat(speedInput.value); if (value >= 0.25 && value <= 5) { speedSlider.value = value; } }); // 应用按钮 applyBtn.addEventListener('click', () => { const newSpeed = parseFloat(speedInput.value); if (newSpeed >= MIN_SPEED && newSpeed <= MAX_SPEED) { changeSpeed(newSpeed); overlay.remove(); } else { alert(`请输入有效的速度值 (${MIN_SPEED}-${MAX_SPEED})`); } }); // 取消按钮和背景点击 cancelBtn.addEventListener('click', () => overlay.remove()); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // 更新菜单显示 (简化版不需要动态更新) function updateMenus() { // 菜单项是静态的,不需要更新 } // 初始化 async function init() { await loadSettings(); setupAudioListener(); registerMenus(); // 监听页面变化,确保音频监听器始终有效 const observer = new MutationObserver((mutations) => { const hasAudio = mutations.some(mutation => Array.from(mutation.addedNodes).some(node => node.nodeName === 'AUDIO' || (node.querySelector && node.querySelector('audio')) ) ); if (hasAudio) { setAudioSpeed(currentSpeed); } }); if (document.body) { observer.observe(document.body, {childList: true, subtree: true}); } console.log('ChatGPT朗读速度控制器已启动'); } // 等待DOM加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, {once: true}); } else { init(); } })();