用 Web Speech API 給 Twitch.tv 實況製作字幕
2019-02-20
小弟沒甚麼語言天份,基本上除了廣東話以外都聽得很吃力,所以就做了這個瀏覽器插件在看 Twitch.tv 即時加入字幕
虛擬音效裝置 Virtual Audio Device
我會使用虛擬音效裝置把網頁的聲音輸出轉換成輸入裝置,把網頁的聲音變成麥克風輸入讓瀏覽器可以辨析得到
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) {}
});
...
}