原文:https://jsdev.space/tts-sentence-reader/ 翻译:yingyu5658 yingyu5658@outlook.com
在这篇文章中,我们将构建一个简单的Web工具来探究Text-toSpeech(TTS)在JavaScript中是如何工作的。我们也将深入研究 句子级高亮 的工作逻辑。这两项功能经常结合在一起使用,以走到浏览器中打造无障碍的动态阅读体验。
步骤:
学习在浏览器中,TTS是如何工作的
探究动态高亮句子的实现方法
用HTML, CSS, JavaScript构建一个小工具(Demo & Code )
📢 浏览器中的TTS概述 JavaScript提供一个内置的API:SpeechSynthesis
,它允许我们使用系统中的可用嗓音去大声朗读问文字。
核心对象:
speechSynthesis
— 控制播放、暂停、恢复、停止
SpeechSynthesisUtterance
— 作为TTS引擎的待播报文本
✨示例: 1 2 const msg = new SpeechSynthesisUtterance ("Hello, world!" );window .speechSynthesis .speak (msg);
⚙️ 添加嗓音和配置 1 2 3 4 5 6 const utter = new SpeechSynthesisUtterance ("This is a test" );const voices = window .speechSynthesis .getVoices ();utter.voice = voices.find (v => v.lang === 'en-US' ); utter.rate = 1.2 ; utter.pitch = 1 ; window .speechSynthesis .speak (utter);
你也可以追踪朗读的开始和结束:
1 2 utter.onstart = () => console .log ('Started speaking' ); utter.onend = () => console .log ('Finished speaking' );
✍️ 句子高亮 展示给用户哪一个句子正在阅读,我们需要用CSS和JavaScript来高亮文本。
示例 HTML: 1 2 3 4 <p > <span class ="sentence" > First sentence.</span > <span class ="sentence" > Second sentence.</span > </p >
处理高亮的CSS: 1 2 3 4 .sentence .active { background-color : yellow; font-weight : bold; }
JavaScript高亮逻辑: 1 2 3 4 5 function highlight (index ) { document .querySelectorAll ('.sentence' ).forEach ((el, i ) => { el.classList .toggle ('active' , i === index); }); }
🚀项目: 使用TSS和高亮的阅读器 我们的程序将要:
逐句朗读文本
高亮朗读中的文本
提供播放、暂停、恢复、停止
让用户能选择嗓音
📄 HTML结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta name ="description" content ="Build an interactive sentence-level text-to-speech reader with highlight, playback controls, and local progress tracking using HTML and JavaScript." /> <title > Interactive TTS Article Reader</title > </head > <body > <h1 > Read Along TTS Demo</h1 > <div class ="toolbar" > <button id ="start" > Play</button > <button id ="pause" disabled > Pause</button > <button id ="resume" disabled > Resume</button > <button id ="stop" disabled > Stop</button > <button id ="reset" > Reset</button > <select id ="voices" > </select > </div > <div class ="text-block" id ="reader" > <span class ="line" > Learning to code is a never-ending journey.</span > <span class ="line" > Technologies evolve rapidly, requiring constant adaptation.</span > <span class ="line" > JavaScript, HTML, and CSS are essential tools for web development.</span > <span class ="line" > Frameworks like React and Vue enhance front-end capabilities.</span > <span class ="line" > Back-end skills with Node.js extend JavaScript to the server.</span > </div > <div class ="progress" > <span id ="progressText" > 0/0</span > <div class ="bar" > <div class ="bar-fill" id ="bar" > </div > </div > </div > </body > </html >
🎨 CSS 样式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 body { font-family : sans-serif; margin : 0 ; padding : 2rem ; background : #f0f4f8 ; color : #333 ; } h1 { text-align : center; margin-bottom : 2rem ; } .toolbar { display : flex; justify-content : center; flex-wrap : wrap; gap : 1rem ; margin-bottom : 2rem ; } .toolbar button ,.toolbar select { padding : 0.6rem 1.2rem ; border-radius : 4px ; border : none; font-size : 1rem ; } .toolbar button { background : #0077cc ; color : white; cursor : pointer; } .toolbar button :disabled { background : #ccc ; cursor : not-allowed; } .text-block { background : white; padding : 1.5rem ; border-radius : 6px ; box-shadow : 0 2px 6px rgba (0 , 0 , 0 , 0.1 ); } .line { display : block; margin-bottom : 1rem ; cursor : pointer; transition : background 0.3s ; } .line :hover { background : #f9f9f9 ; } .line .active { background : #fff3cd ; font-weight : bold; } .progress { text-align : center; margin-top : 1rem ; } .bar { height : 8px ; width : 100% ; background : #eee ; border-radius : 4px ; overflow : hidden; } .bar-fill { height : 100% ; width : 0 ; background : linear-gradient (to right, #0077cc , #005fa3 ); transition : width 0.3s ; }
💡 JavaScript逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 const lines = document .querySelectorAll ('.line' );const playBtn = document .getElementById ('start' );const pauseBtn = document .getElementById ('pause' );const resumeBtn = document .getElementById ('resume' );const stopBtn = document .getElementById ('stop' );const resetBtn = document .getElementById ('reset' );const voiceSelect = document .getElementById ('voices' );const progressText = document .getElementById ('progressText' );const progressBar = document .getElementById ('bar' );const synth = window .speechSynthesis ;let voices = [];let currentIndex = 0 ;let currentUtterance = null ;let isPaused = false ;function populateVoices ( ) { voices = synth.getVoices (); voiceSelect.innerHTML = '' ; voices.forEach ((voice, index ) => { const opt = document .createElement ('option' ); opt.value = index; opt.textContent = `${voice.name} (${voice.lang} )` ; voiceSelect.appendChild (opt); }); } synth.onvoiceschanged = populateVoices; populateVoices ();function updateUI ( ) { progressText.textContent = `${currentIndex + 1 } /${lines.length} ` ; progressBar.style .width = `${((currentIndex + 1 ) / lines.length) * 100 } %` ; } function highlightLine (index ) { lines.forEach ((line, i ) => { line.classList .toggle ('active' , i === index); }); lines[index]?.scrollIntoView ({ behavior : 'smooth' , block : 'center' }); } function speakLine (index ) { if (index >= lines.length ) return ; const text = lines[index].textContent ; const utter = new SpeechSynthesisUtterance (text); const selected = voiceSelect.value ; if (voices[selected]) utter.voice = voices[selected]; utter.rate = 1 ; utter.onstart = () => { highlightLine (index); playBtn.disabled = true ; pauseBtn.disabled = false ; resumeBtn.disabled = true ; stopBtn.disabled = false ; }; utter.onend = () => { if (!isPaused) { currentIndex++; if (currentIndex < lines.length ) { speakLine (currentIndex); } else { resetControls (); } } }; currentUtterance = utter; synth.speak (utter); updateUI (); } function resetControls ( ) { playBtn.disabled = false ; pauseBtn.disabled = true ; resumeBtn.disabled = true ; stopBtn.disabled = true ; } playBtn.onclick = () => { currentIndex = 0 ; speakLine (currentIndex); }; pauseBtn.onclick = () => { synth.pause (); isPaused = true ; pauseBtn.disabled = true ; resumeBtn.disabled = false ; }; resumeBtn.onclick = () => { synth.resume (); isPaused = false ; pauseBtn.disabled = false ; resumeBtn.disabled = true ; }; stopBtn.onclick = () => { synth.cancel (); resetControls (); }; resetBtn.onclick = () => { synth.cancel (); currentIndex = 0 ; highlightLine (currentIndex); updateUI (); }; lines.forEach ((line, index ) => { line.addEventListener ('click' , () => { synth.cancel (); currentIndex = index; speakLine (currentIndex); }); }); updateUI ();
✅ 它们如何工作
每一个句子都是<span class="sentence">
我们迭代句子并使用SpeechSynthesisUtterance
大声朗读
在朗读过程中,高亮正确的文本并滚动
一个句子结束后,自动朗读下一个句子
🔚 尾声 阅读了本文,你理解了:
浏览器TTS的运行原理
如何实现动态高亮文本
如何从零构建一个功能齐全的阅读界面
你可以扩展项目功能:
在localStorage
保存阅读进度
添加进度条
加载外部文章或用户输入