RoboCup Junior Japan Rescue Kanto OB
2005~2013
2005~2013
本来ならPythonそのものの入門記事を予定していたのですが、
最近利用したライブラリがかな~り難解だったので、
再度使うときに忘れないようにするための備忘録を作っておくことにしました。
最近利用したライブラリがかな~り難解だったので、
再度使うときに忘れないようにするための備忘録を作っておくことにしました。
今回はJuliusというライブラリについてです。
これはいわゆる音声認識ライブラリでして、
某大学と某大学が研究で開発した基本無償提供のオープンソースライブラリです。
公式ページ
特徴としては、
・オースンソースであること
・マルチプラットフォームであること
・単語辞書を用いたシステムであること
・日本語のライブラリであること(英語版はあるけど公式は有償)改変した無償版を配布している方はおられました。
・認識におけるパラメータ調整が行えること(長くなるのでここではスルー
・なんかAPIがあるっぽい(試してないデス。
という具合。単語辞書ありきのシステムなので単語辞書を作らないと動いてくれません。
ネットの前評判では認識制度は上々、商用制限もないので使えると超便利という評価でした。
ただ、大学なので当然ですが、割とアカデミックな要素が強く、ぶっちゃけ使うだけでもとても難しいです。
正直ブログを調べてみると、使えた!という記事よりも使えなかった!という記事の方が目立つ程度には難しいです。
そのためネットでは情報が錯綜しています。
バージョンによっても挙動が違うとか違わないとかいう噂があるのでご注意を
また、Read meや、こちらから編集するファイルの文字コードが悉くshift-JISとなっているらしく、
Linuxユーザーの方がブチギレていました(笑)僕含め
とまぁ、前置きはこのぐらいにして本編をば。
今回は文法の解析をしてほしかったのでgrramar-kitを利用します。
まずは記事掲載時の最新版grammar-kit-4.3.1をDL、解凍します。.
起動についてはまぁ色々方法はありますが、とりあえず今回はbin/win32まで降りてみます。
cmdで
julius -C ../../testmic.jconfと打つと起動します。エディタで開いてjconfはパス遡ればわかりますがただのconfigです。
ここで辞書ファイルの方を指定しています。初期状態ではfruitを参照しているのがわかると思います。
当該のフォルダを開いてみるとfruit.dfa fruit.term fruit.dict の3つのファイルがあると思います。
これがいわゆる辞書になります。ここに登録されている単語、文法で認識を行っています。
今micで起動していますので、マイク入力が使えるはずです。
"(りんご|蜜柑|ぶどう)([0-9]+個)? を? 下さい" とマイクに向かってしゃべると
### Recognition: 2nd pass (RL heuristic best-first) STAT: 00 _default: 20 generated, 20 pushed, 7 nodes popped in 190 sentence1: <s> ぶどう を ください </s> wseq1: 7 0 3 4 8 phseq1: silB | b u d o u | o | k u d a s a i | silE cmscore1: 1.000 0.362 0.999 0.654 1.000 score1: -6065.491699と、このような感じで認識してくれます。正規表現は果たしてこれであっているのか・・・?
と、まぁ動かすだけならここまでです。Win環境下だとここまでは順調でした。
ところが問題がここから。
このJuliusは辞書にある単語しか認識しないので辞書を自分で作らないと単語認識してくれません。
五十音全てを持つ辞書は標準で用意されていますが、それを使うと認識率は当然落ちるので、
ちゃんとした認識を行うのであれば、自分で辞書を生成できるようにしないといけません。
ですが、Juliusはこの辞書ファイルの生成が大変厄介です。
このあたりの内容はJuliusbook含め、公式情報だけだと解決が不可能になるので
Google先生のお力を借りることになりました。
まず辞書ファイルはfruit.dfa fruit.term fruit.dict の3つのファイルです。
これらはfuit.voca及びfruit.grammarより生成されています。
で、ここの生成に必要なファイルがmkdfa.plというファイルになります。
plです。perlです。perl環境がないと動かないんですこの子。
更にこのmkdfaを動かすにはmkfaとdfa_minimizeが必要になります。
これらは全てJuliusの実行ファイルと同じフォルダ内にあります。
で、perlということだったので、Windowsでの初手は当然cygwinです。ですが。。。
そのままではcygwinでは起動することができません。tmpフォルダの生成に失敗するので
手動でusrtmpdirを設定しないとなりません。
設定しても動かないという例は多々確認して、それに対する解決策も色々ネットにはありましたが、
正直原因となりうる項目が多すぎて自分がどのパターンなのか探すのも面倒になるのでお勧めできません。
じゃあLinuxはどうかというと、linuxではmkdfa内で呼び出されるmkfaが動きません。
自分は試していないのですが、
mkfaを動かすためのライブラリはいくつか標準でインストールされていないものがあるようなので
mkfaを単体起動して調べてみる必要があるそうです。
Windows内にPerlが入っていれば割と問題なく動くようです。が、自分の環境には入っておらず。
それでもなんか$thisdirの取得ができないらしいので、何か別の方法で実行ファイルの絶対パスを入れる必要があるっぽいです
と、いう訳で。もういろいろ難しくて面倒だったので
Pythonで書き直しました(´・ω・`)
#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (c) 1991-2011 Kawahara Lab., Kyoto University # Copyright (c) 2000-2005 Shikano Lab., Nara Institute of Science and Technology # Copyright (c) 2005-2011 Julius project team, Nagoya Institute of Technology # # Generated automatically from mkdfa.pl.in by configure. # # Julius for python convert by radiumproduction # Copyright (c) 2007-2017 RadiumProduction import os import sys import re import subprocess ## setup # tmpdir usrtmpdir = ""; # specify if any args = sys.argv argc = len(sys.argv) # mkfa executable location thisdir = os.getcwd() mkfabin = thisdir+"/mkfa.exe" # dfa_minimize executable location minimizebin = thisdir+"/dfa_minimize.exe"; # find tmpdir tmpdirs = [usrtmpdir, os.environ.get('TMP'), os.environ.get('TEMP'), "/tmp", "/var/tmp", "/WINDOWS/Temp", "/WINNT/Temp"] def usage(): print "mkdfa.pl --- DFA compiler"+os.linesep print "usage: "+__file__+" [-n] prefix"+os.linesep print "\t-n ... keep current dict, not generate"+os.linesep exit() tmpdir="" for tmp in tmpdirs: t = tmpdirs.pop(0) if (t == ""): continue if (os.path.isdir(t) and os.access(t,os.W_OK)) : tmpdir = t break if (tmpdir == ""): sys.stderr.write("Please set working directory in \$usrtmpdir at "+__file__+os.linesep) exit(1) ############################################################# if (argc < 2 or args[1] == "-h"): usage() make_dict = 1; make_term = 1; CRLF = 0; gramprefix = ""; for arg in args: if (arg == "-t"): make_term = 1 elif (arg == "-n"): make_dict = 0 else: gramprefix = arg if (gramprefix == ""): usage() gramfile = gramprefix+".grammar" vocafile = gramprefix+".voca" dfafile = gramprefix+".dfa" dictfile = gramprefix+".dict" termfile = gramprefix+".term" tmpprefix = tmpdir+"\g"+str(os.getpid()) tmpvocafile = tmpprefix+".voca" rgramfile = tmpprefix+".grammar" # generate reverse grammar file try: gram=open(gramfile,'r') rgram=open(rgramfile,'wb') n=0 for row in gram: row=row.rstrip('\r\n') # row=re.sub(r"\r+$","",row) row=re.sub("#.*","",row) if not(re.match("^[ \t]*$",row)==None): continue spdata=row.split(":") spword=spdata[1].split(" ") spword.reverse() rgram.write(spdata[0]+":"+" ".join(spword)+os.linesep) n+=1 gram.close() rgram.close() except: sys.stderr.write("cannot open "+gramfile+" or "+rgramfile+os.linesep) exit(1) print gramfile+" has "+str(n)+" rules"+os.linesep # make temporary voca for mkfa (include only category info) if (not(os.access(vocafile,os.R_OK) )): sys.stderr.write("cannot open voca file "+vocafile) exit(1) try: voca=open(vocafile,'r') tmpvoca=open(tmpvocafile,'w') if(make_term==1): gterm=open(termfile,'wb') n1=0 n2=0 termid=0 for row in voca: row=row.rstrip('\r\n') # row=re.sub(r"\r+$","",row) row=re.sub("#.*","",row) if not(re.match("^[ \t]*$",row)==None): continue mt_data=re.match("^%[ \t]*([A-Za-z0-9_]*)",row) if not(mt_data==None): tmpvoca.write("#"+mt_data.group(1)+os.linesep) if make_term==1: gterm.write(str(termid)+"\t"+mt_data.group(1)+os.linesep) termid+=1 n1+=1 else: n2+=1 voca.close() tmpvoca.close() if make_term==1: gterm.close() print vocafile+" has "+str(n1)+" categories and "+str(n2)+" words"+os.linesep except: sys.stderr.write("cannot open "+vocafile+" or "+tmpvocafile+os.linesep) exit(1) print "---"+os.linesep # call mkfa and make .dfa status=0; if not(os.access(minimizebin,os.X_OK)): # no minimization print "Warning: dfa_minimize not found in the same place as mkdfa.pl"+os.linesep print "Warning: no minimization performed"+os.linesep if tmpprefix.find("cygdrive")!=-1 : status = subprocess.check_call(mkfabin+" -e1 -fg `cygpath -w "+rgramfile+"` -fv `cygpath -w "+tmpvocafile+"` -fo `cygpath -w "+dfafile+"` -fh `cygpath -w "+tmpprefix+".h`",shell=True) else : status = subprocess.check_call(mkfabin+" -e1 -fg "+rgramfile+" -fv "+tmpvocafile+" -fo "+dfafile+" -fh "+tmpprefix+".h",shell=True) else : # minimize DFA after generation if tmpprefix.find("cygdrive")!=-1 : status = subprocess.check_call(mkfabin+" -e1 -fg `cygpath -w "+rgramfile+"` -fv `cygpath -w "+tmpvocafile+"` -fo `cygpath -w "+dfafile+".tmp` -fh `cygpath -w "+tmpprefix+".h`",shell=True) else : status = subprocess.check_call(mkfabin+" -e1 -fg "+rgramfile+" -fv "+tmpvocafile+" -fo "+dfafile+".tmp -fh "+tmpprefix+".h",shell=True) subprocess.check_call(minimizebin+" "+dfafile+".tmp -o "+dfafile,shell=True) os.remove(dfafile+".tmp") os.remove(rgramfile) os.remove(tmpvocafile) os.remove(tmpprefix+".h") print "---"+os.linesep if (status != 0) : # error print "no .dfa or .dict file generated"+os.linesep exit() # convert .voca -> .dict # terminal number should be ordered by voca at mkfa output if make_dict == 1 : nowid = -1 try: voca=open(vocafile,'r') dictf=open(dictfile,'wb') for row in voca: row=row.rstrip('\r\n') # row=re.sub(r"\r+$","",row) row=re.sub("#.*","",row) if not(re.match("^[ \t]*$",row)==None): continue if not(re.match("^%",row)==None): nowid+=1 continue else: a=row.split() name=a.pop(0) dictf.write(str(nowid)+"\t["+name+"]\t"+" ".join(a)+"\r\n")#+os.linesep) voca.close() dictf.close() except: sys.stderr.write("cannot open "+vocafile+"or"+dictfile+os.linesep) exit(1) gene = dfafile if make_term == 1: gene += termfile if (make_dict == 1) : gene += dictfile print "generated: "+gene+os.linesep
はい。解決です。訳あって、もともとここの変換プログラムはexe形式にしたかったんですよ。
ただ、なんかperlってexeに書き出すとアホみたいに重くなるって聞いたので、はい。
HelloWorldだけでで3M持ってかれるって聞いたんですけどマジです?
でもここで終わりではないんです。
mkdfaにおいて変換元となる、vocaファイルとgrammarファイルですが、
grammarファイルは文法ファイルということで、こちらで手書きで文法を定義していくものです。
○○をください、とか、○○教えて、とか認識するフレーズを定義します。
しかし、vocaファイルは単語とそれに対する読みを音素列で定義してあるファイルです。
Juliusにおける音素列はこちらのサイトにあるようなのですが、如何せんこれを見ながら打つのはアホらしい。。。
ということでJuliusにはyomi2voca.plというプログラムがありまして、
これを使って単語とそのひらがな読みの羅列でできたyomiファイルからvocaファイルを生成することができます。
でも、これもperlだったので
Pythonで書き(ry
#!/usr/bin/python # -*- coding: utf-8 -*- # Julius for python convert by radiumproduction # Copyright (c) 2007-2017 RadiumProduction import sys import os import re args = sys.argv error=0 lineno=0 yomi = open(os.path.dirname(os.path.abspath(__file__))+"/"+args[1]+".yomi", 'r') voca = open(os.path.dirname(os.path.abspath(__file__))+"/"+args[1]+".voca", 'w') for line in yomi: if re.match(u"^%",line): voca.write(line)#+"\n") continue; word=line.split() word[1]=word[1].rstrip(os.linesep) word[1]=word[1].replace("う゛ぁ","b a ") -------------------------------(省略)-------------------------------------- word[1]=word[1].replace("を","o ") word[1]=re.sub(r"^ ","",word[1]) word[1]=re.sub(r":+",":",word[1]) word[1]=re.sub(r" :",":",word[1]) lineno+=1 if re.match(r"^[a-zA-Z:]+$",word[1]): if error==0: error=1 print "Error: (they were also printed to stdout)"+"\n" print "line "+str(lineno)+":"+word[0]+"\t"+word[1]+"\n" voca.write(word[0]+"\t"+word[1]+"\n") if len(args)==3: if args[2]=="-n": voca.write(r"% NOISE"+"\n") voca.write(r""+"\tsp"+"\n") voca.write(r"% NS_B"+"\n") voca.write(r"<s>"+"\tsilB"+"\n") voca.write(r"% NS_E"+"\n") voca.write(r"</s>"+"\tsilE"+"\n") yomi.close() voca.close()
こっちは単純にperlのs/gによる置換をreplaceで置換しただけです。perlの文字列処理パないわ
元ファイルからコピペ→置換でOKです
後はNS_BとNS_Eを自動で書きだす機能と-nのオプションでsp(休符)を書き出す機能を追加しました。
※NS_BとNS_Eはnoise_beginとnoise_endの略みたいです。文法の最初と最後にいるみたい。
spを入れる場合はgrammarの方でspを使わないとmkdfaでエラーを吐くのでご注意を。
という訳で作ったファイルで辞書を生成してみます。今回、prefixはtestで行います。
辞書ファイルは全て同じprefixじゃないと正しく認識されないのでご注意ください。
test.yomi
% NAME ココア ここあ チノ ちの リゼ りぜ 千夜 ちや シャロ しゃろ % CHAN ちゃん ちゃん % CUTE かわいい かわいい もふもふ もふもふ
test.grammar
S : NS_B NAME_S CUTE NS_E NAME_S : NAME CHAN NAME_S : NAME
cmd入力
python yomi2voca test python mkdfa test
これで最後に
generated: test.dfatest.termtest.dictとなっていれば成功です。因みに僕はpyinstallerを使っているのでexeにしてから実行しています。
文字コードは公式ではWinodowsはUTF-8、それ以外はEUCという風に指定されていますが、
先述したように、配布されているサンプルは何故か全てshift-jisです。
という訳で、どうやらshift-jisでも動くようです。
因みにwindowsのメモ帳はUTFで保存すると先頭に意味不明な文字が付加されるので、
辞書ファイルはメモ帳では作らないでください(´・ω・`)
3ファイルが生成されたらそれを、今回はとりあえずgrammar-kit直下に置きます。ちゃんとフォルダ作ろう?
生成されない場合は余分な改行とか、grammarファイルの文法ミスとかです。
入力仕様は本家様に合わせているので、変なもの入れると失敗するのはご愛敬です。
んでもって同一階層のtestmic.jconfを開いて
-gram test -C hmm_ptm.jconf -input mic -demoと変更。
あとはさっきと同じようにJuliusを起動すれば・・・
pass1_best: <s> ココア かわいい </s> pass1_best_wordseq: 3 0 2 4 pass1_best_phonemeseq: silB | k o k o a | k a w a i i | silE pass1_best_score: -4262.506348 ### Recognition: 2nd pass (RL heuristic best-first) STAT: 00 _default: 10 generated, 10 pushed, 5 nodes popped in 142 sentence1: <s> ココア かわいい </s> wseq1: 3 0 2 4 phseq1: silB | k o k o a | k a w a i i | silE cmscore1: 1.000 0.941 1.000 1.000 score1: -4261.156738 pass1_best: <s> チノ ちゃん もふもふ </s> pass1_best_wordseq: 3 0 1 2 4 pass1_best_phonemeseq: silB | ch i n o | ch a N | m o f u m o f u | silE pass1_best_score: -4179.256836 ### Recognition: 2nd pass (RL heuristic best-first) STAT: 00 _default: 15 generated, 15 pushed, 6 nodes popped in 139 sentence1: <s> チノ ちゃん もふもふ </s>と、まぁこんな感じでリアルタイムでの音声認識が可能です。
文法はwordseqから判断できますね(´・ω・`)
Windowsで文字化けする場合は
chcp 65001で文字コードをUTFに変えてしまいましょう。
で、記事の本題はここから。
まだpythonを使ってJuliusを動かしてないですからね。
プログラムに組み込む方法としては、JuliuslibとかいうAPIもあるようですが、
今回は簡潔に行きたかったのでsocketを用いた方法で行きます。あっちは情報が少なすぎる
まずはJuliusをモジュールモードで起動します。
julius -C ../../testmic.jconf -modulejconfで変更は可能ですが、Juliusは標準で10500番を使っているようなので、今回はそれで設定します。
クライアントのプログラムは此方。
標準出力してからファイルにも出力しています。贅沢ですね(´・ω・`)/
#!/usr/bin/python # -*- coding: utf-8 -*- import socket port = 10500 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("localhost", port)) f=open("reading_data.txt",'wb') while(1): data=client.recv(1024) print data f.write(data)これ以上記事を伸ばしたくないので、出力結果は割愛しますが
それぞれ別個で動かすと、なんかクライアントの方にリアルタイムでデータがぶっこまれます。
出力形式はxmlっぽいタグ付きの何かです。正規表現か何かで抜き出すといいんじゃないかな。これ正式名称あるのかな
というわけで今回の記事は5000兆年ぶりくらいに真面目なライブラリ備忘録でした。
やる気になったら1日かからずに全部終わったっていうか、この記事書く時間のほうが長かったくらいなので
plファイルで悩んでいる方はpythonやrubyで書き直すことをオススメします。
(^・ω・)ノ curonet at RadiumProduction
カレンダー
最新CM
カテゴリー
らじぷろ目次
らじぷろ検索機
最新記事
(01/01)
(08/27)
(04/29)
(01/01)
(11/20)
(09/06)
(09/04)
(08/09)
(08/06)
(07/27)
(05/29)
(03/15)
(01/01)
(05/07)
(01/11)
プロフィール
HN:
Luz
性別:
男性
アーカイブ