YouTubeLiveにてVTuberの動画や生放送を眺めていると,母国語以外のチャットが流れることもありそれらのチャットをリアルタイムに拾うことができていない配信者が見受けられます.
確かにいちいち翻訳ツールにかけるのは面倒ですが,この状況は非常にもったいないと感じます.
そこで比較的自然な翻訳をしてくれるDeepLを用いて自動的にチャットを翻訳するツールがあれば便利なのでは?と思い作ってみました.
Summary of technical topics
Monitor the chat field of YouTube Live with MutationObserver, determine the language with chrome.i18n.detectLanguage(), translate with DeepL API, and replace textContent
with translation result.
※ 技術的な話題は ココ から始まります
先行事例と課題
**YouTube LiveのチャットをDeepL翻訳してリアルタイム表示する**はPythonのtkinterを用いたGUIアプリケーションであり,YouTube data APIでチャットを取得している.
しかし...
-
実行ファイルは提供されていないのでPython環境を構築する必要があり,環境構築が壁になる(実際,利用しようとして苦戦している方をTwitterで見かけた)
-
すべてのチャットを翻訳してしまうので DeepL APIの使用料が気になる
-
以前YouTubeLiveのチャットをniconico風にオーバーレイする【Python】を作ったときYouTube data APIの利用上限が厳しいと感じたので,できればYouTube data API以外の方法でチャットを取得したい
-
Liveアーカイブでは利用できない
作ったもの
Google Chrome Web Storeにて無料公開されています.
拡張機能をインストールできる人であれば (DeepL APIを契約次第) だれでも簡単に利用開始できます.
Chat Translator for DeepL | Chrome 拡張機能
できること
- YouTubeLiveのチャットの言語を解析し,翻訳したい言語であればDeepL APIを用いて翻訳結果に自動的に書き換える
- Chat v2.0 Style GeneratorのようなカスタムCSSを適用可能
推しポイント
-
Chrome拡張機能なのでChromeを使っている人であればダウンロードするだけで(=環境構築などせずに)誰でも簡単に利用できる
-
チャットの言語を簡易的に判定して翻訳したい言語に含まれている場合のみ翻訳するので,無駄にDeepLAPIを使用せずに済む
-
YouTube上のチャット欄を参照してチャットを取得するので,YouTube data API使用量上限を気にせずに利用できる
- すなわち,チャット欄が残されているLiveアーカイブでも利用できる
- そもそもYouTube data APIの使用申請が結構面倒なのでそこの壁をなくした点も大きい
ペルソナ
- YouTubeLiveのチャットを見て視聴者とコミュニケーションをとる配信者
- Viewers of live broadcasts in languages other than their native tongue
技術的な話
Chat Translator for DeepLは様々な機能を持っているが,ここではチャット欄に関連する内容のみを扱う.
拡張機能概要
ファイル名 | 備考 |
---|---|
manifest.json | 拡張機能に用いるファイル類を定義 |
chat-translator.js | 言語解析,翻訳,翻訳結果への置換 |
options.js, options.html | 各種設定変更 |
manifest.json
{
"manifest_version": 2,
"name": "Chat Translator for DeepL",
"version": "1.0.3",
"permissions": ["storage","identity","identity.email"],
"description": "Translate YouTube chats using DeepL API",
"icons": { "128": "icon128.png" },
"web_accessible_resources": ["icon24.png"],
"options_ui":{
"page":"options.html",
"chrome_style":true
},
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_popup": "options.html",
"run_at": "document_start"
},
"content_scripts": [
{"run_at":"document_end",
"matches":["https://www.youtube.com/live_chat*"],
"all_frames":true,
"js":["chat-translator.js"]}
]
}
ポイントはchat-translator.js
を読み込む下の部分の"all_frames":true,"
.
"content_scripts": [
{"run_at":"document_end",
"matches":["https://www.youtube.com/live_chat*"],
"all_frames":true,
"js":["chat-translator.js"]}
]
Content scripts - Chrome Developersによると,
The "all_frames" field allows the extension to specify if JavaScript and CSS files should be injected into all frames matching the specified URL requirements or only into the topmost frame in a tab.
であり,YouTubeのチャット欄はiframe
として挿入されているので,この設定をしないとjsが読み込まれない.
chat-translator.js
チャット取得
チャットが書かれるたびに自動的に翻訳したい.
これは,MutationObserverでチャット欄全体を監視することで実現できる.
具体的には以下のとおりである.
let chatsClass = document.querySelector("#items.yt-live-chat-item-list-renderer");
let observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.id == "message") {
detectLanguage(mutation.target);//言語解析関数
}
});
});
observer.observe(chatsClass, {
childList: true,
subtree: true,
});
-
chatsClass
はチャットアイテムのリスト -
chatsClass
以下に何かが追加されるたびにその要素をmutation.target
として取得できる - チャットのテキストは
mutation.target
のid
がmessage
になっているので,その場合のみdetectLanguage()
(後述)を実行する.
言語解析(detectLanguage()
)
以上により流れてきたチャットのテキストをリアルタイムに取得することができるようになった.あとはそれを翻訳すればよいのだが,やみくもにすべてのチャットを翻訳してしまうとDeepL APIの使用料がとんでもないことになってしまう.
したがって,DeepLに投げる前にそのチャットが翻訳してほしいものなのかを判定してからDeepLに投げる必要がある.
今回は非常に高速に言語解析できるchrome.i18n
のdetectLanguageを用いて言語解析する.詳しい解析方法や実装は適宜調べてもらいたい.
function detectLanguage(targetelm) {
let chat = targetelm.textContent;
if (
(chat.length >= minlength) &&
(chat.length <= maxlength)
) {
chrome.i18n.detectLanguage(chat, function (result) {
let outputLang = result.languages[0].language;
api_translation(targetelm, outputLang);//実際にDeepLで翻訳&置換する関数
});
}
}
-
id
がmessage
の要素のtextContent
がチャットのテキスト -
minlength
,maxlength
はoptions.js
で変更可能な変数- スパムのような長いテキストをここで判定対象外にする
-
result.languages
に判定結果が含まれている- 多くの場合において正確に言語解析されるが,日本語 + 英字のようなチャットだと誤判定する場合がある
- 例えば「煽りwwww」というチャットは,
[{"language": "ne","percentage": 57}, {"language": "ja","percentage": 42} ]
のように解析され,日本語のチャットであるにもかかわらずresult.languages[0].language="ne"
となってしまう
翻訳&翻訳結果に置換
以上によりチャットの言語解析まで行った.あとは,翻訳したい言語に含まれているチャットをDeepL APIを用いて翻訳&翻訳結果に置換していく.
DeepL APIの仕様はDeepL APIにまとまっている.REST APIで非常に使いやすい.
チャットの言語が翻訳したい言語(options.js
で変更可能)に含まれているか判定し,含まれている場合のみDeepL APIに投げてチャットのテキストを翻訳結果に置換する.
function api_translation(elm, outputLang) {
chrome.storage.sync.get(null, function (items) {
if (items.translang.includes(outputLang)) {
let target_chat = elm.textContent;
let target = items.target;
if (typeof target === "undefined") {
target = "JA";
}
let api_url = "https://api.deepl.com/v2/translate";
let params = {
auth_key: deeplpro_apikey,
text: target_chat,
target: target,
};
let data = new URLSearchParams();
Object.keys(params).forEach((key) => data.append(key, params[key]));
fetch(api_url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; utf-8",
},
body: data,
}).then((res) => {
if (res.status == "200") {
res.json().then((resData) => {
elm.textContent = resData.translations[0].text;
});
console.log(
"Original : " +
comment +
"\nTranslation from DeepL API : " +
resData.translations[0].text
);
} else {
elm.textContent =
"This is a sample translation of Chat Translator for DeepL";
switch (res.status) {
case 400:
console.log(
"Chat Translator for DeepL Error : " +
res.status +
"\nBad request. Please check error message and your parameters."
);
break;
default:
console.log("Defautl error");
}
}
});
}
});
}
- まず
chrome.storage.sync.get(null, function (items) {});
にて翻訳候補言語と翻訳先言語の設定をitems
として読み込む -
if (items.translang.includes(outputLang)) {}
で,解析結果言語が翻訳候補言語(=items.translang
)に含まれるか判定 - DeepLにかかわる部分は以下の通りである.
let api_url = "https://api.deepl.com/v2/translate";
let params = {
auth_key: deeplpro_apikey,//DeepL API_KEY
text: target_chat,//翻訳したいチャットのテキスト
target: target,//翻訳先言語(ex.English→日本語ならJA)
};
let data = new URLSearchParams();
Object.keys(params).forEach((key) => data.append(key, params[key]));
fetch(api_url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; utf-8",
},
body: data,
}).then((res) => {
if (res.status == "200") {
res.json().then((resData) => {
elm.textContent = resData.translations[0].text;
});
}
}
- これくらいは読んで理解していただけると思うので説明は省略()
- 実際の実装では
res.status
によるエラー処理も加えてある
GitHub