Whisperを使ってYouTube字幕ファイルの作成がこれほど簡単だったなんて!

少し前のエントリでYouTubeから音声データをダウンロードするといった内容のことをやっていたのですが、それならその音声データをWhisperに入力し、生成された音声解析のテキストデータをYouTubeの字幕ファイルに変えてみたらどうなるかなと思っていました。Whisperのログ出力はほぼそのままでも字幕ファイルフォーマットであるSRTファイルに近い出力フォーマットになっているからです。

uepon.hatenadiary.com

ただ、そのまま取り出した文字データはタイムコードのないモノになるので、後で処理を行うと面倒なことになります。今回はWhisperを使用し、ログファイルとして出力されたデータをファイル化してYouTubeでも使用可能な字幕データにしてやろうという試みです。

Whisperとは?

Whisperは```OpenAI社のは音声認識機械翻訳のサービスになります。詳細は以下のGitHubを見てもらえればと思います。

github.com

使用方法も結構簡単で

$ pip install git+https://github.com/openai/whisper.git 

を実行することでPythonのライブラリだけでなく、コマンドからも実行することができるようになります。今回はPythonから使用します。先程のGitHubのドキュメントにもあるように、音声ファイルと音声モデルを指定することで音声認識されたデータが出力されます。

GitHubにあるサンプル

import whisper

model = whisper.load_model("base")
result = model.transcribe("audio.mp3")
print(result["text"])

このプログラムではbaseという認識モデルを使用して、audio.mp3を解析しテキストファイルを作成しています。このプログラムのアプトプットでは解析時はタイムコードとともに解析したテキストのログと、解析後のテキスト全文を出力しています。コードとしてprint(result["text"])となっている部分がテキストの全文となります。せっかく出力されているログの部分はコードとしては現れず標準出力(stdout)として画面表示されています。。本当に使いたいのはこの標準出力の部分なのです。

ということで、Pythonのコードで標準エラーの出力をファイルを取得し、そのデータを変換して字幕ファイルに変換します。

YouTubeで対応している字幕フォーマットがいくつかあります。


YouTubeに対応した字幕ファイルフォーマット

SubRip(.srt)形式

これは非常に一般的な字幕フォーマットで、プレーンテキストファイルです。各字幕エントリには、開始および終了時間と字幕テキストが含まれています。

srtファイルのサンプル

1
00:00:02,000 --> 00:00:04,000
こんにちは、皆さん

2
00:00:05,000 --> 00:00:07,000
今日は天気が良いです

SubViewer(.sub)形式

SubViewerフォーマットもプレーンテキストフォーマットで、各エントリには開始および終了時間、および字幕テキストが含まれています。

subファイルのサンプル

00:00:02,00 00:00:04,00
こんにちは、皆さん

00:00:05,00 00:00:07,00
今日は天気が良いです

SBV (YouTube)(.sbv)形式

これはYouTube固有のフォーマットで、開始および終了時間、および字幕テキストを含むエントリがあります。上記との違いはタイムコードの桁や小数点の表記だけのように感じます。

sbvファイルのサンプル

0:00:02.000,0:00:04.000
こんにちは、皆さん

0:00:05.000,0:00:07.000
今日は天気が良いです

今回使用する字幕ファイルのフォーマット

この3つから出力形式を選ぶのですが、srtファイルはファイル内にシーケンス番号があるので、あとから編集する可能性を考えるとちょっと面倒です(ちなみにシーケンス番号が飛び飛びになっていても問題はないようですが)。

ということで、今回はSubViewerフォーマットへ変換してみることにします。

作成方針

今回は2つの処理プログラムを組み合わせて処理を行っていきます。

  1. 音声ファイルからWhisperで音声解析を行ってテキストファイルを出力する
  2. 音声解析したテキストデータをSubViewerフォーマットに変換する

あとはこの2つの処理をシェルスクリプトとして実行するようにまとめます。

音声ファイルからWhisperで音声解析を行ってテキストファイルを出力

このプログラムは引数に指定された音声ファイルをもとに解析結果のログを出力します。

create_audio_to_whisperlog.py

import sys
import argparse
import whisper

def process_audio_to_text(input_audio_file, output_text_file):
    # Load the Whisper model
    model = whisper.load_model("small") # Options: tiny, base, small, medium, large

    original_stdout = sys.stdout
    # Save the result to the output text file
    with open(output_text_file, 'w', encoding='utf-8') as f:
        sys.stdout = f
        # Transcribe the audio file to text
        result = model.transcribe(input_audio_file, fp16=False, verbose=True, language='ja')    
    sys.stdout = original_stdout

def main():
    # Create a parser for parsing command-line arguments
    parser = argparse.ArgumentParser(description='Transcribe an audio file to text.')
    parser.add_argument('input_audio_file', type=str, help='Path to the input audio file.')
    parser.add_argument('output_text_file', type=str, help='Path to the output text file.')

    # Parse the arguments
    args = parser.parse_args()
    
    # Call the function to process the audio file and transcribe it to text
    process_audio_to_text(args.input_audio_file, args.output_text_file)

if __name__ == '__main__':
    main()

上記のプログラムのポイントは標準出力であるstdoutの内容をファイルに保存している点と model.transcribe()の引数にverbose=Trueを明示的に追記している点です、。この関数の引数は指定しなければ、verbose=Trueとなり解析結果を標準出力に出力してくれますが、Falseを設定していると何も出力されないので注意してください。

また、今回はWhisperの音声解析モデルをsmallを使用しています。モデルにはサイズ・解析の品質レベルによってtinybasesmallmediumlargeがあります。tinyが一番小さく品質が低く、largeが一番大きく品質が高くなります。品質の高いモデルは解析の時間が必要となりますので、どれがいいかは使ってみて比較が必要かなと思います。

ポイントの箇所を取り出してみました

    original_stdout = sys.stdout # ※標準出力をいったん保存
    # Save the result to the output text file
    with open(output_text_file, 'w', encoding='utf-8') as f:
        sys.stdout = f  # ※標準出力をファイルに保存する指定
        # Transcribe the audio file to text
        result = model.transcribe(input_audio_file, fp16=False, verbose=True, language='ja')    
    sys.stdout = original_stdout  # ※標準出力をもとに戻す

音声解析したテキストデータをSubViewerフォーマットに変換

このプログラムは引数に指定されたWhisperの解析結果ををもとに字幕ファイルを生成します。 タイムコードも表示されているテキストデータの変換なのでそれほど難しい処理ではありません。

transform_whisperlog_to_subfile.py

import argparse

def transform_multiple_lines(input_text):
    entries = input_text.strip().split("\n")
    output_entries = [transform_subtitle(entry) for entry in entries]
    return "\n\n".join(output_entries)

def transform_subtitle(input_text):
    lines = input_text.strip().split("\n")
    output_lines = []
    for line in lines:
        time_codes, text = line.split("] ")
        start_time, end_time = time_codes[1:].split(" --> ")
        output_time = f"00:{start_time},00:{end_time}"
        output_lines.extend([output_time, text])
    return "\n".join(output_lines)

def main():
    parser = argparse.ArgumentParser(description="Transform subtitle format")
    parser.add_argument("input_file", help="Path to the input file")
    parser.add_argument("output_file", help="Path to the output file")
    args = parser.parse_args()

    with open(args.input_file, "r", encoding="utf-8") as infile:
        input_text = infile.read()

    transformed_text = transform_multiple_lines(input_text)

    with open(args.output_file, "w", encoding="utf-8") as outfile:
        outfile.write(transformed_text)

if __name__ == "__main__":
    main()

変換を行うシェルスクリプト

上記2つのpythonプログラムをもとに、実行のシェルスクリプト(入力ファイルをaudio.mp3として、中間ファイルをwhisper.logを生成し、出力する字幕ファイルをoutput.subとしています)を作成します。スクリプト内ではpythonのプログラムを&&でつないでいますが、これは1つ目のプログラムが正常終了する場合にのみ2つ目のプログラムが実行されるというような動作をします。

audio2sub.sh

#!/bin/bash
python3 ./create_audio_to_whisperlog.py audio.mp3 whisper.log \
&& python3 ./transform_whisperlog_to_subfile.py whisper.log output.sub

【参考】

qiita.com

このスクリプトを作成後に実行権限を付与します。

chmod 755 audio2sub.sh

実行!

実行は以下のように行います。 (注意)入力する音声ファイルaudio.mp3スクリプトファイルと同じパスにある必要があります。

$ ./audio2sub.sh

実行すると中間ファイルとして音声解析を行った中間ファイルwhisper.logが生成されます。 今回は天気予報を読み上げの音声データを使ってみました。

whisper.logの内容

$ cat whisper.log 
[00:00.000 --> 00:04.000] 105日木曜日の全国の天気をお伝えします。
[00:04.000 --> 00:07.000] まずは天気図から見ていきましょう。
[00:07.000 --> 00:12.000] 定期圧や全線の通過に伴い、北日本や北陸では雨・風が強まります。
[00:12.000 --> 00:14.000] 荒れた天気に警戒が必要です。

中間ファイルの生成が成功すると字幕ファイルoutput.subが生成されます。

出力される字幕ファイルoutput.subの内容

$ cat output.sub 
00:00:00.000,00:00:04.000
105日木曜日の全国の天気をお伝えします。

00:00:04.000,00:00:07.000
まずは天気図から見ていきましょう。

00:00:07.000,00:00:12.000
定期圧や全線の通過に伴い、北日本や北陸では雨・風が強まります。

00:00:12.000,00:00:14.000
荒れた天気に警戒が必要です。

YouTubeに字幕ファイルを取り込む

字幕ファイルができれば、あとは字幕ファイルの取り込みになります。

YouTubeのファイルの管理画面(YouTube Studio)から字幕ファイルのアップロードができます(公開後でも字幕を付けることができます)。 画面を開いて画面の右下にある【字幕】ボタンをクリックします。

ボタン部分の拡大

字幕追加のダイアログが開き、【ファイルをアップロード】をクリックすると・・・

クリックするとタイムコードありか、タイムコードなしか選択ダイアログが表示されます。今回のSubViewerフォーマットの字幕ファイルにはタイムコードが含まれているので【タイムコードあり】を選択して、【続行】をクリックします。

すると、字幕がタイムコード順にならんでプレビューを行うことができます。データの編集もこの画面で行うこともできるので便利ですね。

あとは【完了】ボタンをクリックすればOKです。

おわりに

音声ファイルからWhisperを使用して、YouTubeの字幕ファイルを生成し、動画につけるということをやってみました。Whisperの入力のできるの音声ファイルの容量は25MByteではありますが、音声ファイル自体で25MByteはかなり長時間になると思いますので、ほぼ大丈夫なのではないかと思います。超えてしまう場合には音声の品質などを変えるとかチャンネルを減らすとかも工夫はできそうです。

かなりの品質で字幕をつくることができるので非常にいいですよね~ということで終わりたいと思います。

ちなみに、Whisperのサンプルコードをpipeでフィルタプログラムに通せばもっと楽に実装できます😁

/* -----codeの行番号----- */