Tech Sketch Bucket of Technical Chips by TIS Inc.

日本語連続音声認識エンジン"Julius"をAndroidで動作させる 2

Pocket

日本語連続音声認識エンジン"Julius"をAndroidで動作させるの連載2回目です。Androidでオフライン音声認識を行うアプリに着々と近づいています。

今回は、 前回 生成したAndroidARM系CPU用Juliusライブラリと、AndroidのJavaアプリとをつなぐJNIコードを紹介します。
残念なことに、今回も一筋縄ではいきません。Juliusをライブラリとして利用する手順が複雑なため、Javaのnativeメソッド経由でJuliusライブラリを呼び出す"フツーのJNI"だけでなく、Juliusライブラリからコールバック関数経由でJavaメソッドが呼び出されるという逆方向の処理が必要なためです。

さて、覚悟も決まったところで、コードの世界に飛び込みましょう。
動作するソースコードは、 githubの Julius for Android を参照してください。


Juliusライブラリを利用する流れとJNIコード

今回の連載で利用しているJulius 4.2.2では、基本的に下記の流れで処理が進みます。julius-simple/julius-simple.c がとても参考になりますので、確認するとよいでしょう。
今回は4つのJNIメソッドで、処理の流れを表現しています。エラー処理は煩雑なので省略していますので、実際のソースコードは githubのinterface.c を確認してください。

1. Juliusを初期化する

コンフィグファイルと音声ファイルを利用する場合、Juliusは下記のような流れで初期化します。

1-1. j_config_load_file_new(コンフィグファイルのパス) で、Juliusのコンフィグレーション構造体[jconf]を生成する。
1-2. j_create_instance_from_jconf(jconf) で、認識エンジンのインスタンス[recog]を生成する。
1-3. callback_add により、認識終了時等に呼び出されるコールバック関数を登録する。
1-4. j_adin_init(recog) により、(コンフィグで設定されているならば)マイクの初期化を行う。

コンフィグファイルの文法が間違っていたり、音響モデルや言語モデルの読み込みに失敗した場合、この段階でエラーとなります。

callback_add(Recog *recog, int code, void (*func)(Recog *recog, void *data), void *data) を用いてコールバック関数を登録しますので、コールバック関数のシグネチャは static void callback(Recog *recog, void *data) にします。
なおコールバック登録時に4つめの引数に何らかのポインタを設定すると、コールバック関数が呼び出された際にそのポインタが引き渡されます。今回の処理内容のように利用用途がない場合、NULLを渡せば良いでしょう。

Julius初期化部分のJNIコードは、エラー処理を省けばこのようになります。シンプルですね。
なおJavaから呼び出される関数間で認識エンジンとJNIの環境情報を共有しなければ動かないため、グローバル変数としてそれらを保持します。このJNI処理を複数スレッドから同時に呼んだりしないでね!

2. 入力された音声を認識する

Java側でマイクからの音声をファイルに保存した後に、下記のような流れでJuliusの音声認識処理を呼び出します。

2-1. j_open_stream(recog, 音声ファイルのパス) により、音声ファイルをストリームとして開く。
2-2. j_recognize_stream(recog) により、音声認識処理を行う。

音声認識はそれなりに時間がかかる重い処理なので、j_recognize_streamは内部的に別スレッドを生成し、そちらで処理を行ないます。そのためこの関数はすぐにリターンします。
認識エンジンの処理が終了すると、1-3.で登録したコールバック関数が呼び出されます。よって音声認識した結果の処理は、コールバック関数に記述することになります。

実際のコードでは、何らかのエラーが出た際に「エラーになった」ことをJava側に通知するためのエラー処理が書かれています。

3. 音声認識の結果を得る

Juliusが音声認識を行なった後、認識結果が詰まった認識エンジンインスタンスを引数に、1-3.で登録したコールバック関数が呼び出されます。

3-1. 引数として渡される認識エンジンに、認識結果が詰まっている。julius-simple/julius-simple.cを参考に、認識エンジンから認識結果の文章を取り出す。
3-2. 認識結果の文章を表示する。

認識結果には信頼スコアなど様々なデータが詰まっていますが、今回のプロトタイプでは認識結果の文章だけ利用しています。
信頼スコアが低すぎる場合は再発声を促すなど、上手く活用してください。

認識結果の文章は、Juliusの初期化時に与えた言語モデルファイルのエンコードで返ってきます。(今回はShift_JISですが、JuliusのバージョンによってはEUC-JPの場合もあります。)

このコールバックの結果を画面に表示するために、JNI側からJavaのメソッドを呼び出します。返ってきた文字列をJNI側でutf-8に変換し、JavaのStringを生成してそれを引数にJavaメソッドを呼び出しても良いのですが、JNI側で文字コード変換処理を書くのが大変なのでそのままByte配列で返すことにしました。
JNI側からJavaメソッドを呼び出すのは、Javaのリフレクションを理解していればそれほど難しい処理では無いと思います。リソースの解放漏れに注意してください。

4. Juliusを解放する

最後に、開いた音声ファイルを閉じた後にJuliusを解放します。後片付けはキッチリね!

4-1. j_close_stream(recog) で、2-1.で開いた音声入力ストリームを閉じます。
4-2. j_recog_free(recog) で、認識エンジンのインスタンスを解放します。

認識エンジンのインスタンスが解放されると同時に、認識エンジンが確保したメモリも全て解放されます。

Juliusは本来複数の認識エンジンを同居させることができるはずなのですが、今回は「連続音声認識用」と「記述文法用」のコンフィグファイルを切り替える形で実装しています。そのため認識方法を変更する際には、「Juliusを解放→別のコンフィグファイルで再度Juliusを初期化」という手順を踏まなければなりません。

JNIコードのビルド

上記で解説したJNIコードをビルドし、Java側から利用できるsharedライブラリ(*.so)を生成します。
なおその際に、 第一回 で生成したJuliusライブラリを取り込まなければなりません。

Android.mk

クロスコンパイルしたJuliusのライブラリは、$TARGET_DIR/lib以下にlibjulius.a、及びlibsent.aとして生成されています。またヘッダファイルも$TARGET_DIR/include以下に生成されています。

これらのヘッダファイルとstaticライブラリを取り込むビルドスクリプトは、以下となります。

このビルドスクリプトのポイントは下記4点です。
なお今回は、$TARGET_DIRをJNIのRootディレクトリに設定したため、$(LOCAL_PATH)は$TARGET_DIRと同じ場所を指しています。環境によって読み替えてください。

  • Juliusライブラリ用のヘッダファイルをincludeするために、LOCAL_C_INCLUDES$(LOCAL_PATH)/includeを追加します
  • LOCAL_STATIC_LIBRARIESjuliussentを設定し、統合すべきstaticライブラリを登録します
  • libjulius.alibsent.aにはC標準ライブラリやGCCライブラリを組み込んでいませんので、LOCAL_LD_LIBS-lc-lz-lgccを設定して明示的にリンクします
  • LOCAL_LD_LIBS-L$(LOCAL_PATH)/lib-ljulius-lsentを設定することで、libjulius.alibsent.aもリンクさせます

このビルドスクリプトを用いてndk-buildすれば、はれてJavaから利用できるsharedライブラリjulius_arm.soが生成されます。

次回は

最終回の次回は、マイクから入力された音声をファイルとして記録し、このJNIコードを利用して音声認識を行うActivityを紹介します。
・・・Juliusが認識できる音声ファイルを作るのが、これまた一苦労だったりします。次回も濃いよ!

エンジニア採用中!私たちと一緒に働いてみませんか?