Amazon Transcribeとは
音声ファイルから自動的に文字を起こしてくれるAmazonのサービスです。
https://aws.amazon.com/jp/transcribe/
類似のものに、Google CloudのSpeech-to-Textがあります。
https://cloud.google.com/speech-to-text?hl=ja
今回説明しないもの
・AWSの設定の詳細
・S3へのアップロード、ダウンロードの詳細
・動画や音声の詳細
準備
AWS
・Cognito
iOSアプリからAWSにアクセスするためのやつ。
・S3
Transcribeに使う音声ファイルやjobファイルを置くストレージ。
・Transcribe
本丸。Create Jobから特に困ることなく設定可能です。
iOS
・AWS SDK for iOS
AWSS3とAWSTranscribeだけあれば大丈夫です。(AWSCoreもついてくるので。)
おおまかな流れ
iOSから音声(動画)ファイルをS3にアップロード
-> iOSからTranscribeのjobを実行命令
-> AWSのTranscribeがjobを実行し、先ほどアップロードしたS3のファイルを文字化
-> Transcribeの結果取得
-> 変換結果のjsonファイルをS3からダウンロード
実装
初期設定
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// AWS Cognito & S3 & Transcribe registration
let credentialProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId: "ap-northeast-1:*******************")
if let configuration = AWSServiceConfiguration(region:.APNortheast1, credentialsProvider:credentialProvider) {
AWSS3TransferUtility.register(with:configuration!, forKey: "MY_S3")
AWSTranscribe.register(with:configuration!, forKey: "MY_Transcribe")
}
}
SceneDelegate(AppDelegate)のおなじみの起動ファンクションでAWS初期設定します。
Cognitoでregionと設定の際に発行されたIdentity Pool IDを設定します。
設定後、Transcribe、 AWSS3TransferUtilityへの登録も行っておきます。
S3へ該当ファイルをアップロード
let bucketName = "mybucket"
let mp4URL = URL(fileURLWithPath: "your/video.mp4")
if let awss3 = AWSS3TransferUtility.s3TransferUtility(forKey: "MY_S3") {
do {
let videoData = try Data(contentsOf: mp4URL)
let videoName = "s3video.mp4"
awss3.uploadData(
videoData,
bucket: bucketName,
key: videoName,
contentType: "mp4",
expression: nil, // 途中経過task nullable
completionHandler: { task, error in
if let error = error {
print("s3 upload error \(error)")
} else {
print("success upload")
}
} catch let error {
print("data convert error \(error)")
}
}
今回はmp4ファイルで行います。Transcribeがフォローしているファイルタイプは、flac, mp3, mp4, wavの4タイプです。(2020/2/20現在)
録画なりして端末に入ってるmp4ファイルをData型で取得し、AWSS3TransferUtility.uploadDataでアップロードします。
ここでのbucketはCognitoで設定したものと同じでなくてはなりません。
expressionはアップロードの途中経過を取得できるtaskですが今回は省きます。
また、uploadData自体もTaskとして登録できるものもあるので使い分けてください。
TranscribeのJobを実行
let awstrans = AWSTranscribe(forKey: "MY_Transcribe")
let jobName = "MY_JOB"
if let startRequest = AWSTranscribeStartTranscriptionJobRequest() {
startRequest.languageCode = .jaJP // language code結構いっぱいある
let media = AWSTranscribeMedia()
media?.mediaFileUri = "https://s3-ap-northeast-1.amazonaws.com/\(bucketName)/\(videoName)"
startRequest.media = media // 先ほどアップロードしたs3のファイルのurlを指定する
startRequest.mediaFormat = .mp4 // flac, mp3, mp4, wav
startRequest.mediaSampleRateHertz = 44100 // いらないかも
startRequest.transcriptionJobName = jobName
startRequest.outputBucketName = bucketName
// Job実行
awstrans.startTranscriptionJob(startRequest, completionHandler: {response, error in
if let error = error {
print("start job error \(error)")
} else {
print("success start job")
}
}
}
AWSTranscribeStartTranscriptionJobRequestでJobのステータスを設定します。
ここで勘違いしていたのが、startTranscriptionJobのcompletionがJob完了時に呼ばれるものだと思ってましたが、これはあくまでJobがスタートした時に呼ばれるものでした。
TranscribeのJobが完了するまで待って取得
// timer使うので
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { timer in
if let getJobRequest = AWSTranscribeGetTranscriptionJobRequest() {
getJobRequest.transcriptionJobName = projectId
awstrans.getTranscriptionJob(getJobRequest, completionHandler: {response, error in
if let error = error {
print("get job error \(error)")
self.timer.invalidate()
}
if let reason = response?.transcriptionJob?.failureReason {
print("job failed \(reason)")
}
if response?.transcriptionJob?.transcriptionJobStatus == .completed {
// 完了後、awsにアップロードされた、結果の記載されたjsonのuriが取得できる
print(response?.transcriptionJob?.transcript?.transcriptFileUri)
self.timer.invalidate()
}
})
}
})
}
探してみたところ、Jobの完了通知をしてくれるものは見当たらなかったので、取り急ぎTimerで完了するまでgetし続けるという原始的なことをしました。しかも、mp4ファイルだと3MBぐらいのサイズでも40秒とかかかったので、interval=10としました。
JobStatus=completeとなった段階で、transcriptionJobにいろいろな値がセットされて返却されるので、結果の記載されたjsonのURIを取得する。
余談ですが、ハンドラ内でTimerを実行する際は実行スレッドに注意。
S3からTranscribeの結果jsonを取得
awss3.downloadData(
fromBucket: bucketName,
key: projectId + ".json",
expression: nil,
completionHandler:{task, location, data, error in
if let error = error {
print("s3 download error \(error)")
} else {
if let data = data {
do {
let jsonDecoder = JSONDecoder()
let transcribeData = try jsonDecoder.decode(AmazonTranscribe.self, from: data)
print(transcribeData.results.transcripts)
} catch let error {
print("decode error \(error)")
}
}
}
}
)
struct AmazonTranscribe: Decodable {
let jobName:String
let accountId:String
let results: AmazonTranscribeResults
let status: String
}
struct AmazonTranscribeResults: Decodable {
let transcripts: [AmazonTranscribeTranscripts]
let items: [AmazonTranscribeItem]
}
struct AmazonTranscribeTranscripts: Decodable {
let transcript: String // 全文
}
struct AmazonTranscribeItem: Decodable {
let startTime: Double
let endTime: Double
let alternatives: [AmazonTranscribeAlternatives]
let type: String
private enum CodingKeys: String, CodingKey {
case startTime = "start_time"
case endTime = "end_time"
case alternatives, type
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard let startTimeDouble = Double(try values.decode(String.self, forKey: .startTime)) else {
fatalError("The start time is not an Double")
}
guard let endTimeDouble = Double(try values.decode(String.self, forKey: .endTime)) else {
fatalError("The end time is not an Double")
}
startTime = startTimeDouble
endTime = endTimeDouble
alternatives = try values.decode([AmazonTranscribeAlternatives].self, forKey: .alternatives)
type = try values.decode(String.self, forKey: .type)
}
}
struct AmazonTranscribeAlternatives: Decodable {
let confidence: Double
let content: String
private enum CodingKeys: String, CodingKey {
case confidence, content
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard let confidenceDouble = Double(try values.decode(String.self, forKey: .confidence)) else {
fatalError("The confidence is not an Double")
}
confidence = confidenceDouble
content = try values.decode(String.self, forKey: .content)
}
}
おまけでjsonをDecodeするのにつかったDecodableも載せておきます。返却値はこんな(AmazonTranscribe.swift)感じです。
各値の意味は深く調べてませんが、全文(なぜリスト?)と代替候補っぽいのはありました。
感想
正直なところ、日本語の文字起こし精度はさほど高くないように感じられました。
ファイルの形式等を調整したらもうちょい精度あがりそうですが、
試しにGoogleHomeとの、「ねぇGoogle、明日の天気は?」「明日の新宿は最高気温15度、最低気温7度で晴れるでしょう」というやりとり動画を送信してみたら、
「めぐる 明日 の 天気 は 明日 の 新宿 は 最高 気温 十 五 度 再 激 音 など で 買える でしょ」
というアウトプットがきました。この場合の使える情報は、明日の新宿が最高気温15度ってことだけでしょう。
英語はしゃべれないし、発音も悪いので試してません。