Skip to content

Instantly share code, notes, and snippets.

@Eotones
Last active March 14, 2024 08:30
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save Eotones/d67be7262856a79a77abeeccef455ebf to your computer and use it in GitHub Desktop.
Save Eotones/d67be7262856a79a77abeeccef455ebf to your computer and use it in GitHub Desktop.
speechSynthesis強制使用Chrome中的Google小姐中文語音

speechSynthesis強制使用Chrome中的Google小姐中文語音

網路上的window.speechSynthesis教學主要都只有說切換指定語言

像是這樣就能切換成中文語音:

const synth = window.speechSynthesis;

const speak = (msg) => {
  let u = new SpeechSynthesisUtterance();
  u.lang = 'zh-TW';
  u.text = msg;
  synth.speak(u);
};

speak("你要讀出的中文內容1");
speak("你要讀出的中文內容2");

但是這種寫法在系統有支援多種語音的情況下只會選擇系統預設語音

  • win7繁中 - 無支援中文語音
    • 2020/8月補充: 現在win7繁中版可以去載Chromium版的Edge來使用微軟中文語音
    • 2021/9月補充: 微軟Chromium版的Edge新增新的類神經元運算的語音,效果會更接近人聲
      • 台灣的話推薦用Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)語音
      • 剛裝好的Chromium版的Edge不會馬上有新的類神經元運算語音,似乎都要用javascript去使用語音功能來觸發內部的語音列表更新,更新完後要重開瀏覽器(觸發方式可以用下方那行的測試網頁來觸發語音列表更新)
      • edge語音列表是隱藏在內部的更新,外觀完全看不出來有在更新,只能靠重新整理下方網頁來確認新的語音列表是否載入完成
    • 瀏覽器支援的語音列表可以到這個網頁測試: Speech synthesiser
  • win7簡中 - 有支援微軟中文語音(系統預設)
  • win10,win11 - 簡繁中都有支援微軟中文語音(系統預設)

如果想要切換成其他語音(例如Chrome才有的Google中文語音)

就要另外再寫

const synth = window.speechSynthesis;

const speak = (msg) => {
  let u = new SpeechSynthesisUtterance();
  //u.lang = 'zh-TW'; //這句絕對不要用
  u.text = msg;

  // 用 synth.getVoices() 取得支援的語音列表
  // 要注意網頁開啟後首次執行 window.speechSynthesis 後才會把語音功能載入
  // 大概要花幾秒鐘的時間才能完全載入,再來才能正確取得語音列表
  // 所以要想辦法延遲 synth.getVoices(); 的執行時間
  // 
  // 或是使用下面的事件偵測
  // (這個事件在載入過程會被觸發多次,很難用,他是偵測changed不是偵測完全載入完成,
  // 但這是window.speechSynthesis裡唯一的一個事件偵測,沒其他選擇)
  // var voices;
  // window.speechSynthesis.onvoiceschanged = function() {
  //   voices = synth.getVoices();
  //   // 注意 speak() 不要直接寫在這裡,不然會被重覆觸發多次
  // };
  let voices = 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';
    }
  }

  synth.speak(u);
};

speak("你要讀出的中文內容1");
speak("你要讀出的中文內容2");

參考資料

window.speechSynthesis是瀏覽器內原生的語音功能,詳細用法可以看以下MDN的教學文件

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");
*/
@Eotones
Copy link
Author

Eotones commented May 21, 2020

更正一個以前寫錯的地方

const synth = window.speechSynthesis;

const speak = (msg) => {
  let u = new SpeechSynthesisUtterance();
  u.text = msg;

  synth.speak(u);
};

speak("語音內容1");
speak("語音內容2");

第4行和第5行應該是每一句新語音都要重新new一次
之前寫法耍蠢把第4行和第5行拿來重複使用
所以u.text的語音內容會一直被洗掉
如果有語音一直讀錯句的話要注意是不是這個問題

@geminixiang
Copy link

geminixiang commented Jun 24, 2020

Vue.js 版本如下,要用的時候呼叫 this.speak(文字)

var vm = new Vue({
  el: "#app",
  data: {
    synth: window.speechSynthesis
  },
  methods {
    speak(msg) {
      this.synth.cancel(); //取消上一次發聲,避免讀到空字串而卡住 2021-05-15
      let u = new SpeechSynthesisUtterance();
      u.text = msg;
      this.synth.speak(u);
    }
  }
}

搞了一陣子,原本想用以下Google TTS改網址,但非常不太穩定
https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=en-us&q=hello

@Eotones
Copy link
Author

Eotones commented Sep 9, 2021

目前Chromium版本Edge下使用"Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)"測出來會導致語音卡死的bug:

  • 中文全形符號(包含英文數字全形)
  • 空白

遇到這些字元會導致語音不會唸,會觸發onstart,但不會觸發onend,也不會觸發onerror
因為無法正確觸發onend,所以導致語音列隊中後續的句子都不會唸

Chrome中文語音目前不會有這問題
Chromium版本Edge之後會不會修好就不知道了

@Eotones
Copy link
Author

Eotones commented Sep 28, 2021

又測到一個可能是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卡死的機會

但是不確定穩定性就沒有加在上方的範例語法了
上方的語法還是使用原生語法中的語音列隊

@Eotones
Copy link
Author

Eotones commented Oct 6, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment