# 文字語音播報(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>