Tech Blog
  • HOME
  • Blog
  • Apple WatchでAmiVoiceの非同期APIを試してみた

Apple WatchでAmiVoiceの非同期APIを試してみた

公開日:2022.01.24 最終更新日:2024.02.21

hayashi 林政樹

こんにちは。林です。
株式会社アドバンスト・メディアにて、iOS/WatchOSアプリの開発を担当しています。

はじめに

先日、弊社のAmiVocie Cloud Platform(以下、ACP)から非同期HTTP音声認識APIの利用が開始されました。これにより、ACPで利用できる音声認識APIは、以下の3つとなりました。

・同期HTTP音声認識API

・非同期HTTP音声認識API

・WebSocket音声認識API

※各プロトコルの詳細については、下記の紹介記事をご参照ください。

AmiVoice Cloud Platformの3種類の音声認識API(非同期HTTP音声認識APIが新たに追加されました)


AmiVoice Cloud Platform-Tech Blog

前回は、ACPのWebSocket音声認識APIを利用して、リアルタイムで音声の文字化が可能なWatchOSアプリの開発に挑戦しました。

WebSocket + AVAudioEngineを駆使してApple Watchでリアルタイム音声認識してみた


AmiVoice Cloud Platform-Tech Blog

そこで今回は、新たに追加された非同期HTTP音声認識APIを利用して、

・ボイスメモのように音声を記録する

・記録した音声が非同期で文字化される

の2点の機能を持つWatchOSアプリを開発したいと思います。

興味のある方は、ぜひご一読お願いします。

アプリ

今回開発したアプリは、下図のように画面遷移します。

f:id:amivoice_techblog:20211014180055p:plain

このアプリでは音声を録音し保存すると、記録画面には「…」の吹き出しが表示されます(このとき、音声が記録されたタイミングで、その音声はACP側に転送され、音声認識処理が実行されます)。

その後認識が完了したタイミングで、

・更新ボタンをタップ

・Apple Watchをバックグラウンドからフォアグラウンドに状態遷移させる

のいずれか行うことで、転送した音声の認識結果を取得し、「…」が認識結果に変更されます。

また音声記録は複数個記録することができ、各音声の認識結果は非同期的に反映されます(下図)。

f:id:amivoice_techblog:20211014112651p:plain

実装

○開発環境

  • macOS Big Sur 11.4
  • Xcode 12.5.1
  • Swift 5.0
  • WatchOS 6.0 and later

○ソースコード

ダウンロード

○非同期HTTP音声認識API

冒頭でも述べましたが、今回は非同期HTTP音声認識APIを利用して、Apple Watchで記録した音声をバッチ認識させていきます。

そのためには、まず非同期HTTP音声認識APIの仕様を確認してみます。

I/F仕様 非同期HTTP音声認識API 詳細 – AmiVoice Cloud Platform を参照すると、アプリ側で非同期HTTP音声認識APIを利用するためには、

 1. 音声認識ジョブを作成する

 2. ポーリングをして音声認識ジョブの状態をチェックし、結果を取得する

の2つのステップを実行する必要があります。

具体的には、下図のようなApple Watch-ACP間でのやり取りが必要となります。

f:id:amivoice_techblog:20211014110106p:plain

まず、ACP上で音声認識ジョブを作成するために、Watch側で音声ファイルをマルチパートPOSTすることで、音声認識のリクエストを行います。

作成された音声認識ジョブは、

・”queued”

・”started”

・”processing”

・”completed”

・”error”

の5つの状態を持っており(「I/F仕様 非同期HTTP音声認識API 詳細」- 「レスポンス」参照)、状態が”completed”になった後に、音声認識結果を取得することができます。

そのため音声認識ジョブを作成後は、このジョブの状態をポーリングにより都度監視する必要があります(但し、本アプリではユーザーの操作によってジョブの状態を監視しています)。

以上をまとめると、非同期HTTP音声認識APIを利用するためには、

①POSTで音声認識のリクエストを実行

②GETでジョブの状態を取得

の2つの処理をアプリ側で実装する必要があります。

本記事では、この①と②の実装についてご紹介していきたいと思います。

①POSTで音声認識のリクエスト

非同期HTTP音声認識APIを利用してバッチ認識を実行するための最初のステップである音声認識ジョブを作成するためには、該当する音声ファイルをマルチパート方式でPOSTすることで音声認識のリクエストを実行する必要があります。

I/F仕様 HTTP音声認識API 詳細 – AmiVoice Cloud Platform」の「マルチパートPOSTのリクエストイメージ」を参照すると、WatchOSで音声認識のリクエストを行うために、下記形式に沿ってHTTPリクエストを作成していきます。

なお、今回の実装ではprofileWordsの設定は省きます。

POST https://acp-api.amivoice.com/v1/recognize
Content-Type: multipart/form-data;boundary=some-boundary-string
--some-boundary-string
Content-Disposition: form-data; name="u"
(このパートには<AppKey>を格納します)
--some-boundary-string
Content-Disposition: form-data; name="d"
grammarFileNames=-a-general profileWords=hogehoge%20%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92%E3%81%A6%E3%81%99%E3%81%A8
--some-boundary-string
Content-Disposition: form-data; name="a"
Content-Type: application/octet-stream
(最後のパートに音声データを格納します)
--some-boundary-string--

上記のリクエストイメージを参考にして、SwiftでHTTPリクエストを実装すると、下記のようになります。ここでは、APPKEYと音声を格納する位置に注意していください。

let appKey = "XXXX...."  // 自分のAPPKEY
let urlStr = "https://acp-api-async.amivoice.com/v1/recognitions" // 非同期HTTP音声認識APIのリクエスト先URL
guard let url = URL(string: urlStr) else { return } 

var request = URLRequest(url: url)
request.httpMethod = "POST" 

let boundary = String(arc4random() % 10000)
request.allHTTPHeaderFields = ["Content-Type":"multipart/form-data; boundary=\(boundary)"] 

var body = Data() 

body.append("--\(boundary)\r\n".data(using: .utf8)!) 
body.append("Content-Disposition: form-data; name=\"u\"\r\n\r\n".data(using: .utf
8)!) 
body.append("\(appKey)\r\n".data(using: .utf8)!)

body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"d\"\r\n\r\n".data(using: .utf8)!)
body.append("grammarFileNames=-a-general\r\n".data(using: .utf8)!)

body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"a\"\r\n".data(using: .utf8)!)
body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)

// 音声データの格納
body.append(data)
body.append("\r\n".data(using: .utf8)!)
body.append("--\(boundary)\r\n".data(using: .utf8)!)

request.httpBody = body as Data

リクエスト作成後は、URLSessiondataTask(with:completionHandler:)でリクエストを実行します。リクエストが完了後は、completionHandlerでレスポンス(Data, Error, URLResponse)が返ってきます。取得したデータは、JSONDecoderを介して任意のデータ型であるJobData型にデコードします。デコードに成功後は、job.sessionidといった形式でsessionidを取得することができます。

URLSession.shared.dataTask(with: request) { data, response, error in 
    if let error = error { return } 
    guard let data = data else { return } 
    
    do { 
        let decoder = JSONDecoder() 
        let job = try decoder.decode(JobData.self, from: data)
        // -> jobからジョブの状態や認識結果を取り出す

    } catch let error { 
        print("Error: \(error)")
    } 
}.resume()
struct JobData: Codable { 
    var sessionid: String?
    var text: String? 
}
②GETでジョブの状態の取得

非同期HTTP音声認識APIを利用するための次のステップとして、作成した音声認識ジョブの状態を確認し、認識結果を取得する必要があります。「I/F仕様 非同期HTTP音声認識API 詳細」の「2.ジョブの状態の確認、結果の取得」を見ると、

下記のように、音声認識ジョブの作成時に保持したsessionidをパラメータに設定し、GETを実行します。

GET https://acp-api-async.amivoice.com/v1/recognitions/{session_id}

またリクエスト作成時に、Authorizationヘッダーに自分のAPPKEYを指定します。

①の時と同様に、リクエストを実行するとcompletionHandlerでレスポンス(Data, Error, URLResponse)が返ってきます。取得したデータを、JSONDecoderを介してJobData型にデコードします。

let appKey = "XXXX...."  // 自分のAppKey
let urlStr = "https://acp-api-async.amivoice.com/v1/recognitions/" + session_id 
guard let url = URL(string: urlStr) else { return } 

var request = URLRequest(url: url) 
request.allHTTPHeaderFields = ["Authorization":"Bearer \(appKey)"]

URLSession.shared.dataTask(with: request) { data, response, error in 
...
}.resume()
struct JobData: Codable { 
        // -> POST 
    var sessionid: String? 
    var text: String?
    // -> GET  
    var audio_md5: String?
    var audio_size: Int?  
    var session_id: String?
    var service_id: String?
    var status: String?
    // -> If success
    var utteranceid: String?
    var code: String? 
    var message: String? 
    var results: JobResult? 
    // -> If failure
    var error_message: String?  
}

struct JobResult: Codable { 
    var text: String? 
    var results: [Results]? 
    
    struct Results: Codable { 
        var confidence: Double? 
        var starttime: Int? 
        var endtime: Int? 
        var rulename: String? 
        var tags: [Int]? 
        var text: String? 
        var tokens: [Tokens]? 
    
        struct Tokens: Codable { 
            var confidence: Double? 
            var spoken: String? 
            var written: String? 
            var starttime: Int? 
            var endtime: Int?
            var label: String? 
        } 
    } 
}

※ 2022/2/17 JobData構造体を一部修正

JobData型にデコードされたデータにおいて、job.statusの値を確認することで、ジョブの状態を把握することができます。

job.status == “completed”つまりジョブの状態が”completed”に遷移したとき、job.textより認識結果を取得することができます。取得した認識結果は、job.session_idと一致するアプリ側のsessionidと紐ついた「…」を認識結果に変更します。

このように、音声認識のジョブを作成した際に、それぞれのsessionidを保持していますので、複数個の音声ファイルをバッチ認識にかけても、各sessionidを頼りに認識結果を反映することができます。

以上より、

①POSTで音声認識のリクエストを実行

②GETでジョブの状態を取得

を実装することで、非同期HTTP音声認識APIを介したバッチ認識が可能となります。

今回開発したアプリは、こちらに置いてありますので、ご興味ある方はApple Watchで動作させてみてください。

おわりに

今回は、AmiVoice Cloud Platformの非同期HTTP音声認識APIを利用して、録音した音声をバッチ認識できるWatchOSアプリの開発をしてみました。実際に非同期HTTP音声認識APIを利用してみて、アプリに組み込みやすく、簡単に実装することができました。みなさんもぜひお試しください。

参考

AmiVoice Cloud Platform関連

マニュアル Archive – AmiVoice Cloud Platform

Codable, JSONDecoder

Apple Developer Documentation

【Swift】Codableについて備忘録

この記事を書いた人

  • 林政樹

    Swift/Objective-Cで、iOS/WatchOSアプリの開発をしています。

     
APIを無料で利用開始