きっかけ
「メーデー!:航空機事故の真実と真相」 (英題 Air Crash Investigation) のDVDボックスを買いました。
英語版なんですが、字幕ぐらいはあるだろうと思ったら、英語字幕すらありませんでした...
幸いなことにCSS (Content Scramble System) によるアクセスコントロールがかかっていなかったため、なんとか字幕を作れるのではないかと試みたのがきっかけです。
やったこと
- 映像データから音声のみを抽出する (ffmpeg)
- 音声データから文字起こしをする (Amazon Transcribe)
- 文字起こし結果をSubRip字幕データにする (Python)
- 映像データに字幕データを埋め込む (mkvmerge)
必要なツールのインストール
macOSの場合はhomebrewを使ってインストールできます。
Python3とpipが使えることも前提条件です。
brew install ffmpeg mkvtoolnix
pip3 install boto3
1. 映像データから音声のみを抽出する
ffmpegを使うと簡単に音声データのみを含むファイルにコピーできます。
ffmpeg -i original.m4v -acodec copy -vn output.m4a
2. 音声データから文字起こしをする
Amazon Transcribeを使うにはS3に音声データをアップロードする必要があります。
今回は簡単化のため、S3にアップロードし、Amazon Transcribeにジョブを投入するだけの簡単なスクリプトをPythonで書きました。
このあと出てくるコードは動けばいいやで10分ちょっとで書いたコードなので、全体的にかなり雑です...
from boto3 import client, resource
import os
import sys
AWS_ACCESS_KEY = "hogehoge"
AWS_SECRET_ACCESS_KEY = "fugafuga"
BUCKET = "somebucket"
def upload(filepath):
    basename = os.path.basename(filepath)
    s3_client = resource(
        "s3",
        region_name="ap-northeast-1",
        aws_access_key_id=AWS_ACCESS_KEY,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    )
    s3_client.Bucket(BUCKET).upload_file(filepath, basename)
def transcribe(filename):
    url = "s3://{}/{}".format(BUCKET, filename)
    transcribe_client = client(
            "transcribe",
            region_name="ap-northeast-1",
            aws_access_key_id=AWS_ACCESS_KEY,
            aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    )
    response = transcribe_client.start_transcription_job(
            TranscriptionJobName=filename,
            LanguageCode="en-US",
            MediaFormat="mp4",
            Media={
                    "MediaFileUri": url
            },
            OutputBucketName=BUCKET,
    )
def main():
    filepath = sys.argv[1]
    upload(filepath)
    transcribe(os.path.basename(filepath))
if __name__ == "__main__":
    main()
引数に先ほどのファイルを渡して実行します。
python 01-transcribe.py output.m4a
結果のJSONファイルはWebコンソールからダウンロードしてください。 (手抜き)
3. 文字起こし結果をSubRip字幕データにする
結果のJSONには、認識した単語とその開始と終了時間が配列として入っています。
そのまま一単語づつ字幕として表示したのではあまりにも読みづらくなってしまいます。
そこで、一定のルールに従って表示する範囲を決め、字幕データ(srt)を生成することにしました。
- 前の単語から3秒以上空いたら切る
- ピリオド、!、?が来たら文の終わりと判断して切る
- 長くなってきたら切る
- カンマがきたときは短めに切る
- 次の字幕の開始時刻か2秒後かどちらか早い方を表示終了時間にする
このルールは適当に決めたものなので、好きなようにいじってください。
import json
import sys
def sec2time(sec):
    h = int(sec/3600)
    m = int((sec%3600) / 60)
    s = int(sec % 60)
    mils = int((sec%1)*1000)
    return "{:02d}:{:02d}:{:02d},{:03d}".format(h, m, s, mils)
def convert2srt(filepath):
    with open(filepath, "r") as f:
        data = json.load(f)
    start_time = 0
    end_time = 0
    s = ""
    index = 0
    for item in data["results"]["items"]:
        is_output = False
        if "start_time" in item:
            item["start_time"] = float(item["start_time"])
            item["end_time"] = float(item["end_time"])
            if item["start_time"] - end_time > 3:
                # 時間が空いたか
                is_output = True
            elif len(s) >= 110:
                # 長くなっていたら
                is_output = True
            
            if s != "":
                if len(s)>1 and s[-2].isupper():
                    pass
                else:
                    last = s[-1]
                    if last in (".", "?", "!"):
                        is_output = True
                    
                    if last == "," and len(s) > 80:
                        is_output = True
        if is_output:
            end_time = min(item["start_time"], end_time+2.0)
            if s != "":
                print(index)
                index += 1
                print("{0} --> {1}".format(sec2time(start_time), sec2time(end_time)))
                print(s)
                print("")
            start_time = 0
            end_time = 0
            s = ""
        if "start_time" in item:
            if start_time == 0:
                start_time = item["start_time"]
            end_time = item["end_time"]
            if s and (len(item["alternatives"][0]["content"])>1 or s[-1] != "."):
                s += " " + item["alternatives"][0]["content"]
            else:
                s += item["alternatives"][0]["content"]
        else:
            s += item["alternatives"][0]["content"]
    if s != "":
        print(index)
        index += 1
        print("{0} --> {1}".format(sec2time(start_time), sec2time(end_time+2.0)))
        print(s)
        print("")
def main():
    filepath = sys.argv[1]
    convert2srt(filepath)
if __name__ == "__main__":
    main()
引数にJSONファイルを指定し、リダイレクトでテキストファイルに結果を保存します。
python 02-makesrt.py result.json > result.srt
こんな感じの出力になっているはずです
64
00:06:00,139 --> 00:06:16,839
Then the next Nano second, it was pure, unadulterated pandemonium Way number three going down.
65
00:06:16,839 --> 00:06:18,720
It looks like we lost number three engine.
66
00:06:18,720 --> 00:06:23,149
We're descending rapidly coming back.
4. 映像データに字幕データを埋め込む
mkvmergeを使うと簡単にmkvファイルに字幕データを埋め込むことができます。
mkvmerge -o output.mkv original.m4v --language 0:eng --track-name 0:English result.srt
埋め込んだ字幕はVLCなどで再生するときに表示することができます。
結果
まぁまぁほとんど違和感なく表示されてるんじゃないかと思います。
こういうときにささっとツールが作れるのがプログラミングの醍醐味ですね。


