位元詩人 [網頁設計] 教學:使用免費的網頁語音合成 API 撰寫會說外語的程式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

藉由語音合成 (Text To Speech) 技術,就不需要為每段文字預錄語音檔案,可透過電腦運算即時發聲。由於語音合成算是相對困難的技術,通常都是透過付費 API 來取得這項特性,隨使用量付費。現在瀏覽器也內建這項技術了,本文會介紹如何使用網頁語音合成 API。

由於網頁語音合成 API 尚在起步階段,各個瀏覽器對這項特性的支援程度不一。目前來說,Chrome 做得最好,其他瀏覽器則支援有限。本文也會討論瀏覽器的支援度議題。

撰寫第一個範例

我們先暫時不考慮瀏覽器支援度,直接寫最簡短的範例如下:

let utterance = new window.SpeechSynthesisUtterance('Hello World');
utterance.lang = 'en-US';
window.speechSynthesis.speak(utterance);

目標文字會存入 SpeechSynthesisUtterance 物件。除了文字外,也要設置 utterance 的語系,因為語言合成 API 不負責辨識文字的語系。

實際發聲的函式是 SpeechSynthesis.speak()。把設置好的 utterance 當參數傳入即可,使用上並不複雜。

偵測瀏覽器是否支援語音合成 API

對於瀏覽器來說,語音合成 API 算是相對新穎的技術,故各個瀏覽器對其支援程度不一。在使用這項特性時,應該要在程式中主動偵測目標瀏覽器是否支援,而不是一廂情願地認定客戶端一定能使用該 API。

最基本的方式是偵測語音合成 API 是否能用:

if (!!(window.speechSynthesis) && !!(window.SpeechSynthesisUtterance)) {
    /* TTS API is supported. */
}

若該 API 無法使用,要有相對應的處理方式。我們會在下一節討論此議題。

除了支援該 API 外,也要確認是否支援目標語系。本段範例程式碼以日文為目標語言:

let targetLang = 'ja-JP';
let supported = false;

let voices = window.speechSynthesis.getVoices();
for (let i = 0; i < voices.length; ++i) {
  if (voices[i].lang === targetLang) {
    supported = true;
    break;
  }
}

只有在 supportedtrue 時,代表目標瀏覽器支援日文,這時候就可以使用語音合成 API。

Chrome 直接支援 20 餘種語系的語音合成 API,不依賴宿主系統的語音合成功能。所以,稍微偷吃步的方法是直接偵測目標瀏覽器為 Chrome:

if (window.chrome && !window.opr) {
  /* The HTTP client is Chrome. */
}

理論上,舊版的 Chrome 有可能不支援語音合成 API。但 Chrome 等現代瀏覽器 (modern browser) 基本上都是滾動更新,只要考慮最新兩個版本的支援度即可。

處理瀏覽器沒有語音合成 API 的情境

當瀏覽器不支援語音合成 API 時,就要使用替代性的方法。替代方法會視語音合成 API 對網頁程式的重要性而異。

如果語音合成 API 是可有可無的功能,建議在瀏覽器不支援語音合成 API 時直接隱藏發聲按鈕。因為功能異常的按鈕比沒有按鈕的使用者體驗更差。

反之,若語音合成 API 是必要的功能,則必需使用第三方語音合成 API 為備案。由於語音合成 API 是相對困難的功能,基本上都要付費,而且要自己寫後端程式。

網路上有一些語音合成 API 的 polyfill,那些 polyfill 都是假的。所謂的語音合成 polyfill 只是在背後跑一些第三方語音合成 API,這些第三方 API 背後的公司在發現自家的 API 被濫用後都會鎖住該功能。所以,還是老老實實自架語音合成 API,才能確保該功能可用。

實際範例

在本節中,我們綜合先前所談論的內容,根據兩種不同的情境,各寫一段範例程式碼。

偵測語音合成 API 並製作按鈕

在本小節中,我們只在目標瀏覽器支援網頁語音合成 API 時,才以 JavaScript 動態生成發聲按鈕。反之,若目標瀏覽器不支援語音合成 API,我們就不生成發聲按鈕。

參考範例程式碼如下:

/* `t` is utterance. */

let speaker;
if (window.chrome && !window.opr) {
  /* Currently, only Chrome support Japanese TTS by default.
     Hence, we always show the speaker when the target browser
     is Chrome. */
  speaker = document.createElement('img');
  speaker.src = '/img/speaker.png';
  speaker.classList.add('btn-sound');
  speaker.addEventListener('click', function () {
    let utterance = new SpeechSynthesisUtterance(t);
    utterance.lang = 'ja-JP';
    window.speechSynthesis.speak(utterance);
  });
}
else if (!!window.speechSynthesis && !!window.SpeechSynthesisUtterance) {
  /* Other browsers only support what the hosts provide.
     Hence, we add a speaker button only if Japanese TTS
     is supported on a host. */
  let voices = window.speechSynthesis.getVoices();

  let supported = false;

  for (let i = 0; i < voices.length; ++i) {
    if (voices[i].lang === 'ja-JP') {
      supported = true;
      break;
    }
  }

  if (supported) {
    speaker = document.createElement('img');
    speaker.src = '/img/speaker.png';
    speaker.classList.add('btn-sound');
    speaker.addEventListener('click', function () {
      let utterance = new SpeechSynthesisUtterance(t);
      utterance.lang = 'ja-JP';
      window.speechSynthesis.speak(utterance);
    });
  }
}

if (speaker) {
  /* Add `speaker` to target webpage. */
}

在此段範例程式碼中,我們先辨識目標瀏覽器是 Chrome 還是其他瀏覽器。當目標瀏覽器是 Chrome 時,我們認定目標瀏覽器器支援日文語音合成,故直接以純 JavaScript 動態生成發聲按鈕。

當目標瀏覽器不是 Chrome 時,會進行兩階段偵測。先偵測語音合成 API 是可用的,再偵測是否支援日文的語音合成。當兩項條件皆成立時,才以 JavaScript 生成發聲按鈕。

這段程式直接以網頁 API 來寫,不透過前端 JavaScript 函式庫。其實原生 JavaScript 本來就有動態生成 UI (使用者介面) 的功能,使用前端框架是為了自動處理資料和頁面間的連動性。

以後端 API 做為語音合成 API 的備案

在本小節中,我們優先使用瀏覽器所提供的網頁語音合成 API 來發聲,這樣可以節約網站的費用。只有在目標瀏覽器不支援語音合成 API 時,我們才動用自架的語音合成 API。

參考範例程式碼如下:

// @flow
function playSound(word: string) {
  let supported = false;

  if (window.chrome && !window.opr) {
    /* Currently, only Chrome support Japanese TTS by default.
       Hence, we utilize it when the client is Chrome. */
    let utterance = new SpeechSynthesisUtterance(word);
    utterance.lang = 'ja-JP';
    window.speechSynthesis.speak(utterance);
    supported = true;
  }
  else if (!!window.speechSynthesis && !!window.SpeechSynthesisUtterance) {
    /* Other browsers only support what the hosts provide.
       Hence, we utilize Japanese TTS only if it is supported
       on a host. */
    let voices = window.speechSynthesis.getVoices();

    for (let i = 0; i < voices.length; ++i) {
      if (voices[i].lang === 'ja-JP') {
        supported = true;
        break;
      }
    }

    if (supported) {
      let utterance = new SpeechSynthesisUtterance(word);
      utterance.lang = 'ja-JP';
      window.speechSynthesis.speak(utterance);
    }
  }

  if (!supported) {
    /* Fallback to Google Cloud TTS when no built-in Japanese TTS. */
    superagent
      .post(`${baseUrl}/tts/`)
      .send({ source: word })
      .set('accept', 'json')
      .then(function (res) {
        let src = `data:audio/mp3;base64,${res.body.output}`;

        let sound = new Howl({
          src: [src]
        });

        sound.play();
      })
      .catch(function (err) {
        if (err.response) {
          console.log(err.response);
        }
      });
  }
}

如同前一小節,我們會偵測目標瀏覽器是否為 Chrome。若為是,則直接發聲。若為否,則要先偵測是否支援日文語音合成,才發聲。

若瀏覽器沒有提供這項特性時,我們就以 Ajax 呼叫自架的後端程式。該後端程式會回傳 base64 字串形態的語音資料。

為了節省篇幅,這裡不展示後端程式的寫法。每家語音合成 API 都會有範例程式,讀者可自行參考相關文件。

附註

有些瀏覽器在頁面載入後才能正常使用語音合成 API,故建議把語音合成的程式碼寫在 DOMContentLoaded 事件內。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。