ブログ

ブログです

ただいまシステムの中身② Socketを介した認識結果のXMLパース

前回までで音声認識をする部分は完成したから、今回は認識結果を出力して渡す部分について。
まずはJuliusをモジュールモードで起動して、その後にpythonでsocketつないでxmlをもらう、といった流れ。
f:id:hira-hide:20170423165131p:plain
前回の最後にサーボ制御についても説明するとか書いてたけど、それはさらに次回。

参考ページ

今回のはほぼこちらのページを参考にさせてもらった。
blog.livedoor.jp

Juliusのモジュールモード起動

Julius起動時のオプションに -module を付けるとモジュールモードでの起動になって、クライアントからの TCP/IP 接続待ち状態になる。その状態でクライアントからの接続を受けると音声認識可能な状態になって、クライアントに認識結果をXMLで送信するようになる。
より詳細については公式ドキュメントを参照してね。
第10章 モジュールモード

ちなみに、いちいちオプションに -module つけんのめんどくせぇよってときは、前回作った設定ファイル(word.jconf)に -module を書いとけば勝手にモジュールモードにしてくれる。

Julius出力XML

JuliusのXMLは以下のような構成になってるらしい。公式ページから引用。改行コードは "\n"で、クライアントがパーズしやすくするために、メッセージ送信ごとにデータの終端として"." のみの行が送信される。

<STARTPROC/>
<INPUT STATUS="LISTEN" TIME="994675053"/>
<INPUT STATUS="STARTREC" TIME="994675055"/>
<STARTRECOG/>
<INPUT STATUS="ENDREC" TIME="994675059"/>
<GMM RESULT="adult" CMSCORE="1.000000"/>
<ENDRECOG/>
<INPUTPARAM FRAMES="382" MSEC="3820"/>
<RECOGOUT>
  <SHYPO RANK="1" SCORE="-6888.637695" GRAM="0">
    <WHYPO WORD="silB" CLASSID="39" PHONE="silB" CM="1.000"/>
    <WHYPO WORD="上着" CLASSID="0" PHONE="u w a g i" CM="1.000"/>
    <WHYPO WORD="を" CLASSID="35" PHONE="o" CM="1.000"/>
    <WHYPO WORD="白" CLASSID="2" PHONE="sh i r o" CM="0.988"/>
    <WHYPO WORD="に" CLASSID="37" PHONE="n i" CM="1.000"/>
    <WHYPO WORD="して" CLASSID="27" PHONE="sh i t e" CM="1.000"/>
    <WHYPO WORD="下さい" CLASSID="28" PHONE="k u d a s a i" CM="1.000"/>
    <WHYPO WORD="silE" CLASSID="40" PHONE="silE" CM="1.000"/>
  </SHYPO>
</RECOGOUT>
.

これをXMLパーサで受け取って解析すれば、認識結果の信頼度命令を抽出できるから、音声制御が可能になる。

Python処理

今回プログラムはすべてPythonで書いた。理由は特になくて、みんなRaspberry Pi動かすときはPython使ってるっぽいからそうしただけ。サンプルコード多いと助かるしね。
まずはとりあえずプログラム全体の掲載。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import xml.etree.ElementTree as ET

def main():
    host = 'localhost'
    port = 10500

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, port))

    try:
        data = ''
        while 1:
            if '</RECOGOUT>\n.' in data:
                root = ET.fromstring('<?xml version="1.0"?>\n' + data[data.find('<RECOGOUT>'):].replace('\n.', ''))
                for whypo in root.findall('./SHYPO/WHYPO'):
                    command = whypo.get('WORD')
                    score = float(whypo.get('CM'))
                    
                    if command == u'ただいま' and score >= 0.9:
                        # ここにただいま処理
                    elif command == u'パキン' and score >= 0.996:
                        # ここにパキン処理
                    elif command == u'おやすみ' and score >= 0.93:
                        # ここにおやすみ処理
                    elif command == u'いってきます' and score >= 0.93:
                        # ここにいってきます処理
                    elif command == u'部屋つけて' and score >= 0.93:
                        # ここに部屋つけて処理
                    elif command == u'おはよう' and score >= 0.9:
                        # ここにおはよう処理
                    elif command == u'部屋消して' and score >= 0.9:
                        # ここに部屋消して処理
                    elif command == u'廊下つけて' and score >= 0.93:
                        # ここに廊下つけて処理
                    elif command == u'廊下消して' and score >= 0.9:
                        # ここに廊下消して処理
                data = ''
            else:
                data = data + client.recv(1024)
    except KeyboardInterrupt:
        client.close()

if __name__ == "__main__":
    main()

個々の処理は次回以降また説明するとして、今回はsocketとXML解析についてだけ。

Socket接続部分

host = 'localhost'
port = 10500

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))

Socketについてはこれだけ。あ、もちろん最初に import socket は忘れないように。
今回は Host も Client も自分自身だから、IPアドレスlocalhost として、ポートはデフォルトの10500番
この宣言で socket が接続されるから、音声認識待ち状態になって認識結果をXMLで投げるようになる。

XML解析部分

try:
    data = ''
    while 1:
        if '</RECOGOUT>\n.' in data:
            root = ET.fromstring('<?xml version="1.0"?>\n' + data[data.find('<RECOGOUT>'):].replace('\n.', ''))
            for whypo in root.findall('./SHYPO/WHYPO'):
                command = whypo.get('WORD')
                score = float(whypo.get('CM'))
                
                if command == u'ただいま' and score >= 0.9:
                    # ここにただいま処理
                elif command == u'パキン' and score >= 0.996:
                    # ここにパキン処理
                elif command == u'おやすみ' and score >= 0.93:
                    # ここにおやすみ処理
                elif command == u'いってきます' and score >= 0.93:
                    # ここにいってきます処理
                elif command == u'部屋つけて' and score >= 0.93:
                    # ここに部屋つけて処理
                elif command == u'おはよう' and score >= 0.9:
                    # ここにおはよう処理
                elif command == u'部屋消して' and score >= 0.9:
                    # ここに部屋消して処理
                elif command == u'廊下つけて' and score >= 0.93:
                    # ここに廊下つけて処理
                elif command == u'廊下消して' and score >= 0.9:
                    # ここに廊下消して処理
            data = ''
        else:
            data = data + client.recv(1024)
except KeyboardInterrupt:
    client.close()

まず data という空の変数を作って、whileでずっとループを回して受け取ったデータをこの変数に格納できるようにする。
上に書いたように Julius の XML は \n. で終端されるから、それを判定基準として if を入れている。
PythonXML パーサでちゃんと処理できるように、まず xml ヘッダと改行コード(\n)を付け足して、RECOGOUT 要素以下を XML としてパースする。

実際の認識結果は タグとして渡されるから、whypo に格納して、value を見て判定を行っていく。ここでは WORD(認識した単語)と CM(信頼度)を抽出して命令実行のための判定に使っている。
f:id:hira-hide:20170423181632j:plain
print命令で WORD と CM を出力した結果はこんな感じ。あとは処理部分を記述すればだいたいおわり。

次回はサーボモータ制御について書く予定。