4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

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

背景

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

これまでは少量だったので地道に手作業でやっていましたが、せっかくなので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を使用しています。

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

ゼロから書くのは大変なのでサンプルプログラムを微調整するだけです。

このサンプルプログラムを実行すると、音声認識されたJSONデータが取得できます。
詳しくは

返却されるJSON (AmiVoice)

キー キー キー 説明
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一覧を参照のこと。
message エラー内容を表す文字列 *2 JSONに含まれるcodeとmessage一覧を参照のこと。

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

JSONを読み込む

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

  • 句読点(、か。)
  • 時間(ミリ秒)
  • 文字数

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

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

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であれば、メディアプールからビデオトラックにドロップするだけでした。

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

これから

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

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?