LoginSignup
0
1

More than 3 years have passed since last update.

Amazon TranscribeにSwiftアプリから連携してみた

Last updated at Posted at 2020-02-22

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
AWSS3AWSTranscribeだけあれば大丈夫です。(AWSCoreもついてくるので。)

おおまかな流れ

iOSから音声(動画)ファイルをS3にアップロード
-> iOSからTranscribeのjobを実行命令
-> AWSのTranscribeがjobを実行し、先ほどアップロードしたS3のファイルを文字化
-> Transcribeの結果取得
-> 変換結果のjsonファイルをS3からダウンロード

実装

初期設定

SceneDelegate.swift
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)")
                }
            }
        }
    }
)
AmazonTranscribe.swift
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度ってことだけでしょう。
英語はしゃべれないし、発音も悪いので試してません。

0
1
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
0
1