日本語連続音声認識エンジン"Julius"をAndroidで動作させる 1
iPhone4SやiPhone5をお持ちのみなさん、 Apple Siri は活用していますか?NTT docomoのスマートフォンをお持ちの方は、 しゃべってコンシェル を使ってらっしゃいますか?
AndroidやiOSを搭載したスマートデバイスが花盛りの昨今、Apple Siriやしゃべってコンシェルのような スマートデバイスに話しかける」ことで何らかのアクションを起こさせるサービスが、特別な機器を揃えずとも使えるようになりました。
このようなサービスは一般消費者にとっても有用ですが、スマートデバイスを企業内で利用するシーンでは特に力を発揮します。
例えば両手がふさがった状態で機械の整備をしている時に、胸ポケットに入れたスマートデバイスに「次は何をするんだっけ?」と話しかけたら、「次は右の3番ボルトを10N.mのトルクで締めてください」とか答えてくれたら、すごく便利ですよね。
このようなサービスを実現するための入り口が、音声認識です。人が話した「音声データ」を解析してプログラムが理解できる「文字列」に変換する技術のことで、1960年代のコンピュータの黎明期から研究されている歴史の古い分野です。スマートデバイスとクラウドの普及に従い、音声認識技術はこの数年で一気に一般化しました。
残念ながらApple SiriのAPIはまだ公開されていませんが、Androidでは RecognizerIntent を用いることで簡単に音声認識アプリを作ることができます。ただしこのRecognizerIntentには、大きな問題が一つあります。インターネットへのアクセスが可能な状態(=オンライン)でなければ利用できないのです。しかし、例えば地下で配管工事をしている作業者の支援など、オフラインで音声認識ができれば役に立つシーンはたくさんあります。
そこで今回を含めた全3回で、オープンソースで公開されている 「日本語連続音声認識エンジン Julius」 をAndroid上に移植し、Androidでオフライン音声認識を行う方法について解説します。
動作するソースコードは、 githubに Julius for Android として公開しています。よければforkして動作を確認してください。
今回の連載もかなり濃いですが、ついてきてくださいネ!
日本語連続音声認識エンジン"Julius"とは
Julius は、オープンソースの日本語連続音声認識エンジンです。日本語の音声認識を行うオープンソースのエンジンとしては「ほぼ唯一の存在」であり、音声を連続した文章として認識するだけでなく、自ら指定した単語と文法に強制的にマッチさせることもできるなど、かなり自由度の高いエンジンです。
(一方、その分利用するにはハードルが高いのも確かです。十数行で音声認識をさせることができる一方で、細かい制御が不可能な"RecognizerIntent"とは対照的ですね)
http://julius.sourceforge.jp/ の "What's Julius?"より
Julius は,音声認識システムの開発・研究のためのオープンソースの高性能な汎用大語彙連続音声認識エンジンです. 数万語彙の連続音声認識を一般のPC上でほぼ実時間で実行できます. また,高い汎用性を持ち,発音辞書や言語モデル・音響モデルなどの音声認識の各モジュールを組み替えることで,様々な幅広い用途に応用できます. Julius はオープンソースソフトウェアで,ソースコードを含めてどなたでもフリーで入手することができます. Julius の研究・開発に関わっている主な機関は以下の通りです.
Copyright © 1991-2012 京都大学 河原研究室
Copyright © 1997-2000 情報処理振興事業協会(IPA)
Copyright © 2000-2005 奈良先端科学技術大学院大学 鹿野研究室
Copyright © 2005-2012 名古屋工業大学 Julius開発チーム
Juliusの公式マニュアルにはLinuxとWindows用のビルド手順が示されていますし、少し工夫すればMacOSXでも苦労なく動作させることができます。
またJulius自身はC言語で書かれているため、C言語と相性の良いiOS上でも動作させることが可能です。実際 FLCL.jp では、 Julius on iOSの サンプルアプリ が公開されています。
・・・ところがAndroid上で動作させた情報はどこにも紹介されて無いのです・・・
うん、無いなら自分でやればいいのさ! AndroidOSもLinuxの亜種、やってやれないことはないよね!!?
Android用のARM系CPUとIntelCPUのエンディアンの違い
Android上で動作するネイティブアプリは、AndroidNDKに搭載されたクロスコンパイラを用いて、デバイスのCPUにあわせた実行形式にクロスコンパイルする必要があります。この際に注意しなければならないのは、Android用のARM系CPUはビッグエンディアンで、IntelCPUはリトルエンディアンということです。(エンディアンの詳細は、Wikipediaの エンディアン などを参考にしてください)
Juliusが音声認識を行うためには、音響モデルと言語モデルを必要とします(詳細は Juliusのマニュアル を参照)。これらのモデルを自分で作ることも可能ですが、今回はJuliusの配布パッケージに同梱されているサンプルのモデル定義を利用しています。
このモデル定義はバイナリファイルとして作成されていますが、IntelCPUなどリトルエンディアンのCPU用に作られているため、ビッグエンディアンであるAndroid用ARM系CPUの場合、一部のソースコードでバイトオーダーを変更してやらないと上手く動作しないのです。
(ややこしいことに、iPhoneに搭載されているARM系CPUはリトルエンディアンで動作しています。そのためiOS上では特に修正無くJuliusが動作するのです・・・)
JuliusをAndroidARM用にビルド
注意:以下、2012/09/01時点で最新の Julius 4.2.2 をベースに説明します。4.2.2以外のバージョンでは、ソースコードが異なる場合があります。
ビルドスクリプト
Juliusでは環境差異を吸収するために、autoconfが利用されています。基本的にはconfigureでARM用にビルドすることを指定すれば良いはずなのですが、なぜか上手く行かないので、今回は
configureが生成するconfig.cacheに、直接bigendianを指定するオプションを書き加える
というかなり強引な方法でクリアしました。
なおこのスクリプトは、下記の環境で動作するように書かれています。
OS | MacOSX Lion (10.7.4) |
---|---|
AndroidNDK | android_ndk_r8b |
Juliusのライブラリ(libjulius.aとlibsent.a)を生成するディレクトリ(TARGET_DIR)やAndroidNDKをインストールしたディレクトリ(NDK_ROOT)は、適切に書き換えてください。
またTOOL_ROOTはMacOSX用のクロスコンパイルツールを指しています。これも必要に応じて適切に変更してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#!/bin/sh export TARGET_DIR="$HOME/Documents/workspace/JuliusForAndroid/jni" export NDK_ROOT="$HOME/Lib/android/android-ndk-r8b" export TOOL_ROOT="$NDK_ROOT/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin" export SYSROOT="$NDK_ROOT/platforms/android-8/arch-arm" export ARCH_ROOT="$NDK_ROOT/platforms/android-8/arch-arm" export CC="$TOOL_ROOT/arm-linux-androideabi-gcc -mandroid --sysroot=$SYSROOT" export AR="$TOOL_ROOT/arm-linux-androideabi-ar" export AS="$TOOL_ROOT/arm-linux-androideabi-as" export LD="$TOOL_ROOT/arm-linux-androideabi-ld" export NM="$TOOL_ROOT/arm-linux-androideabi-nm" export RANLIB="$TOOL_ROOT/arm-linux-androideabi-ranlib" export SIZE="$TOOL_ROOT/arm-linux-androideabi-size" export STRIP="$TOOL_ROOT/arm-linux-androideabi-strip" export OBJDUMP="$TOOL_ROOT/arm-linux-androideabi-objdump" export CPPFLAGS="-I$ARCH_ROOT/usr/include/" export CFLAGS="-nostdlib -DANDROID_CUSTOM -DANDROID_DEBUG" export LDFLAGS="-Wl,-rpath-link=$ARCH_ROOT/usr/lib/ -L$ARCH_ROOT/usr/lib/" export LIBS="-lc -lz -lgcc -llog" make clean echo "ac_cv_c_bigendian=\${ac_cv_c_bigendian=yes}" > config.cache ./configure --host=arm-eabi --prefix=$TARGET_DIR make make install |
このシェルスクリプトでは、以下の作業を実施しています。
- CCやAR、LD等のコンパイラ関係のコマンドを全てARM用のツールに置き換える
- AndroidOS用のCヘッダファイルを指定する
- AndroidOSの標準ライブラリやGCCライブラリを適切に読み込むために、以下のコンパイルオプションを設定する
- "-nostdlib"
- "-Wl,-rpath-link=$ARCH_ROOT/usr/lib/ -L$ARCH_ROOT/usr/lib/"
- "-lc -lz -lgcc"
- NDK側からLogCatにログを出力できるように、"-llog"オプションを設定する
- プリプロセッサ用に、ANDROID_DEBUGとANDROID_CUSTOMをdefineする
- config.cacheに、BigEndian用のMakefileを生成する次のオプションを強引に書き加える
- ac_cv_c_bigendian=${ac_cv_c_bigendian=yes}
- ARM用にconfigureし、makeする
このスクリプトにより、TARGET_DIR/lib以下にAndroid用にビルドされたJuliusライブラリが生成されます。が、実は動作しません。音響モデルと言語モデルのバイナリファイルを読み込む部分でコケます。
そこでJuliusのソースコードを一部修正し、エンディアンの調整を行います。ANDROID_CUSTOMは、そのAndroid用に修正したソースコードを有効にするためのプリプロセッサシンボルです。
また同時に、JuliusのログをAndroidのlogcatに出力させるための修正も行なっておきましょう。
read_binhmm.c
read_binhmm.cでは、バイナリ形式のHMM音響モデルを読み込むための関数が定義されています。ANDROID_CUSTOMシンボルが定義されている場合、rdnfunc関数のswap_bytesが有効になるように修正しています。
(WORDS_BIGENDIANというシンボルが謎です・・・上手く動いて無いようです)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
static boolean rdnfunc(FILE *fp, void *buf, size_t unitbyte, int unitnum) { size_t tmp; if (unitnum == 0) return TRUE; if (gzfile) { tmp = myfread(buf, unitbyte, unitnum, fp); } else { tmp = fread(buf, unitbyte, unitnum, fp); } if (tmp < (size_t)unitnum) { jlog("Error: read_binhmm: failed to read %d bytes\n", unitbyte * unitnum); return FALSE; } //修正部分 ANDROID_CUSTOMが定義されている場合、WORDS_BIGENDIANを無効にする #ifdef ANDROID_CUSTOM #undef WORDS_BIGENDIAN #endif #ifndef WORDS_BIGENDIAN if (unitbyte != 1) { swap_bytes(buf, unitbyte, unitnum); } #endif //修正部分 ANDROID_CUSTOMが定義されている場合、WORDS_BIGENDIANを再定義する #ifdef ANDROID_CUSTOM #define WORDS_BIGENDIAN #endif return TRUE; } |
ngram_read_bin.c
ngram_read_bin.cは、バイナリ形式の単語N−gramの言語モデルを読み込むための関数が定義されています。read_binhmm.cとは逆に、ANDROID_CUSTOMシンボルが定義されている場合はswap_bytesしないようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
static boolean rdnfunc(FILE *fp, void *buf, size_t unitbyte, size_t unitnum) { size_t tmp; if ((tmp = myfread(buf, unitbyte, unitnum, fp)) < unitnum) { jlog("Error: ngram_read_bin: failed to read %d bytes\n", unitbyte*unitnum); return FALSE; } //修正部分 ANDROID_CUSTOMが定義されている場合は、SWAPしないようにする #ifndef ANDROID_CUSTOM if (need_swap) { if (unitbyte != 1) { swap_bytes(buf, unitbyte, unitnum); } } //修正部分 ANDROID_CUSTOMが定義されている場合は、SWAPしないようにする #endif return TRUE; } |
jlog.c
jlog.cは、Juliusのログを出力するためのユーティリティ関数が定義されています。Juliusは通常stdoutにログを出しますが、AndroidNDKの場合stdoutは/dev/nullに捨てられますので、logcatにも出すように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#define ERR_STR "ERROR:" ... void jlog(char *fmt, ...) { va_list ap; char buf[256] = {'\0'}; if (initialized == FALSE) { outdev = stdout; } else if (outdev == NULL) return; va_start(ap,fmt); vfprintf(outdev, fmt, ap); vsprintf(buf, fmt, ap); if (strncmp(buf, ERR_STR, 6) == 0) { __android_log_print(ANDROID_LOG_ERROR, "Julius jlog", buf); } #ifdef ANDROID_DEBUG else { __android_log_print(ANDROID_LOG_DEBUG, "Julius jlog", buf); } #endif va_end(ap); return; } |
256文字のchar配列を生成し、渡されたメッセージをフォーマットに従ってchar配列に書き込みます。
フォーマットの最初6文字が"ERROR:"の場合は、ERRORレベルでlogcatに出力します。それ以外のログは、ANDROID_DEBUGシンボルが定義されている場合はDEBUGレベルでlogcatに出力します。
次回は
ここまでで、やっとJuliusのAndroid用ライブラリ(libjulius.aとlibsent.a)が生成できました。
次回はJuliusライブラリとAndroidJavaアプリとをつなぐJNIコードを紹介します。
・・・次回も濃いよ!
エンジニア採用中!私たちと一緒に働いてみませんか?