Amazon Transcribeで作る字幕データ

# きっかけ

「メーデー！：航空機事故の真実と真相」 (英題 Air Crash Investigation) のDVDボックスを買いました。
英語版なんですが、字幕ぐらいはあるだろうと思ったら、英語字幕すらありませんでした...

幸いなことにCSS (Content Scramble System) によるアクセスコントロールがかかっていなかったため、なんとか字幕を作れるのではないかと試みたのがきっかけです。

# やったこと

1. 映像データから音声のみを抽出する (ffmpeg)
2. 音声データから文字起こしをする (Amazon Transcribe)
3. 文字起こし結果をSubRip字幕データにする (Python)
4. 映像データに字幕データを埋め込む (mkvmerge)

# 必要なツールのインストール

macOSの場合はhomebrewを使ってインストールできます。
Python3とpipが使えることも前提条件です。

```bash
brew install ffmpeg mkvtoolnix
pip3 install boto3
```

# 1. 映像データから音声のみを抽出する

ffmpegを使うと簡単に音声データのみを含むファイルにコピーできます。

```bash
ffmpeg -i original.m4v -acodec copy -vn output.m4a
```

# 2. 音声データから文字起こしをする

Amazon Transcribeを使うにはS3に音声データをアップロードする必要があります。
今回は簡単化のため、S3にアップロードし、Amazon Transcribeにジョブを投入するだけの簡単なスクリプトをPythonで書きました。

このあと出てくるコードは動けばいいやで10分ちょっとで書いたコードなので、全体的にかなり雑です...

```python:01-transcribe.py
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()
```

引数に先ほどのファイルを渡して実行します。

```bash
python 01-transcribe.py output.m4a
```

結果のJSONファイルはWebコンソールからダウンロードしてください。 (手抜き)

# 3. 文字起こし結果をSubRip字幕データにする

結果のJSONには、認識した単語とその開始と終了時間が配列として入っています。
そのまま一単語づつ字幕として表示したのではあまりにも読みづらくなってしまいます。

そこで、一定のルールに従って表示する範囲を決め、字幕データ(srt)を生成することにしました。

* 前の単語から3秒以上空いたら切る
* ピリオド、！、?が来たら文の終わりと判断して切る
* 長くなってきたら切る
* カンマがきたときは短めに切る
* 次の字幕の開始時刻か2秒後かどちらか早い方を表示終了時間にする

このルールは適当に決めたものなので、好きなようにいじってください。

```python:02-makesrt.py
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
```

こんな感じの出力になっているはずです

```plaintext:output
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ファイルに字幕データを埋め込むことができます。

```bash
mkvmerge -o output.mkv original.m4v --language 0:eng --track-name 0:English result.srt
```

埋め込んだ字幕はVLCなどで再生するときに表示することができます。

# 結果

まぁまぁほとんど違和感なく表示されてるんじゃないかと思います。

![vlcsnap-2020-03-03-00h47m23s630.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73788/7f796b16-3dd3-d9b3-1f3b-be2418c352c2.png)

![vlcsnap-2020-03-03-00h50m06s662.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73788/5af5ba30-a2f5-0362-fb01-640be0c46386.png)

こういうときにささっとツールが作れるのがプログラミングの醍醐味ですね。