Android
GoogleCloudPlatform
音声認識
gRPC

Google Cloud Speech-to-Text APIのStreaming RecognizeをAndroidアプリで使用する

はじめに

Google CloudSpeech-to-Text API(Speech API)は、Googleが公開している音声認識のためのAPIです。このAPIを利用すれば、音声の高精度な自動認識を簡単に使用できます。
実際、音声ファイルをHTTPで送信して結果を返してもらうREST APIは簡単に使用できました。しかし、送信した音声をリアルタイムに解析してもらうStreamingRecognizeリクエストはgRPCという私にとって未知のプロトコルを使用しないと利用できず、クライアントライブラリがAndroidに対応していないため少々苦戦しました。

後に調べたところ先人も発見できましたが、少し違った方法で実現したようなので私のやり方をここに残します。

前提条件

APIを利用するために、以下のことは終わっているものとします。

  • APIキーの取得
  • AndroidStudioの準備と使用方法の理解

gRPCとは何か

Google Cloud Speech APIの前に、これを利用するためにgRPCについて大まかに調べたので概要をここに書きます。プログラムを書くのに必要な分の知識しか収集しなかったので、誤りがあるかもしれません。

Protocol Buffers(protobuf)

protobufは、Googleが開発した、gRPCがデータの送受信に使用するフォーマットです。当然、gRPC以外のデータの送受信にも使用できるはずですが。

インターネットでデータの送受信によく使用されるフォーマットとして代表的なものにJSONやXMLなどがありますが、これらは情報をテキストにエンコードするので非効率的です。更に、こういったフォーマットは不定形のデータを扱うので、プログラミングする時にエラーチェックの負担が大きくなります。

protobufはサーバとクライアントの間でやり取りされるデータを、*.protoというファイルで定義し、これを使用します。それだけで自動的に型チェックが行われ、オーバーヘッドの少ない通信を実現できる仕組みです。*.protoファイルは使用する言語のファイルに変換して使用し、プログラマは各言語のネイティブなオブジェクトをそのまま扱うことができます。オブジェクトを通信に適した形に変換して効率的に伝送するのはprotobufの仕事であり、プログラマはこれについて意識する必要がありません。

gRPC

gRPCはprorobufを使用した、Google開発のRPCのプロトコルです。WEBで例えるならば、protobufはHTMLやJavascript、gRPCはHTTPに相当するものだと思いますが、どこが境界線なのかは私はよくわかっていません。

HTTP2のように、複数のパイプを同時に開いて効率的な通信ができるらしいです。

Streamingリクエスト

gRPCが扱えるリクエストには、1つのリクエストに1つの答えを返却する単純なものの他に、お互いにデータを断続的に送り続けるStream方式があります。Google Cloud Speech APIのStreamingRecognizeリクエストはこれを利用しています。

Streamingリクエストは、stub(gRPCの中で開く1つのパイプのようなもの)にStreamObserverを渡してStreamObserverを受け取ることから始まります。
渡すStreamObserverは自身が相手からの通信を待ち受けるためのオブジェクトで、受け取るStreamObserverは相手が通信を待ち受けているオブジェクトです。こうして、互いに相手に自分のStreamObserverを渡すことで互いに送りたいときに断続的に情報を送信でき、受信した情報を処理できるようになります。

当然、自分の手元にある相手のStreamObserverは(相手のマシンのメモリ上にある実体という意味での)本物ではなく、gRPCが作成したものです。このオブジェクトのメソッドを実行すると、gRPCがどうにかして相手のStreamObserverのメソッドを実行してくれるのです。

gradle(AndroidStudioが利用しているビルド環境)の設定

protobufやgRPCを使用するために、gradleファイルを少し書き換えます。このように.gradleを書き換えることで*.protoをアンドロイドアプリのプロジェクト内で一緒にコンパイルできました。書き換えたのは、プロジェクトではなくモジュールの.gradleです。

build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'    //追加

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "jp.example.android.app"
        minSdkVersion 21
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

def grpc_version = "1.3.0"    //追加

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'

//以下全て追加部分
    implementation "io.grpc:grpc-auth:$grpc_version"
    implementation "io.grpc:grpc-okhttp:$grpc_version"
    implementation "io.grpc:grpc-protobuf-lite:$grpc_version"
    implementation "io.grpc:grpc-stub:$grpc_version"
    implementation "javax.annotation:javax.annotation-api:1.2"
    implementation "com.google.auth:google-auth-library-oauth2-http:0.9.0"
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.1.0"
    }
    plugins {
        javalite {
            artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0"
        }
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:$grpc_version"
        }
    }
    generateProtoTasks {
        all().each { task ->
            task.plugins {
                javalite {}
                grpc {
                    option 'lite'
                }
            }
        }
    }
}

*.protoの配置

プロジェクト/モジュール名/src/main/proto/ 以下に配置した.protoファイルがコンパイルされます。
まず、cloud_speechのprotoファイル(cloud_speech.proto)をダウンロードしてsrc/main/proto/直下に配置します。
加えて、これが依存するすべてのファイルを用意します。正しい場所にファイルを配置しないと動かないので注意してください。(被依存ファイルはすべてproto/google/以下に配置されることになります)このとき、横着をして不要なファイルまで一緒に入れると不要なファイルでコンパイルエラーが出てうまく行きません。

必要なファイルのリストは以下のとおりです。私が使用したのは2018/2/13時点でのmasterのファイルですが、最新版でも特に問題ないと思います。

https://github.com/googleapis/googleapis/ から取得するファイル

https://github.com/google/protobuf/ から取得するファイル

Javaでの通信のプログラム

以下の手順をアプリの中で踏むことで、サーバと通信してリアルタイムな音声認識が可能です。

認証情報のデコード

JSON形式のAPIキー(トークン)が res/raw/token.json として組み込まれる場合

GoogleCredentials cred = GoogleCredentials.fromStream(
        resources.openRawResource(R.raw.token));

サーバへの接続

ManagedChannelBuilder cb
    = ManagedChannelBuilder.forTarget("speech.googleapis.com");
ManagedChannel channel = cb.build();
SpeechGrpc.SpeechStub stub
    = SpeechGrpc.newStub(channel)
              .withCallCredentials(MoreCallCredentials.from(cred));

StreamObserverの作成

public class MyObserver implements StreamObserver<StreamingRecognizeResponse> {
    @Override
    public void onNext(StreamingRecognizeResponse value) {
        if(value.hasError()){
            //エラー処理
            return;
        }
        if(value.getSpeechEventType() == StreamingRecognizeResponse.SpeechEventType.END_OF_SINGLE_UTTERANCE) {
            //一区切りの認識の終了処理
            return;
        }

        String newmsg = "";     //現在認識中の文字列
        for(StreamingRecognitionResult res:value.getResultsList()){
            newmsg += res.getAlternatives(0).getTranscript();
        }
        //newmsg = 現在認識中の文字列
    }

    @Override
    public void onError(Throwable t){
        //相手のonNextで例外が発生したときに呼ばれる模様。おそらくこのAPIで使用されることはない
    }

    @Override
    public void onCompleted(){
        //ストリームを閉じる処理
    }
}

StreamingRecognize の開始

StreamingRecognitionConfigの値は必要に応じて変更してください。

StreamObserver<StreamingRecognizeRequest> ostream
    = stub.streamingRecognize(new MyObserver());

StreamingRecognitionConfig conf
    = StreamingRecognitionConfig.newBuilder()
        .setConfig(
            RecognitionConfig.newBuilder()
                .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
                .setSampleRateHertz(rate)
                .setLanguageCode(lang)
                .build())
        .setSingleUtterance(true)
        .setInterimResults(true)
        .build();

ostream.onNext(
    StreamingRecognizeRequest.newBuilder()
        .setStreamingConfig(conf)
        .build());

新しい音声の断片の送信

//ByteString flagment = voice data;
ostream.onNext(
    StreamingRecognizeRequest.newBuilder()
        .setAudioContent(flagment)
        .build());

音声の終了

ostream.onCompleted();

動作デモ