忍者ブログ
RoboCup Junior Japan Rescue Kanto OB

             2005~2013
[1100]  [1099]  [1098]  [1097]  [1095]  [1094]  [1093]  [1091]  [1090]  [1089]  [1088
本来なら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 -module
jconfで変更は可能ですが、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
Comments
※コメントは内容確認後に手動で公開するようにしております。反映までしばらくお待ちください。
Your Name
Title
color
Comment
 

カレンダー
02 2024/03 04
S M T W T F S
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
最新CM
[05/09 ONE RoboCuper]
[05/07 HDD ほしいよー]
[04/21 ブラック3辛]
[12/26 bols-blue]
[06/08 ONE RoboCuper]
かうんた
カウンター カウンター
らじぷろ目次
らじぷろ検索機
プロフィール
HN:
Luz
性別:
男性

PR

忍者ブログ 2007-2021,Powered by Radium-Luz-Lα+-Rescatar in RadiumProduction [PR]


Related Posts Plugin for WordPress, Blogger...