読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Juliusで認識結果の取り出し方を調べてみた

C++ Julius

はじめに

前のエントリでは CALLBACK_RESULT で登録できるコールバック関数の中身で何をやっているのやら…、という感じだったので、Juliusのコードを追っかけてざっと調べてみました備忘録です。何の知識もない人が追いかけたのでかなりの間違いを含んでいるかもしれませんがあしからず。

解説

forを紐解く

まずは前回のブログで CALLBACK_RESULT に登録したコールバック関数の中身を見てみます。

callback_add(recog, CALLBACK_RESULT, [](Recog *recog, void*) {
	for (const RecogProcess *r = recog->process_list; r; r = r->next) {
		WORD_INFO *winfo = r->lm->winfo;
		for (int n = 0; n < r->result.sentnum; ++n) {
			Sentence *s   = &(r->result.sent[n]);
			WORD_ID *seq = s->word;
			int seqnum   = s->word_num;
			for (int i = 0; i < seqnum; ++i) {
				std::cout << winfo->woutput[seq[i]];
			}
		}
	}
}, NULL);

RecogProcess をグリグリまわす for 文が回ってます。RecogProcess言語モデル(Language Model: LM)や音響モデル(Acoustic Model: AM)と一連のパラメータを保持する構造体です。前回のエントリのコードで実際に実行してみた時の中身を gdb 使って出力してみます。

(gdb) p r
$1 = {live = 1 '\001', active = 0, config = 0x82aa750, am = 0x82ac8b8, lm = 0x82aa918,
lmtype = 2, lmvar = 1, ccd_flag = 0 '\000', wchmm = 0x82b7c98, trellis_beam_width = 138,
backtrellis = 0x836fe50, pass1 = {tlist = {0x83976c8, 0x8398d38}, tindex = {0x838edd8,
0x8390690}, maxtnum = 287, expand_step = 138, expanded = 0 '\000', tnum = {138, 0},
n_start = 0, n_end = 137, tl = 0, tn = 1, score_pruning_max = -2399.31201,
score_pruning_threshold = -1000000, score_pruning_count = 0, token = 0x838e808,
wordend_best_score = 0, wordend_best_node = 0, wordend_best_tre = 0x0,
wordend_best_last_cword = 0, totalnodenum = 138, bos = {backscore = 0, lscore = 0,
wid = 65535, begintime = -1, endtime = -1, last_tre = 0x0, next = 0x0},
nodes_malloced = 1 '\001', lm_weight = 0, lm_penalty = 0, lm_penalty_trans = 0,
penalty1 = 0, in_sparea = 0 '\000', tmp_sparea_start = 0, last_tre_word = 0,
first_sparea = 0 '\000', sp_duration = 0, pausemodelnames = 0x0, pausemodel = 0x0,
pausemodelnum = 0}, pass2 = {hypo_len_count = {0, 1, 1, 1, 2, 0 <repeats 146 times>},
maximum_filled_length = -1, framemaxscore = 0x838f6c0, stocker_root = 0x0, popctr = 5,
genectr = 13, pushctr = 13, finishnum = 1, current = 0x83bec28, cm_alpha = 0.0500000007,
cm_tmpbestscore = -2399.31177, cm_tmpsum = 1, l_stacksize = 13, l_stacknum = 0,
l_start = 0x0, l_bottom = 0x0, wordtrellis = {0x8391308, 0x838dfb0}, g = 0x838f4e0,
phmmseq = 0x838f698, phmmlen_max = 9, has_sp = 0x0, wend_token_frame = {0x0, 0x0},
wend_token_gscore = {0x0, 0x0}, wef = 0x0, wes = 0x0, cnword = 0x0, cnwordrev = 0x0},
pass1_wseq = {11, 1, 10, 8, 12, 0 <repeats 145 times>}, pass1_wnum = 5,
pass1_score = -2399.31201, sp_break_last_word = 0, sp_break_last_nword = 0,
sp_break_last_nword_allow_override = 0 '\000', sp_break_2_begin_word = 0,
sp_break_2_end_word = 0, peseqlen = 109, graph_totalwordnum = 0, result = {status = 0,
num_frame = 109, length_msec = 0, sent = 0x8390e38, sentnum = 1, wg1 = 0x0, wg1_num = 0,
wg = 0x0, confnet = 0x0, pass1 = {word = {11, 1, 10, 8, 12, 0 <repeats 145 times>},
word_num = 5, score = -2399.31201, confidence = {0 <repeats 150 times>}, score_lm = 0,
score_am = -2399.31201, gram_id = 0, align = 0x0}}, graphout = 0 '\000',
order_matrix = 0x0, order_matrix_count = 0, have_interim = 0 '\000', hook = 0x0, next = 0x0}

デカイ構造体ですね。また更にここからネストしているという…。と言うことは置いておいて、最後を見てみると

next = 0x0

となっています。つまり、NULLを指しています。このループは1回しか回らないわけです。

なぜ1回しか回らないのか

第9章 複数モデルを用いた認識に書いてあるように Julius は複数のモデルの並列音声認識をサポートしています。引数で複数のモデルを指定すると初っ端の j_create_instance_from_jconf を呼んだ時に、Julius: libjulius/include/julius/jfunc.hの呼び出しグラフを見ると分かるように j_regogprocess_new が呼ばれます。ここで recog->process_list に RecogProcess が追加されるわけです。通常はひとつのモデルしか使わないので1個しか入っていません。なので、この for は1回しか回りません。まぁ、決まり文句的に書いておけば良さそうです。

winfo?

次に「r->lm->winfo」を見てみます。「r」がモデルで、「r->lm」は言語モデルを参照しています。言語モデルって言っても漠然としているので、要は -gram で与えた情報とか諸々が入っている構造体と考えれば OK だと思います。で、WORD_INFO は「Word dictionary structure to hold vocabulary」と説明があるように、単語辞書の構造体です。中に構造体のメンバである char** woutput を調べれば登録している単語が引っ張ってこれます。例えば、

(gdb) p r->lm->winfo->woutput
$9 = (char **) 0x835c2c8
(gdb) p *($9)
$10 = 0x8368e10 "テレビ"
(gdb) p *($9+1)
$11 = 0x8368e40 "電気"
(gdb) p *($9+2)
$12 = 0x8368e68 "を"

といった具合に格納されているわけです。

winfo->woutput[ seq[i] ]はどうやら…

ということでどうやら seq[i] は認識結果の単語ID(woutput[ID]でアクセスできる)を返してくれるもののようです。「r->result.sentnum」で文章の数だけループするようですが、毎回「1」しか入らないので、どういう時に2以上の数字が入るかは分からないです。
まぁ、とりあえずループの中身を見てみると、「r->result.sent[n]」で認識結果のSentence構造体を得ています。中には文字列は含まれておらず、WORD_IDの配列が格納されています。これが認識結果の文字群になるわけです。このWORD_IDで先ほどの単語辞書、winfo->woutputから文字列をさらってきて連結すれば、文章がめでたく完成するわけです。

おわりに

Julius はネストされた構造体に深く潜らないとわからない場所も色々とありますが、一度分かってしまえば後は色々と利用できそうです。他の箇所も随時解析していきたいと思います。