用 Web Speech API 給 Twitch.tv 實況製作字幕

2019-02-20

小弟沒甚麼語言天份,基本上除了廣東話以外都聽得很吃力,所以就做了這個瀏覽器插件在看 Twitch.tv 即時加入字幕

Twitch Real-time Subtitle

虛擬音效裝置 Virtual Audio Device

我會使用虛擬音效裝置把網頁的聲音輸出轉換成輸入裝置,把網頁的聲音變成麥克風輸入讓瀏覽器可以辨析得到

Chrome Microphone Popup

Windows 用家可以使用 vb-audio 實現虛擬裝置,而 Linux 需要比較高的技術來去設定,我也是誤打誤撞才成功的,因為太麻煩了有機會再說吧

免費的 Web Speach API

做瀏覽器插件的好處是可以使用免費的 Web Speech API,而不用支付昂貴的 API 費用,例如 Google API,然而代價就是 API 的語音全部都會經過他們的伺服器,但又不是錄我的聲音所以沒關係吧

使用方法很簡單,只要幾句 code 就可以運行了

const recognition = new webkitSpeechRecognition();

recognition.lang = "en-US";
recognition.start();
recognition.interimResults = false;

recognition.onresult = (event) => {
  const sentence = event.results[0][0].transcript;
};

interimResults 預設是開啟的,效果是只要語音有小停頓也會即時回傳結果,像這樣

Response 1: I
Response 2: I go to school
Response 3: I go to school by bus

如果把它關閉,API 會盡量在句子完結時再回傳,這樣回傳的速度會比較慢但相對可讀性也會提高

用 jQuery 插入字幕

接收到句子後便可以即時把它插入到影片內,使用 jQuery 的原因只是為了方便,本地插件就不考慮效能問題了,這邊我要先取得目標網站的影片 container,以 Twitch 為例就是 .video-player__container,然後在裡面插入 <div id="my-subtitle-container"></div> 為字幕預留空間

function getSubtitleContainer() {
  let container = $("#my-subtitle-container");
  const videoContainerEle = ".video-player__container"; // 這是 Twitch 的
  const wrap = $(videoContainerEle);
  if (container.length === 0) {
    $(".video-player__container").append(
      '<div id="my-subtitle-container"></div>',
    );
    container = $(videoContainerEle);
  }
  return container;
}

從 API 接收到 event 的時候便更改 container 內的 html

recognition.onresult = (event) => {
  const sentence = event.results[0][0].transcript;
  $("#my-subtitle-container").html(`<p>${sentence}</p>`);
};

用簡單的 CSS 把字幕移到影片框架的底部正中

#my-subtitle-container {
  position: absolute;
  bottom: 5%;
  width: 100%;
  text-align: center;
}

#my-subtitle-container p {
  display: inline-block;
  margin: 0 auto 2px auto;
  font-family: "微軟正黑體";
  color: #000;
  font-weight: bold;
  background: rgba(255, 255, 255, 0.8);
  padding-left: 5px;
  padding-right: 5px;
}

翻譯伺服器

中文以外的語音我會先進行翻譯再顯示,所以我們需要一個翻譯伺服器,在接收到句子時先進行翻譯,伺服器詳情可參考自製「Chrome 版 Word Wise」智能翻譯英文生字,至於中文的直接顯示出來就好了

提升字幕可讀性

因為我設定了 API 只會在句末的時候才回傳,所以字幕顯示延遲是一定有的,而且很容易被下一句字幕覆蓋掉,所以我做了一個堆疊字幕的效果,讓字幕能夠在屏幕逗留 5 秒鐘

class SubTitle {
  timestamp: Moment;
  text: string;

  constructor(text: string) {
    this.text = text;
    this.timestamp = moment();
  }
}

function render() {
  const expired = subtitles.find(subtitle =>
    subtitle.timestamp.isBefore(moment().add(-5, 'seconds'))
  );
  if (expired) {
    subtitles.shift();
  }
  $('#my-subtitle-container').html(
    subtitles.map(subtitle => `${subtitle.text}<br/>`).join()
  );
}

const subtitles = [];

recognition.onresult = event => {
  const sentence = event.results[0][0].transcript;
  subtitles.push(new SubTitle(sentence));
  render();
};

setInterval(() => {
  render();
}, 1000);

防止 API 中斷

這個 API 有一個很奇怪的特性,在 API 回傳之後有一定機率會停止 recognition,或許是我用的方法錯了,但我發現每次回傳後再 start() 一次基本上就不會再斷,所以就沒有考究了

recognition.onresult = function(event) {
  const sentence = event.results[0][0].transcript;
  setTimeout(() => {
    try {
      recognition.start();
    } catch (err) {}
  });
  ...
}