YouTubeLiveのチャットを言語解析してDeepLによる翻訳結果に置換するChrome拡張機能を作った

YouTubeLiveにてVTuberの動画や生放送を眺めていると，母国語以外のチャットが流れることもありそれらのチャットをリアルタイムに拾うことができていない配信者が見受けられます．
確かにいちいち翻訳ツールにかけるのは面倒ですが，この状況は非常にもったいないと感じます．
そこで比較的自然な翻訳をしてくれる[DeepL](https://www.deepl.com/)を用いて自動的にチャットを翻訳するツールがあれば便利なのでは?と思い作ってみました．

# Summary of technical topics
Monitor the chat field of YouTube Live with [MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver), determine the language with [chrome.i18n.detectLanguage()](https://developer.chrome.com/docs/extensions/reference/i18n/#method-detectLanguage), translate with [DeepL API](https://www.deepl.com/docs-api/translating-text/), and replace `textContent` with translation result.

※ 技術的な話題は **[ココ](https://qiita.com/teahat/items/052a91d69b63b6d7de0c#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A9%B1)** から始まります

# 先行事例と課題
**[YouTube LiveのチャットをDeepL翻訳してリアルタイム表示する](https://qiita.com/KawaiKatori/items/7bf4e1d34d71275f9362)**はPythonのtkinterを用いたGUIアプリケーションであり，[YouTube data API](https://developers.google.com/youtube/v3/getting-started?hl=ja)でチャットを取得している．
しかし...

- 実行ファイルは提供されていないのでPython環境を構築する必要があり，環境構築が壁になる(実際，利用しようとして苦戦している方をTwitterで見かけた)

- すべてのチャットを翻訳してしまうので [DeepL API](https://www.deepl.com/pro#developer)の使用料が気になる
- 以前[YouTubeLiveのチャットをniconico風にオーバーレイする【Python】](https://qiita.com/teahat/items/c44aca37223c54b0cf19#api%E4%B8%8A%E9%99%90%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)を作ったときYouTube data APIの利用上限が厳しいと感じたので，できればYouTube data API以外の方法でチャットを取得したい
- Liveアーカイブでは利用できない

# 作ったもの
Google Chrome Web Storeにて無料公開されています．
拡張機能をインストールできる人であれば (DeepL APIを契約次第) だれでも簡単に利用開始できます．

[Chat Translator for DeepL | Chrome 拡張機能](https://chrome.google.com/webstore/detail/chat-translator-for-deepl/fbmkdedikbcbkkeaofpenfncppepfnlo)

https://github.com/T3aHat/Chat_Translator_for_DeepL

# できること
- YouTubeLiveのチャットの言語を解析し，翻訳したい言語であれば[DeepL API](https://www.deepl.com/pro#developer)を用いて翻訳結果に自動的に書き換える
- [Chat v2.0 Style Generator](http://css4obs.starfree.jp/)のようなカスタムCSSを適用可能

<iframe width="100%" height="460" src="https://www.youtube.com/embed/8nhYxoAYdio" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# 推しポイント

- 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](https://chrome.google.com/webstore/detail/chat-translator-for-deepl/fbmkdedikbcbkkeaofpenfncppepfnlo)は様々な機能を持っているが，ここではチャット欄に関連する内容のみを扱う．

## 拡張機能概要
| ファイル名 | 備考 |
|:-----------|-------------|
| manifest.json       | 拡張機能に用いるファイル類を定義 |
| chat-translator.js | 言語解析，翻訳，翻訳結果への置換 |
| options.js, options.html | 各種設定変更 |

## manifest.json

```json: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,"`．

```json
  "content_scripts": [
    {"run_at":"document_end",
      "matches":["https://www.youtube.com/live_chat*"],
      "all_frames":true,
      "js":["chat-translator.js"]}
  ]
```
[Content scripts - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/content_scripts/)によると，
> 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](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)でチャット欄全体を監視することで実現できる．
具体的には以下のとおりである．

```javascript:chat-translator.js
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](https://developer.chrome.com/docs/extensions/reference/i18n/#method-detectLanguage)を用いて言語解析する．詳しい解析方法や実装は[適宜調べてもらいたい](https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/API/i18n/detectLanguage)．

```javascript:chat-translator.js
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](https://www.deepl.com/docs-api/translating-text/)にまとまっている．REST APIで非常に使いやすい．

チャットの言語が翻訳したい言語(`options.js`で変更可能)に含まれているか判定し，含まれている場合のみDeepL APIに投げてチャットのテキストを翻訳結果に置換する．

```javascript:chat-translator.js
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にかかわる部分は以下の通りである．

```javascript:chat-translator.js
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
https://github.com/T3aHat/Chat_Translator_for_DeepL