# 文字語音播報(Nuxt3) - 功能:使用JS可以將網頁的文字用瀏覽器的語音包播報。 <small>以下程式碼目前只有語音的播放及結束,尚未提供暫停功能</small> - 注意事項:每個瀏覽器、行動裝置的語音包有所不同需注意! ```htmlembedded= <template> <div> <p ref="articleContent">語音播報內容</p> <div class="speakBtnBox"> <button class="speakBtn" @click="speak"> <template v-if="isPlay"> <div class="stop"></div> </template> <template v-else> <div class="play"></div> </template> </button> </div> </div> </template> <script setup> const isPlay = ref(false) // 是否在播報中 const articleContent = ref(null) // 確認語音包已被加載 const loadVoices = () => { if (window.speechSynthesis) { window.speechSynthesis.resume(); // 確保音頻播放未被暫停 } return new Promise((resolve) => { let voices = window.speechSynthesis.getVoices(); if (voices.length !== 0) { resolve(voices); } else { window.speechSynthesis.onvoiceschanged = () => { voices = window.speechSynthesis.getVoices(); resolve(voices); }; } }); }; // 處理過長文本 const splitTextByLength = (text, maxLength = 200) => { const sentences = text.match(/[^。!?.!?]*[。!?.!?]/g) || [text]; const chunks = []; for (let sentence of sentences) { while (sentence.length > maxLength) { chunks.push(sentence.slice(0, maxLength)); sentence = sentence.slice(maxLength); } if (sentence) chunks.push(sentence); } return chunks; }; // 播放段落的語音 const playChunk = (chunk, selectedVoice, retries = 0, maxRetries = 3) => { return new Promise((resolve, reject) => { window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(); utterance.voice = selectedVoice; utterance.lang = selectedVoice.lang; utterance.text = chunk; utterance.volume = 1; utterance.pitch = 1 utterance.rate = 1 utterance.onstart = async () => { console.log("段落播放開始: ", chunk); // 避免螢幕進入省電模式中斷語音 if (!wakeLock && 'wakeLock' in navigator) { try { wakeLock = await navigator.wakeLock.request('screen'); console.log("Wake lock acquired"); } catch (err) { console.error("Failed to acquire wake lock", err); } } }; utterance.onend = () => { console.log("段落播放完成"); resolve(); // 當播放完成時,繼續下一段 }; utterance.onerror = (event) => { if(event.error !== 'interrupted'){ console.error("onerror: ", event.error); if (retries < maxRetries) { console.log(`重試第 ${retries + 1} 次...`); resolve(playChunk(chunk, utterance, retries + 1, maxRetries)); // 重試失敗的段落 } else { reject(new Error("播放失敗,已達最大重試次數")); } } }; window.speechSynthesis.speak(utterance); }); }; let wakeLock = null // 釋放 wakeLock const unmountWakeLock = async() =>{ if (wakeLock) { try { await wakeLock.release(); wakeLock = null; console.log("Wake lock released"); } catch (error) { console.error("Failed to release wake lock", error); } } } // 設置播放狀態 const setPlayState = (state) => { isPlay.value = state; if (!state) { window.speechSynthesis.cancel(); // 停止播放 unmountWakeLock() } }; // 點擊播放或結束 const speak = async() =>{ window.speechSynthesis.cancel(); // 提前調用 window.speechSynthesis.getVoices() 獲取可用語音 const voices = await loadVoices(); // 如果正在播放,則停止播放 if(isPlay.value){ setPlayState(false) return; // 退出,防止重複播放 } else { if (!article_text_block?.value) { console.error("無法捕捉到文本標籤"); return; } let readContent = article_text_block.value.innerText; if (!readContent) { console.error("無法獲取內文"); return; } if(voices){ // 設置語音包 const priorityVoices = [ { voiceURI: '美佳' }, { voiceURI: 'Microsoft Yating - Chinese (Traditional, Taiwan)' }, ]; const selectedVoice = priorityVoices .map((pv) => voices.find((v) => v.voiceURI === pv.voiceURI || v.name === pv.voiceURI)) .find((v) => v !== undefined) || voices.find((v) => /zh[-_]?tw/i.test(v.lang)); if (!selectedVoice) { alert("您的裝置不支援此播放功能"); return; } // 開始播放 setPlayState(true) const chunks = splitTextByLength(readContent); // 文本分段 try { const startTime = Date.now(); // 記錄整篇文章開始播放的時間 for (const chunk of chunks) { if (!isPlay.value) break; // 中止播放 await playChunk(chunk, selectedVoice); } const elapsedTime = (Date.now() - startTime) / 1000; // 計算整篇文章播放耗時(秒) if (elapsedTime < 10) { // 如果整篇文章播放時間小於 10 秒 alert("播放時間過短,可能語音包異常,建議更新文字轉語音的語音包。"); } } catch (error) { console.error("語音播放失敗:", error); } finally { setPlayState(false) } } } } // window 系統 onBeforeUnmount 重整頁面時無法停止播放故在完成渲染時先執行一次停止播放 onMounted(() =>{ window.speechSynthesis.cancel(); // 釋放 wakeLock unmountWakeLock() }) // 離開當前頁面時停止播放 onBeforeUnmount(() => { window.speechSynthesis.cancel(); // 釋放 wakeLock unmountWakeLock() }) </script> <style lang="scss" scoped> $white: #fff; $Blue: #177BA7; .speakBtnBox{ display: flex; justify-content: flex-end; .speakBtn{ width: 50px; height: 50px; border-radius: 50%; border: none; background: $Blue; color: $white; display: flex; justify-content: center; align-items: center; @media screen and (max-width: 960px){ width: 40px; height: 40px; } .stop{ width: 12px; height: 12px; background: $white; @media screen and (max-width: 960px){ width: 10px; height: 10px; } } .play{ border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-left: 15px solid $white; border-right: 15px solid transparent; transform: translate(10px); @media screen and (max-width: 960px){ border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-left: 12px solid $white; border-right: 12px solid transparent; transform: translate(7.5px); } } } } </style> ``` ### 參考資料 [js純前端實現語音播報,朗讀功能](https://blog.csdn.net/hap1994/article/details/137788111) ### 筆記更新 #### 2024/11/28更新筆記 - 添加判定當未匹配到預設的語音包時,從voices取得語音包中第一筆lang包含了zh_tw 或 zh-tw (忽略大小寫)的語音包,修正Android系統語音未取得bug - 文本改為分段播放,修正Android系統文本過長時無法正確播放語音bug - 添加wakeLock,避免手機進入省電模式關閉螢幕導致語音中斷 (需注意添加時機,錯誤的時間添加在iphone可能會造成語音無法正確播放) <small>備註:</small> <small style="padding-left: 2em;">1.確認行動裝備 ios系統、Android系統:小米手機、Sony手機 可正常運行</small> <small style="display:block; padding-left: 2em;">2.Samsung手機在手機的語言包未更新的時,可能會出現speechSynthesis.speak在短暫時間內直接將文本結束,導致語音無法正常使用</small> ------------------------------------------------------------------ <small>**持續更新中!**</small> <small>**以上為自學中的筆記如有錯誤,懇請留言指正,感謝!**</small>