文字到語音(tts)

_clai發表於2024-05-02

Web Speech API

使你能夠將語音資料合併到 Web應用程式中。Web Speech API 有兩個部分:SpeechSynthesis 語音合成(文字到語音 TTS)和 SpeechRecognition 語音識別(非同步語音識別)

SpeechSynthesis: 語音服務的控制器介面, 獲取裝置上關於可用的合成聲音的資訊,開始、暫停語音,或除此之外的其他命令

語音合成透過 SpeechSynthesis 介面進行訪問,它提供了文字到語音(TTS)的能力,這使得程式能夠讀出它們的文字內容(通常使用裝置預設的語音合成器)。不同的聲音類型別透過 SpeechSynthesisVoice 物件進行表示,不同部分的文字則由 SpeechSynthesisUtterance 物件來表示。你可以將它們傳遞給 SpeechSynthesis.speak() 方法來產生語音。

SpeechSynthesisUtterance: 語音請求。 它包含語音服務應該閱讀的內容以及如何閱讀的資訊(例如語言,音高和音量)

SpeechRecognition: 語音識別

語音識別透過 SpeechRecognition 介面進行訪問,它提供了識別從音訊輸入(通常是裝置預設的語音識別服務)中識別語音情景的能力。一般來說,你將使用該介面的建構函式來構造一個新的 SpeechRecognition 物件,該物件包含了一系列有效的物件處理函式來檢測識別裝置麥克風中的語音輸入。SpeechGrammar 介面則表示了你應用中想要識別的特定文法。文法則透過 JSpeech Grammar Format (JSGF.) 來定義。

SpeechGrammar: 語音識別物件服務想要識別的一系列詞語或模式


  • 文字到語音
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Speech API</title>
  </head>
  <body>
    <strong>Web Speech API</strong>
    <hr />
    <select class="select-voice"></select>
    <textarea class="text" cols="50" rows="10"></textarea><br /><br />
    <button type="button" class="btn-play">文字語音播放</button>
    <button type="button" class="btn-pause">暫停播放</button>
    <button type="button" class="btn-resume">恢復播放</button>
    <button type="button" class="btn-end">停止播放</button>

    <script>
      // Web Speech API

      const playBtn = document.querySelector('.btn-play');
      const pauseBtn = document.querySelector('.btn-pause');
      const resumeBtn = document.querySelector('.btn-resume');
      const endBtn = document.querySelector('.btn-end');

      // 文字轉語音

      // 建立 SpeechSynthesisUtterance 物件
      const synth = globalThis.speechSynthesis;
      // console.log("synth => ", synth)

      const text = document.querySelector('.text');
      text.value = 'hellow world, this is a test for web speech api.';

      // 選擇語音聲音
      const selectVoice = document.querySelector('.select-voice');
      const fragment = document.createDocumentFragment();
      const voiceList = [];
      synth.addEventListener('voiceschanged', () => {
        if (voiceList.length === 0) {
          synth.getVoices().forEach((voice) => {
            if (voice.lang.includes('zh')) {
              const option = document.createElement('option');
              option.dataset.lang = voice.lang;
              option.value = voice.name;
              option.textContent = voice.name;
              fragment.appendChild(option);
              voiceList.push(voice);
            }
          });
          selectVoice.appendChild(fragment);
        }
        // 選擇語音聲音
        handleSelectVoice();

        playBtn.removeAttribute('disabled');
      });

      selectVoice.addEventListener('change', handleSelectVoice);

      // 切換語音聲音
      function handleSelectVoice() {
        /** @type {SpeechSynthesisVoice} */
        const selectedVoice = voiceList.at(selectVoice.selectedIndex);
        utterance.voice = selectedVoice;
        // console.log('selectedVoice => ', selectedVoice.name);
      }

      const utterance = new SpeechSynthesisUtterance();
      // 設定文字內容
      utterance.text = text.value;
      const info = {
        start: 0,
        end: 0,
        elapsedTime: 0,
        paused: false,
      };
      playBtn.addEventListener('click', () => {
        // 移除所有語音談話佇列中的談話
        synth.pending && synth.cancel();
        // 新增一個 utterance 到語音談話佇列;它將會在其他語音談話播放完之後播放。
        synth.speak(utterance);
      });
      // 暫停播放
      pauseBtn.addEventListener('click', () => {
        synth.pause();
      });
      // 恢復播放
      resumeBtn.addEventListener('click', () => {
        synth.cancel();
        const sliceText = utterance.text.slice(info.end);
        // console.log('sliceText => ', sliceText);
        utterance.text = sliceText;
        synth.speak(utterance);
      });
      // 結束播放
      endBtn.addEventListener('click', () => {
        synth.cancel();
        info.paused = false;
      });

      utterance.addEventListener('boundary', (e) => {
        const {
          charIndex,
          charLength,
          elapsedTime,
          utterance: { text },
        } = e;
        // name: `word` 所語音的字元,`sentence` 完整句的邊界

        // 儲存正在語音的字元索引和已讀時間
        // const char = text.slice(charIndex, charIndex + charLength);
        info.start = charIndex;
        info.end = charIndex + charLength;
        info.elapsedTime = elapsedTime;
      });
      utterance.addEventListener('pause', (e) => {
        console.log('pause');
      });
      utterance.addEventListener('resume', (e) => {
        console.log('resume');
      });
      utterance.addEventListener('end', (e) => {
        console.log('end');
      });

      window.addEventListener('beforeunload', () => {
        // 停止播放
        synth.pause();
        synth.cancel();
      });
    </script>
  </body>
</html>
  • 語音識別
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Speech API</title>
  </head>
  <body>
    <strong>Web Speech API</strong>
    <hr />
    <audio src="./music.m4a" controls></audio>
    <textarea class="text" cols="50" rows="10"></textarea><br /><br />
    <button type="button" class="btn-speech">語音轉文字</button>

    <script>
      // Web Speech API

      const text = document.querySelector('.text');

      // 按鈕控制
      const speechBtn = document.querySelector('.btn-speech');

      // 語音識別

      // 建立 SpeechRecognition 物件
      /** @type {SpeechRecognition} */
      const recognition = new webkitSpeechRecognition();
      // console.log('recognition => ', recognition);

      // 是否連續識別
      recognition.continuous = true;
      // 識別結果是否包含中間結果
      recognition.interimResults = true;
      // 識別語言
      recognition.lang = 'zh-CN'; // zh-CN, en-US

      speechBtn.addEventListener('click', () => {
        // 開始識別
        recognition.start();
      });

      recognition.onstart = (e) => {
        console.log('開始', e);
      };
      recognition.onaudiostart = (e) => {
        console.log('開始錄音');
      };
      recognition.onspeechstart = (e) => {
        console.log('開始說話');
      };
      // 識別結束
      recognition.onspeechend = (e) => {
        console.log('語音識別結束');
        recognition.stop();
      };
      recognition.onaudioend = (e) => {
        console.log('結束錄音');
      };
      recognition.onend = (e) => {
        console.log('結束');
        // 結束後,重新開始識別
        recognition.start();
      };

      // 識別結果
      recognition.onresult = (e) => {
        const resultList = Object.values(e.results);
        let str = '';
        resultList.forEach((result) => {
          str += result[0].transcript + '\n';
        });

        text.value = str;
        console.log('識別結果: ',e.resultIndex, str);
      };

      // 未識別出結果
      recognition.onnomatch = (e) => {
        console.log('No match', e);
      };

      // 識別錯誤
      recognition.onerror = (e) => {
        // not-allowed:使用者禁止訪問麥克風許可權 audio-capture: 麥克風未開啟 no-speech: 沒有檢測到語音 network: 網路連線問題
        console.log('識別錯誤原因: ', e.error);
        if (e.error === 'not-speech') {
          recognition.stop();
        }
      };
    </script>
  </body>
</html>

相關文章