日記

日本語の勉強のためのブログ

YouTubeの字幕を画像に合成するツールの作成

完成したツール: YouTube字幕合成ツール

GitHubリポジトリ: GitHub - kalaxity/youtube_caption_app

1. 目的

字幕として表示したい文章と画像を入力すると、字幕付き画像が出力されるようなプログラムを作成したい。
ただ単に画像の上に字幕を表示するのはcssのpositionでどうにかなる。そうではなく、画像に字幕を合成して、字幕付き画像として保存できるようにしたい。

2. 環境

  • Windows 10 Home
    バージョン: 21H2
  • Vivaldi
    バージョン: 5.0.2497.48 (Stable channel) (64-bit)

3. 理論(YouTubeの字幕のスタイルについて)

例としてこの動画で字幕を表示し、DevToolで該当要素のコードを確認したところ、次のようになっていた。

<span class="ytp-caption-segment" style="display: inline-block; white-space: pre-wrap; background: rgba(8, 8, 8, 0.75); font-size: 16px; color: rgb(255, 255, 255); fill: rgb(255, 255, 255); font-family: &quot;YouTube Noto&quot;, Roboto, &quot;Arial Unicode Ms&quot;, Arial, Helvetica, Verdana, &quot;PT Sans Caption&quot;, sans-serif;">i can certainly tell</span>

スタイルだけを抜き出すと次のようになる。

{
display: inline-block;
white-space: pre-wrap;
background: rgba(8, 8, 8, 0.75);
font-size: 16px;
color: rgb(255, 255, 255); 
fill: rgb(255, 255, 255);
font-family: "YouTube Noto", Roboto, "Arial Unicode Ms", Arial, Helvetica, Verdana, "PT Sans Caption", sans-serif;
}

4. プログラムの作成方針(どのような仕組みを用いて実現するか)

4.1 canvas

画像と字幕をリアルタイムに合成するという目的から、真っ先に思いついたのがcanvasである。当初はその方向で作成していたのだが、制作が進むにつれて多くの問題点が露わになった。
特に大きな問題は、字幕の描画に関するものである。canvasでは文字を描画する関数fillText()が存在し、フォントや色などもある程度は設定できる。しかし、

  • 文字の表示が綺麗でない
    • 画質が荒く、ぼやけたような表示となってしまう
    • 回避方法があるのかもしれないが不明
  • YouTubeの字幕のような白抜き文字を描けない
    • 厳密には、白抜き文字の描画機能がデフォルトで与えられていない
    • 黒四角を描画し、その上に白文字を描画するしかないと思われる
      • その場合、あらかじめ描画する文字の大きさを測定し、それに合うように黒四角のサイズを計算しなければならず、非常に面倒
      • (一応、描画文字列のサイズはmeasureText()で求まるらしい)
    • 他にはshadowを駆使して白抜き文字を再現する方法も考えたが、面倒で没にした

という問題が解決出来そうもなく、canvasを使わない方法を模索することにした。

4.2 html2canvas

そこで目を付けたのがhtml2canvasというライブラリである。 html2canvas - Screenshots with JavaScript

これはWebサイトの指定箇所のスクリーンショットを撮り、それをcanvas形式で返してくれるツールである。 テストを行ったところ、文字列の表示も特段汚くなることはなく、Webページの表示内容とほぼ変わらないクオリティのスクショを得ることができた。

そこで、
画像の上に重ねて字幕を表示(画像合成ではない。<img>で設置した画像の上に<span>で字幕を設置するだけ)し、
それをhtml2canvasでスクショすれば合成画像が得られるのではと思い付き、この方針で作成することにした。

5. プログラムの作成

5.1 画像の選択・読み込み処理

まず字幕を合成する画像を選択してもらう必要がある。この処理については別記事に分割したため、詳しい説明はそちらをご覧いただきたい。

【JavaScript】canvasに入力画像を描画する - 日記

なお、当該記事では入力画像をcanvasに描画しているが、今回はcanvasを使用しないため、入力画像のURLを<img>タグに記載して、単に画像として表示した。
そのため、当該記事よりコードは短く、以下のようになる。

input_image.addEventListener("change", () => {
    // 選択されたファイル (File型)
    let file = input_image.files[0];
    // そのファイルをData URLとして読み込む
    reader.readAsDataURL(file);

    // Fileを読み込み終わった後の動作
    reader.onload = () => {
        // Data URLをimageに渡す(画像として読み込む)
        image.src = reader.result;
    }
});

5.2 字幕の表示処理

次に、テキストボックスに入力された文章を字幕として表示する。特別なことは何もしておらず、単に3.1項のとおりスタイルを指定し、テキストボックスの文章を<span>内に入れただけである。

ただ、入力文字列が2行以上の場合を考える必要がある。
YouTubeの字幕は、複数行の場合、各行に対し黒背景が適用される。しかし、テキストボックスの内容をそのまま字幕として表示してしまうと、全行に対して黒背景が適用されてしまうのである。

意味がわからないと思うので例を示す。以下のような文章が字幕として表示されるとしよう。

こんにちは
おはようございます

YouTubeの字幕では、次のように描画される。

こんにちは
おはようございます

一方、テキストボックスの中身をそのまま字幕として表示すると、以下のようになってしまう。

こんにちは おはようございます

この差異は、字幕1行ごとに<span>タグを用意しているのか(YouTube)、そうでないのか(テキストボックス中身そのまま)が関係している。
そのため、入力文章を改行で区切り、各行に対して<span>要素を用意する処理を書けばよい。

この処理をコードに直すと以下のようになる。

// あらかじめ字幕表示領域の中身を消しておく
caption_area.innerHTML = "";

// テキストボックス内の文章を変数pに入れる
let p = document.getElementById("textarea").value;

// 改行で区切り、各行に対してforEachで処理
p.split("\n").forEach(e => {
    // 新たな<span>要素を作成
    let newCap = document.createElement("span");
    newCap.setAttribute("class", "caption");
    newCap.innerText = e;
    
    // 今作った<span>を字幕表示領域に追加
    caption_area.appendChild(newCap);
    // 複数行あるときのために<br>も追加
    caption_area.appendChild(document.createElement("br"));
});

5.3 合成画像をスクショする処理

スクリーンショットを撮影するには、撮影箇所を指定する必要がある。具体的には撮影したい場所を<div id="capture">で囲み、

html2canvas(document.getElementById("cv")).then((canvas) => {
    // 撮影されたスクショcanvasをここで処理する
});

のようにすればよい。
詳しくは公式サイトに載っているので飛ばす。
Getting Started | html2canvas

5.4 ちょうど画像の部分だけスクショしたい場合

画像とその上の字幕だけスクリーンショットを撮りたい。
スクショしたい箇所を<div id="capture">で囲んだコードが以下のようになっていたとする。

<div id="capture"> 
    <img src="./画像.jpg" id="image">
    <span id="caption_area">字幕文章</span>
</div>

この場合、<div>要素のサイズを<img>要素のサイズと等しくしないと、余白もスクショしてしまう。

そのため、CSSを用いて<div>のサイズを調整する。
ここでは、width: fit-content;と指定することで、<div>内のコンテンツの横幅に合わせている。

/* <div>について */
#capture {
    width: fit-content;
    height: auto;
}

これで横幅は調整できたが、縦幅の調整は完璧でない。画像の高さより微妙に広くなってしまい、スクショを撮ると(小さいながらも)画像下側に余白ができてしまう。
この問題の対処には苦労したが、画像に対してvertical-align: top;と指定することにより解決した。
参考: 【css】画像(img)の下に余白(隙間)ができる問題を秒で解決する方法 | WEBクリエイターの部屋

/* <img>について */
img {
    vertical-align: top;
}

6. 完成

完成したツール: YouTube字幕合成ツール

GitHubリポジトリ: GitHub - kalaxity/youtube_caption_app