|
class tts2 { |
|
constructor() { |
|
console.log("tts constructor"); |
|
window.speechSynthesis.cancel(); //強制中斷之前的語音 |
|
this.synth = window.speechSynthesis; |
|
this.v_index = 0; |
|
|
|
this.last_tts = ""; |
|
|
|
// |
|
|
|
if (localStorage.getItem("ls_rate") === null) { |
|
this.u_rate = 1.2; // 語速 0.1~10 |
|
} else { |
|
this.u_rate = Number(localStorage.getItem("ls_rate")); |
|
} |
|
|
|
if (localStorage.getItem("ls_volume") === null) { |
|
this.u_volume = 0.5; //音量 0~1 |
|
} else { |
|
this.u_volume = Number(localStorage.getItem("ls_volume")); |
|
} |
|
|
|
if (localStorage.getItem("ls_pitch") === null) { |
|
this.u_pitch = 1; //語調 0.1~2 |
|
} else { |
|
this.u_pitch = Number(localStorage.getItem("ls_pitch")); |
|
} |
|
} |
|
|
|
speak2(textToSpeak) { |
|
if (textToSpeak !== null) { |
|
if (textToSpeak.length > 0) { |
|
let filter_text = this._textFilter(textToSpeak); |
|
|
|
console.log('[將要唸的語音]', filter_text); |
|
|
|
if(filter_text.length > 0){ |
|
|
|
try{ |
|
//console.log(document.getElementById("ttsCheck").checked == true ? "[語音開啟]" : "[語音關閉]"); |
|
|
|
let u = new SpeechSynthesisUtterance(); |
|
|
|
u.rate = this.u_rate; |
|
u.volume = this.u_volume; |
|
u.pitch = this.u_pitch; |
|
|
|
u.text = filter_text; |
|
|
|
u.onend = (event) => { |
|
//console.log(event.utterance.text); |
|
this.last_tts = event.utterance.text; |
|
console.log("tts.onend", event.utterance.text); |
|
}; |
|
|
|
u.onerror = (event) => { |
|
//console.log(event); |
|
console.log("tts.onerror", event); |
|
this.cancel2(); |
|
}; |
|
|
|
// |
|
let voices = this.synth.getVoices(); |
|
for (let index = 0; index < voices.length; index++) { |
|
/* |
|
"Google US English" |
|
"Google 日本語" |
|
"Google 普通话(中国大陆)" |
|
"Google 粤語(香港)" |
|
"Google 國語(臺灣)" |
|
*/ |
|
|
|
//console.log(voices[index].name); |
|
|
|
if (voices[index].name == "Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)") { //HsiaoChen (Neural) - 曉臻 (MS Edge專用) |
|
u.voice = voices[index]; |
|
break; |
|
} else if (voices[index].name == "Google 國語(臺灣)") { //Chrome專用 |
|
u.voice = voices[index]; |
|
break; |
|
} else { |
|
//u.lang = 'zh-TW'; //這邊可能會有語音又被切回系統語音的問題 |
|
} |
|
|
|
//當最後一個都還沒找到時才設u.lang |
|
if(index+1 === voices.length){ |
|
u.lang = 'zh-TW'; |
|
} |
|
} |
|
|
|
//console.log("test"); |
|
|
|
u.onstart = (event) => { |
|
//console.log(event.utterance); |
|
//console.log("tts.onstart", filter_text); |
|
|
|
console.log("tts.onstart", event.utterance.text); |
|
if (event.utterance.text === this.last_tts) { |
|
window.speechSynthesis.cancel(); |
|
console.log("tts.cancel", event.utterance.text); |
|
} |
|
}; |
|
|
|
this.synth.speak(u); |
|
}catch (e){ |
|
console.log(e); |
|
} |
|
|
|
} |
|
} |
|
} |
|
|
|
//return this; |
|
} |
|
|
|
cancel2() { |
|
console.log("tts cancel"); |
|
window.speechSynthesis.cancel(); |
|
} |
|
|
|
volume(volume_val) { |
|
let volume = Number(volume_val); |
|
if (volume >= 0 && volume <= 1) { |
|
this.u_volume = volume; |
|
localStorage.setItem("ls_volume", volume); |
|
console.log(`音量調整為: ${this.u_volume}`); |
|
} else { |
|
console.log(`超出範圍`); |
|
} |
|
} |
|
rate(rate_val) { |
|
let rate = Number(rate_val); |
|
if (rate >= 0.5 && rate <= 2) { |
|
this.u_rate = rate; |
|
localStorage.setItem("ls_rate", rate); |
|
console.log(`語速調整為: ${this.u_rate}`); |
|
} else { |
|
console.log(`超出範圍`); |
|
} |
|
} |
|
pitch(pitch_val) { |
|
let pitch = Number(pitch_val); |
|
if (pitch >= 0.1 && pitch <= 2) { |
|
this.u_pitch = pitch; |
|
localStorage.setItem("ls_pitch", pitch); |
|
console.log(`語調調整為: ${this.u_pitch}`); |
|
} else { |
|
console.log(`超出範圍`); |
|
} |
|
} |
|
reset() { |
|
//localStorage.clear(); |
|
localStorage.removeItem("ls_volume"); |
|
localStorage.removeItem("ls_rate"); |
|
localStorage.removeItem("ls_pitch"); |
|
|
|
this.u_rate = 1.2; // 語速 0.1~10 |
|
this.u_volume = 0.5; //音量 0~1 |
|
this.u_pitch = 1; //語調 0.1~2 |
|
} |
|
|
|
_textFilter(msg) { |
|
msg = msg.trim(); //去除前後空白 |
|
|
|
//網址不唸 |
|
//msg = msg.replace(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g, "網址"); |
|
msg = msg.replace(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g, ""); |
|
|
|
//全形轉半形 |
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize |
|
msg = msg.normalize('NFKC'); |
|
|
|
//過濾掉中文,英文,數字,半形空白以外的所有字元 |
|
msg = msg.replace(/[^0-9a-z\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF ]/ig, ""); // *不要用\s取代空白,因為\s包含全形空白 |
|
|
|
msg = msg.trim(); //去除前後空白 |
|
|
|
msg = msg.replace(/^(1){4,}$/g, "一一一"); |
|
msg = msg.replace(/^(2){4,}$/g, "二二二"); |
|
msg = msg.replace(/^(3){4,}$/g, "三三三"); |
|
msg = msg.replace(/^(4){4,}$/g, "四四四"); |
|
msg = msg.replace(/^(5){4,}$/g, "五五五"); |
|
msg = msg.replace(/^(6){4,}$/g, "六六六"); |
|
msg = msg.replace(/^(7){4,}$/g, "七七七"); |
|
msg = msg.replace(/^(8){4,}$/g, "八八八"); |
|
msg = msg.replace(/^(9){4,}$/g, "九九九"); |
|
|
|
msg = msg.replace(/^(w){4,}$/gi, "哇拉"); |
|
|
|
msg = msg.replace(/^484$/gi, "四八四"); |
|
msg = msg.replace(/^87$/g, "八七"); |
|
msg = msg.replace(/^94$/g, "九四"); |
|
msg = msg.replace(/^9487$/g, "九四八七"); |
|
|
|
return msg; |
|
} |
|
|
|
//舊的過濾方法 |
|
_textFilter_old(msg) { |
|
//網址不唸 |
|
//msg = msg.replace(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g, "網址"); |
|
msg = msg.replace(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g, ""); |
|
|
|
msg = msg.replace(/^(1){4,}$/g, "一一一"); |
|
msg = msg.replace(/^(2){4,}$/g, "二二二"); |
|
msg = msg.replace(/^(3){4,}$/g, "三三三"); |
|
msg = msg.replace(/^(4){4,}$/g, "四四四"); |
|
msg = msg.replace(/^(5){4,}$/g, "五五五"); |
|
msg = msg.replace(/^(6){4,}$/g, "六六六"); |
|
msg = msg.replace(/^(7){4,}$/g, "七七七"); |
|
msg = msg.replace(/^(8){4,}$/g, "八八八"); |
|
msg = msg.replace(/^(9){4,}$/g, "九九九"); |
|
|
|
msg = msg.replace(/^(w){4,}$/gi, "哇拉"); |
|
msg = msg.replace(/^(~){3,}$/g, "~~~"); |
|
msg = msg.replace(/^(\.){3,}$/g, "..."); |
|
|
|
msg = msg.replace(/^484$/gi, "四八四"); |
|
msg = msg.replace(/^87$/g, "八七"); |
|
msg = msg.replace(/^94$/g, "九四"); |
|
msg = msg.replace(/^9487$/g, "九四八七"); |
|
|
|
//過濾掉全形符號(防edge bug) |
|
msg = msg.replace(/[\uFF01-\uFF5E]/g, ""); |
|
|
|
//過濾emoji(最多3個,超過就刪除) |
|
msg = msg.replace(/(\ud83d[\ude00-\ude4f]){4,}/g, ""); |
|
|
|
//過濾標點符號 (Punctuation & Symbols) |
|
msg = msg.replace(/[\u0021-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]/g, ""); |
|
|
|
return msg; |
|
} |
|
} |
|
|
|
// |
|
|
|
const tts = new tts2(); |
|
/* |
|
tts.speak2("大家看到我,就知道我是誰了,我就是歐付寶終結者RRRRRRRRRRRRRRRRRRRRR"); |
|
*/ |
又測到一個可能是Edge才有的bug
Google Chrome狀況:
先指定語音("Google 國語(臺灣)"),然後再指定語言("zh-TW")的話會被強制切回微軟系統語音
但是順序反過來的話不會
Edge的狀況:
不管先後順序,只要有指定語言("zh-TW")的話就會被強制切到微軟系統語音
之後再指定語音("Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)")也會無效
所以我修正後的新語法是把查語音列表的迴圈都查完,如果都沒有要的語音才用指定語言("zh-TW")
這樣就能避開Edge的bug
另外語音列隊的部份我是改成自己寫(用array存,一次只拉一句出來唸)
這樣語音列隊數量比較好控管
也比較有辦法去推測語音是否因為bug卡死,然後來重置語音功能
語音卡bug的狀況通常是有onstart,但是會一直沒有onend,也沒有onerror,所以無法用原生的語法直接判定是否卡bug
卡bug通常是遇到特殊符號或是非指定語音的所屬語言
目前測起來Google Chrome比較沒這問題
但是Edge常發生字唸不出來卡住的問題,所以Edge上要使用比較嚴格的字串過濾,來減少bug卡死的機會
但是不確定穩定性就沒有加在上方的範例語法了
上方的語法還是使用原生語法中的語音列隊