您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为新闻段落添加语音播放功能,目前支持纽约时报、CNA、新加坡海峡时报
// ==UserScript== // @name 新闻段落转语音 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 为新闻段落添加语音播放功能,目前支持纽约时报、CNA、新加坡海峡时报 // @author raymon // @match https://*.nytimes.com/* // @match https://*.channelnewsasia.com/* // @match https://*.straitstimes.com/* // @icon https://openmoji.org/data/color/svg/1F50A.svg // @license MIT // ==/UserScript== (function () { 'use strict'; function appendPlayers() { const style = document.createElement('style'); style.textContent = ` .tts-container { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin: 12px 0 36px; /* 上边距 12px,下边距 36px */ box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: all 0.2s ease; max-width: 100%; box-sizing: border-box; } .tts-container:hover { box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .tts-button { cursor: pointer; padding: 8px 20px; background: #2563eb; color: white; border: none; border-radius: 6px; display: inline-flex; align-items: center; font-weight: 600; font-size: 14px; transition: all 0.2s ease; min-width: 90px; justify-content: center; } .tts-button:hover { background: #1d4ed8; transform: translateY(-1px); } .tts-button:active { transform: translateY(0); } .tts-controls { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; flex-wrap: wrap; } .tts-rate { padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 6px; background: white; font-size: 14px; cursor: pointer; transition: all 0.2s ease; width: 80px; } .tts-rate:hover { border-color: #2563eb; } .tts-rate:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.2); } .progress-bar { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; cursor: pointer; position: relative; margin: 0 12px; min-width: 100px; z-index: 1; transition: all 0.2s ease; } .progress-bar:hover { background: #d1d5db; } .progress { width: 0%; height: 100%; background: #2563eb; border-radius: 4px; transition: width 0.1s linear; position: absolute; top: 0; left: 0; } .progress:hover { background: #1d4ed8; } .time-btn { cursor: pointer; padding: 6px 12px; background: white; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; font-weight: 500; color: #374151; transition: all 0.2s ease; min-width: 50px; text-align: center; } .time-btn:hover { background: #f3f4f6; border-color: #d1d5db; transform: translateY(-1px); } .time-btn:active { transform: translateY(0); } .controls-wrapper { display: flex; align-items: center; margin-top: 0; width: 100%; gap: 12px; flex-wrap: wrap; } @media (max-width: 768px) { .controls-wrapper { flex-direction: column; align-items: stretch; } .tts-controls { margin-left: 0; margin-top: 12px; } .progress-bar { margin: 8px 0; } } `; document.head.appendChild(style); const synth = window.speechSynthesis; let paragraphs = []; if (window.location.hostname.includes('nytimes.com')) { paragraphs = document.querySelectorAll('section[name="articleBody"] p'); } else if (window.location.hostname.includes('channelnewsasia.com')) { paragraphs = document.querySelectorAll('section[data-title="Content"] p'); } else if (window.location.hostname.includes('straitstimes.com')) { const snapshot = document.evaluate( '/html/body/div[5]/div[1]/main/div[1]/article/section[2]//p', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (let i = 0; i < snapshot.snapshotLength; i++) { paragraphs.push(snapshot.snapshotItem(i)); } } function createUtterance(text) { const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'en-US'; utterance.rate = 1.0; utterance.pitch = 1.0; return utterance; } paragraphs.forEach((paragraph) => { const container = document.createElement('div'); container.className = 'tts-container'; const controlsWrapper = document.createElement('div'); controlsWrapper.className = 'controls-wrapper'; const button = document.createElement('button'); button.className = 'tts-button'; button.innerHTML = 'Play'; const controls = document.createElement('div'); controls.className = 'tts-controls'; const rateLabel = document.createElement('span'); rateLabel.textContent = 'Speed'; rateLabel.style.fontWeight = '500'; rateLabel.style.color = '#374151'; const rateControl = document.createElement('select'); rateControl.className = 'tts-rate'; [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0].forEach(rate => { const option = document.createElement('option'); option.value = rate; option.text = `${rate}x`; if (rate === 1.0) option.selected = true; rateControl.appendChild(option); }); const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; const progress = document.createElement('div'); progress.className = 'progress'; progressBar.appendChild(progress); const backwardBtn = document.createElement('button'); backwardBtn.className = 'time-btn'; backwardBtn.textContent = '-3s'; const forwardBtn = document.createElement('button'); forwardBtn.className = 'time-btn'; forwardBtn.textContent = '+3s'; controls.append(rateLabel, rateControl, backwardBtn, progressBar, forwardBtn); controlsWrapper.append(button, controls); container.appendChild(controlsWrapper); let utterance = createUtterance(paragraph.textContent); let isPlaying = false; let progressInterval, duration, startTime; let currentPosition = 0, pausedPosition = 0; function stopSpeaking() { synth.cancel(); isPlaying = false; button.innerHTML = 'Play'; clearInterval(progressInterval); pausedPosition = currentPosition; } function startSpeaking(startPosition = 0) { utterance = createUtterance(paragraph.textContent); utterance.rate = parseFloat(rateControl.value); if (startPosition > 0) { const words = paragraph.textContent.split(' '); const startIndex = Math.floor(words.length * (startPosition / 100)); utterance.text = words.slice(startIndex).join(' '); } progress.style.width = `${startPosition}%`; currentPosition = startPosition; startTime = Date.now(); duration = (utterance.text.length / utterance.rate) * 50; progressInterval = setInterval(() => { if (!isPlaying) return; const elapsed = Date.now() - startTime; currentPosition = Math.min(startPosition + (elapsed / duration) * (100 - startPosition), 100); progress.style.width = `${currentPosition}%`; }, 50); synth.speak(utterance); isPlaying = true; button.innerHTML = 'Pause'; } utterance.onend = () => { isPlaying = false; button.innerHTML = 'Play'; clearInterval(progressInterval); progress.style.width = '100%'; currentPosition = 100; pausedPosition = 0; setTimeout(() => { if (!isPlaying) { progress.style.width = '0%'; currentPosition = 0; } }, 500); }; button.addEventListener('click', () => { if (isPlaying) stopSpeaking(); else startSpeaking(pausedPosition); }); rateControl.addEventListener('change', () => { if (isPlaying) { stopSpeaking(); startSpeaking(currentPosition); } }); backwardBtn.addEventListener('click', () => { const timeAdj = (-3 * 1000) / duration * 100; stopSpeaking(); startSpeaking(Math.max(0, currentPosition + timeAdj)); }); forwardBtn.addEventListener('click', () => { const timeAdj = (3 * 1000) / duration * 100; stopSpeaking(); startSpeaking(Math.min(100, currentPosition + timeAdj)); }); progressBar.addEventListener('click', (e) => { const rect = progressBar.getBoundingClientRect(); const pos = ((e.clientX - rect.left) / rect.width) * 100; stopSpeaking(); startSpeaking(pos); }); // 插入播放器到段落后面 paragraph.parentNode.insertBefore(container, paragraph.nextSibling); }); } // 延迟加载,确保页面内容已经渲染完毕 setTimeout(appendPlayers, 1000); })();