AmiVoiceのJSONデータから字幕ファイル（SRT）をつくってみた

## はじめに

はじめて投稿します。
ゴールデンブリッジの電氣羊と申します。
本業は中国語の翻訳なのですが、趣味でプログラミングにもいそしんでいます。

言語関連で色々試していますが、やはり日曜プログラマー。
専門の人にどんどん叩いていってもらいたいです！

## 背景

このところコロナ禍の影響で通訳はオンライン特需。
通信が不安定になってもいいように、あらかじめビデオ収録形式で行うことも少なくありません。
そうなると翻訳だけでなく **字幕** を扱うことが増えてきました。

これまでは少量だったので地道に手作業でやっていましたが、せっかくなのでSRTファイルを作って省力化してしまおうと思い立ちました。

## SRTファイルについて

字幕をビデオに貼り付けるための、デファクトスタンダードともいえる形式。
中身は非常にシンプルなつくりになっています。

```
1
00:00:01,050 --> 00:00:05,778
最初の字幕

2
00:00:07,850 --> 00:00:11,123
次の字幕

3
00:01:02,566 --> 00:01:12,456
三番目の字幕
```

のように、
　*インデックス*
　*開始時間 --> 終了時間*
　*本文*
　*（改行）*
の4つの要素が並んでできています。

このファイルを使えば、字幕を一気に動画ファイルにインポートできるのです。

## 文字起こしをする

弊社では日本語の文字起こしの下書きに、[AmiVoice Cloud Platform](https://acp.amivoice.com/main/)を使用しています。

日本で作られたものなので、日本語に限ればやはりGoogleなどよりも精度が高いですね。

ゼロから書くのは大変なので[サンプルプログラム](https://acp.amivoice.com/main/manual/%e3%82%b5%e3%83%b3%e3%83%97%e3%83%ab%e3%83%97%e3%83%ad%e3%82%b0%e3%83%a9%e3%83%a0/)を微調整するだけです。

このサンプルプログラムを実行すると、音声認識されたJSONデータが取得できます。
詳しくは
>[返却されるJSON (AmiVoice)](https://acp.amivoice.com/main/manual/i-f%E4%BB%95%E6%A7%98-http%E9%9F%B3%E5%A3%B0%E8%AA%8D%E8%AD%98api%E8%A9%B3%E7%B4%B0/#response)

| キー        | キー       | キー       | 説明                                                         |
| ----------- | ---------- | ---------- | ------------------------------------------------------------ |
| results     |            |            | 「発話区間の認識結果」の配列                                 |
|             | confidence |            | 信頼度(0～1の値。 0:信頼度低, 1:信頼度高)                    |
|             | starttime  |            | 発話開始時間 （音声データの先頭が0）                         |
|             | endtime    |            | 発話終了時間 （音声データの先頭が0）                         |
|             | tags       |            | 未使用（空配列）                                             |
|             | rulename   |            | 未使用（空文字）                                             |
|             | text       |            | 認識結果テキスト                                             |
|             | tokens     |            | 認識結果テキストの形態素の配列                               |
|             |            | written    | 形態素（単語）の表記                                         |
|             |            | confidence | 形態素の信頼度（認識結果の尤度）                             |
|             |            | starttime  | 形態素の開始時間 （音声データの先頭が0）                     |
|             |            | endtime    | 形態素の終了時間（音声データの先頭が0）                      |
|             |            | spoken     | 形態素の読み                                                 |
| utteranceid |            |            | 認識結果情報ID  *1                                           |
| text        |            |            | 「発話区間の認識結果」の全てを結合した全体の認識結果テキスト |
| code        |            |            | 結果を表す1文字のコード *2 [JSONに含まれるcodeとmessage一覧](https://acp.amivoice.com/main/manual/i-f仕様-http音声認識api詳細/#rescode)を参照のこと。 |
| message     |            |            | エラー内容を表す文字列 *2 [JSONに含まれるcodeとmessage一覧](https://acp.amivoice.com/main/manual/i-f仕様-http音声認識api詳細/#rescode)を参照のこと。 |

このJSONデータからtokensにあるstarttime、endtime、writtenを使ってSRT形式に整えていきます。

## JSONを読み込む

JSONが取得できたら早速変換を始めていきます。
字幕のブロックを区切る条件として

- 句読点（、か。）
- 時間（ミリ秒）
- 文字数

あたりを使います。
また、字幕には句読点を付けないので、それらをスキップします。

こういった要素はコマンドライン引数で、柔軟に変えられるようにしておきました。

```python
import argparse
import json

parser = argparse.ArgumentParser()
parser.add_argument("file", help="Designate JSON file name to read")
parser.add_argument("-d", "--delimiters", help="Designate delimiters to separate subtitles. Default value is ['。','、']", default="。,、")
parser.add_argument("-s", "--skip", help="Designate skip words which do not inculud in subtitles. Default value is ['。','、']", default="。,、")
parser.add_argument("-t", "--time", help="Designate allowed time for single subtile by millisecongds. Default value is 5000", default=5000, type=int)
parser.add_argument("-c", "--charas", help="Designate allowed charas for single subtile. Default value is 25", default=25, type=int)

class SRTFomart():
	def __init__(self, args): 
		self.text = ""
		self.blocks = []
		self.delimiters = args.delimiters.split(",")
		self.skipWords = args.skip.split(",")
		self.time = args.time
		self.charas = args.charas

	def readFile(self, file):
		f = open(file, "r", encoding="utf-8")
		contents = f.read()
		f.close()
		data = json.loads(contents)["results"][0]
		self.text = data["text"]
		self.readTokens(data["tokens"])

	def readTokens(self, tokens):
		sub = ""
		startTime = 0
		index = 1
		# subTitles = []
		
		for token in tokens:
			written = token["written"]
			# 字幕が空の場合 startTime を設定する
			if sub == "":
				# 字幕が空でもTokenの中身が句読点などであった場合はスキップ
				if written in self.delimiters or written in self.skipWords:
					continue

				else:
					startTime = token["starttime"]

			# 字幕区切りをつくっていく
			# 各条件で字幕を blocks に格納して一度リセットする
			# 句読点にあたったら
			if written in self.delimiters or len(sub) > self.charas or token["endtime"] - startTime > self.time:
				self.blocks.append(self.createSRTBlock(index, startTime, token["endtime"], sub))
				sub = ""
				startTime = 0
				index += 1

			# 条件以外では字幕をつなげていく
			else:
				if written not in self.skipWords:
					sub += token["written"]
		
		# Forループここまで
		# 最後のブロックを格納する
		self.blocks.append(self.createSRTBlock(index, startTime, tokens[-1]["endtime"], sub))


	def createSRTBlock(self, index, startTime, endTime, sub):
		stime = self.timeFormat(startTime)
		etime = self.timeFormat(endTime)
		return f"{index}\n{stime} --> {etime}\n{sub}\n"

	def timeFormat(self, time):
		time_ = time
		ms_ = int(time_ % 1000)
		time_ = int((time_ - ms_) / 1000)
		sec_ = int(time_ % 60)
		time_ = int((time_ - sec_) / 60)
		mn_ = int(time_ % 60)
		time_ = int((time_ - mn_) /60)
		hr_ = int(time_ % 60)
		if ms_ < 10:
			ms = f"00{ms_}"
		elif ms_ < 100:
			ms = f"0{ms_}"
		else:
			ms = str(ms_)
				
		if sec_ < 10:
			sec = f"0{sec_}"
		else:
			sec = str(sec_)
				
		if mn_ < 10:
			mn = f"0{mn_}"
		else:
			mn = str(mn_)
				
		if hr_ < 10:
			hr = f"0{hr_}"
		else:
			hr = str(hr_)
	
		return f"{hr}:{mn}:{sec},{ms}"

	def exportSRTText(self):
		return "\n".join(self.blocks)


if __name__ == "__main__":
	args = parser.parse_args()
	if not args.file.endswith(".json"):
		print("Please set json file")
	
	else:
		srt = SRTFomart(args)
		srt.readFile(args.file)
		text = srt.exportSRTText()
		srtName = args.file.replace(".json", ".srt")
		f = open(srtName, "w", encoding="utf-8")
		f.write(text)
		f.close()
		print("done")


```

これで無事、SRT形式に変換することができました。

## インポート

最後に、生成されたSRTファイルを調整したり、翻訳したりして、動画編集ソフトにインポートするだけです。
[Davinci Resolve](https://www.blackmagicdesign.com/jp/products/davinciresolve/)であれば、メディアプールからビデオトラックにドロップするだけでした。

これまでの手作業から、かなりの効率化が期待できそうです！

## これから

- 機械翻訳と接続もしたい！
- Wordなどで日本語校正 → 再取り込み をする方法を模索中。

ニューノーマル時代の字幕作成に幸あれ！