Raspberry PiのJuliusによる認識結果を、PaPeRo i にしゃべらせる

PaPeRo iのマイクでRaspberry Pi の Juliusによる音声認識を行う」による音声認識を利用した簡単な例として、認識結果をPaPeRo i にしゃべらせる方法を紹介します。

Raspberry Pi 側の準備

(1) 「PaPeRo iのマイクでRaspberry Pi の Juliusによる音声認識を行う」の手順(Raspberry Pi 側)の(1)~(3)を実施します。

(2) pythonのパッケージws4pyをインストールします。

$ sudo pip3 install ws4py

(3) 「PaPeRo iをRaspberry Pi上のpythonから操作する」の「Raspberry Piへ通信ライブラリをインストール」に従い、ライブラリを配置します。
以下の説明では、ライブラリを ~/papero の下に配置したものとします。

(4) 下記の内容をコピー・ペーストして、julius_sample.py を作成し、~/papero の下に置きます。

import sys
import time
import socket
import select
from enum import Enum
import xml.etree.cElementTree as ET

import pypapero


class State(Enum):
    st0 = 10
    st1 = 11
    st2 = 12
    end = 999


class JuliusManager:
    def __init__(self, host, port, vraw):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.buf_julius_lines = []
        self.buf_recv = b""
        self.finish = False
        try:
            self.sock.connect((host, port))
        except:
            self.finish = True
        self.vraw = vraw

    def receive_msg(self):
        rtn_msg = ""
        self.potential_readers = [self.sock]
        self.potential_writers = []
        self.potential_errs = []
        timeout = 0.1
        ready_to_read, ready_to_write, in_error = \
            select.select(self.potential_readers, self.potential_writers, self.potential_errs, timeout)
        if self.sock in ready_to_read:
            err = False
            try:
                bytes = self.sock.recv(4096)
            except:
                err = True
            if err or (not bytes):
                self.finish = True
            else:
                self.buf_recv += bytes
                messages = self.buf_recv.split(b"\n")
                n_msg = len(messages)
                i_msg = 0
                while i_msg < (n_msg - 1):
                    msgline = messages[i_msg].decode('utf-8')
                    self.buf_julius_lines.append(msgline)
                    if self.vraw:
                        print("Received: " + msgline)
                    i_msg += 1
                self.buf_recv = messages[i_msg]
        while True:
            idx_separate = (-1)
            for i in range(len(self.buf_julius_lines)):
                if self.buf_julius_lines[i] == ".":
                    idx_separate = i
                    break;
            if idx_separate >= 0:
                msg_xml = ""
                for i in range(idx_separate):
                    msg_xml += self.buf_julius_lines[0] + "\n"
                    del self.buf_julius_lines[0]
                del self.buf_julius_lines[0]
                rtn_msg = self.extract_sentence_from_julius_xml(msg_xml)
            if (len(rtn_msg) > 0) or (idx_separate < 0):
                break
        return rtn_msg

    def modify_invalid_char_xml(self, msg_xml):
        rtn_xml = ""
        instr = False
        for i in range(len(msg_xml)):
            chr = msg_xml[i]
            if instr:
                if chr == '<':
                    chr = '&lt;'
                elif chr == '>':
                    chr = '&gt;'
                elif chr == '"':
                    instr = False
            else:
                if chr == '"':
                    instr = True
            rtn_xml += chr
        return rtn_xml

    def extract_sentence_from_julius_xml(self, msg_xml):
        msg_xml_modified = self.modify_invalid_char_xml(msg_xml)
        rtn_sentence = ""
        objs = ET.fromstring("<Root>" + msg_xml_modified + "</Root>")
        shypoes = objs.findall(".//SHYPO")
        for shypo in shypoes:
            for elm in shypo:
                if elm.tag == "WHYPO":
                    word = elm.attrib["WORD"]
                    rtn_sentence += word
        return rtn_sentence

    def send_command(self, command):
        msgbytes = command.encode('utf-8') + b'\n'
        try:
            self.sock.sendall(msgbytes)
        except:
            self.finish = True

    def close_connection(self):
        self.sock.close()


def main(julius, papero):
    state = State.st0
    last_time = time.monotonic()
    past_time = 0
    speech_idx = 0
    speaking = False
    list_sentence = ["聞き取った言葉を発話します。座布団のボタンで終了します。"]
    while state != State.end:
        sentence = julius.receive_msg()
        if len(sentence) > 0:
            print("Recognized: " + sentence)
            list_sentence.append(sentence + "って聞こえたよ。")
        elif julius.finish:
            print("Connection to julius closed.")
            state = State.end
        messages = papero.papero_robot_message_recv(0.1)
        now_time = time.monotonic()
        delta_time = now_time - last_time
        last_time = now_time
        if messages is not None:
            msg_dic_rcv = messages[0]
        else:
            msg_dic_rcv = None
        if papero.errOccurred != 0:
            print("------Error occured(main()). Detail : " + papero.errDetail)
            break
        if state == State.st0:
            if len(list_sentence) > 0:
                if not speaking:
                    julius.send_command("PAUSE")
                    papero.send_turn_led_off("ear")
                    papero.send_turn_led_on("mouth", 
                                            ["G3G3G3G3G3G3G3G3G3", "2", "NNG3G3G3G3G3NN", "2",
                                             "NNNG3G3G3NNN", "2", "NNG3G3G3G3G3NN", "2"], repeat=True)
                    speaking = True
                papero.send_start_speech(list_sentence[0])
                del list_sentence[0]
                past_time = 0
                state = State.st1
            else:
                if speaking:
                    papero.send_turn_led_off("mouth")
                    papero.send_turn_led_on("ear", ["W3W3", "5"], True)
                    julius.send_command("RESUME")
                    speaking = False
        elif state == State.st1:
            past_time += delta_time
            if past_time >= 1.0:
                papero.send_get_speech_status()
                state = State.st2
        elif state == State.st2:
            if msg_dic_rcv is not None:
                if msg_dic_rcv["Name"] == "getSpeechStatusRes":
                    if str(msg_dic_rcv["Return"]) == "0":
                        state = State.st0
                    else:
                        past_time = 0
                        state = State.st1
        if msg_dic_rcv is not None:
            if msg_dic_rcv is not None:
                if msg_dic_rcv["Name"] == "detectButton":
                    papero.send_turn_led_off("mouth")
                    papero.send_turn_led_off("ear")
                    state = State.end


def get_options(argv):
    papero_addr = argv[1]
    vraw = False
    if (len(argv) > 2) and (argv[2] == "-vraw"):
        vraw = True
    return papero_addr, vraw


if __name__ == "__main__":
    if len(sys.argv) > 1:
        papero_addr, vraw = get_options(sys.argv)
        simulator_id = ""
        robot_name = ""
        ws_server_addr = "ws://" + papero_addr + ":8088/papero"
        julius = JuliusManager("localhost", 10500, vraw)
        try:
            papero = pypapero.Papero(simulator_id, robot_name, ws_server_addr)
        except:
            papero = None
        if julius.finish:
            print("Connection to Julius failed.")
        elif papero is None:
            print("Connection to PaPeRo i failed.")
        else:
            main(julius, papero)
        if papero is not None:
            papero.papero_cleanup()
        julius.close_connection()
    else:
        print("Usage:")
        print(sys.argv[0] + " papero_i_ip_addr [-vraw]")

PaPeRo i 側の準備

(1) PaPeRo i 制御用WebSocket通信アドオンシナリオをまだインストールしていない場合は、「PaPeRo iをRaspberry Pi上のpythonから操作する」の「PaPeRo iにアドオンシナリオをインストール」に従ってインストール作業を行います。

(2) 「PaPeRo iのマイクでRaspberry Pi の Juliusによる音声認識を行う」の手順(PaPeRo i 側)の(1)を実施します。

音声認識の実行

(1) Raspberry Pi のターミナルで、入力元として adinnet を指定して、モジュールモードで Julius を起動します。

$ cd ~/julius/dictation-kit-v4.4
$ julius -C main.jconf -C am-gmm.jconf -input adinnet -module

(2) Raspberry Pi のターミナルをもう一つ開き、その中で julius_sample.py を実行します。

$ cd ~/papero
$ python3 julius_sample.py PaPeRoiのIPアドレス

起動が完了すると、PaPeRo i が「聞き取った言葉を発話します。座布団のボタンで終了します」と発話します。

(3) PaPeRo i 側でadintoolを起動し、マイクの音声がJuliusに送られるようにします。

(debian_stretch)# adintool -in mic -out adinnet -server IPアドレス

ここで指定するIPアドレスは、Raspberry PiのIPアドレスです。
これ以降、認識された言葉をPaPeRo i が「~って聞こえたよ」と発話するようになります。

Juliusとクライアントとの間の通信について

Juliusはモジュールモードで起動すると、10500番ポートでクライアントからのTCP接続を待ちます。
クライアント(今回の場合はjulius_sample.py)が接続すると、Juliusから下記のような形でクライアントにXMLのメッセージが送られてきます。

<STARTPROC/>
.
<INPUT STATUS="LISTEN" TIME="1516336015"/>
.
<STOPPROC/>
.
<STARTPROC/>
.
<INPUT STATUS="LISTEN" TIME="1516336023"/>
.
<INPUT STATUS="STARTREC" TIME="1516336026"/>
.
<STARTRECOG/>
.
<INPUT STATUS="ENDREC" TIME="1516336028"/>
.
<ENDRECOG/>
.
<INPUTPARAM FRAMES="118" MSEC="1180"/>
.
<RECOGOUT>
  <SHYPO RANK="1" SCORE="-2926.393311">
    <WHYPO WORD="" CLASSID="<s>" PHONE="silB" CM="0.633"/>
    <WHYPO WORD="こんにちは" CLASSID="こんにちは+感動詞" PHONE="k o N n i ch i w a" CM="0.482"/>
    <WHYPO WORD="。" CLASSID="</s>" PHONE="silE" CM="1.000"/>
  </SHYPO>
</RECOGOUT>
.
<INPUT STATUS="LISTEN" TIME="1516336029"/>
.
<STOPPROC/>
.
<STARTPROC/>
.
<INPUT STATUS="LISTEN" TIME="1516336033"/>
.
<INPUT STATUS="STARTREC" TIME="1516336035"/>
.
<STARTRECOG/>
.
<INPUT STATUS="ENDREC" TIME="1516336040"/>
.
<ENDRECOG/>
.
<INPUTPARAM FRAMES="191" MSEC="1910"/>
.
<RECOGFAIL/>
.
<INPUT STATUS="LISTEN" TIME="1516336040"/>
.
<INPUT STATUS="STARTREC" TIME="1516336057"/>
.
<STARTRECOG/>
.
<INPUT STATUS="ENDREC" TIME="1516336062"/>
.
<ENDRECOG/>
.
<INPUTPARAM FRAMES="185" MSEC="1850"/>
.
<RECOGOUT>
  <SHYPO RANK="1" SCORE="-5087.060059">
    <WHYPO WORD="" CLASSID="<s>" PHONE="silB" CM="0.488"/>
    <WHYPO WORD="ご飯" CLASSID="ご飯+名詞" PHONE="g o h a N" CM="0.413"/>
    <WHYPO WORD="を" CLASSID="を+助詞" PHONE="o" CM="0.018"/>
    <WHYPO WORD="食べ" CLASSID="食べ+動詞" PHONE="t a b e" CM="0.149"/>
    <WHYPO WORD="に" CLASSID="に+助詞" PHONE="n i" CM="0.221"/>
    <WHYPO WORD="いこう" CLASSID="いこう+動詞" PHONE="i k o u" CM="0.037"/>
    <WHYPO WORD="よ" CLASSID="よ+助詞" PHONE="y o" CM="0.013"/>
    <WHYPO WORD="。" CLASSID="</s>" PHONE="silE" CM="1.000"/>
  </SHYPO>
</RECOGOUT>
.
<INPUT STATUS="LISTEN" TIME="1516336063"/>
.
<STOPPROC/>
.
<STARTPROC/>
.
<INPUT STATUS="LISTEN" TIME="1516336068"/>
.

改行コードは “\n”であり、一つのメッセージは”.”のみの行で終端されています。
メッセージが要素を含んでいれば、そこから認識結果の言葉を取り出す事ができます。
但し、タグの属性文字列の中に”<“という文字が含まれている事がある為、このままXMLパーサに渡すとエラーとなってしまうようです。そのため、julius_sample.pyでは、””で囲まれた範囲にある”<“、”>”を<、>に置き換える処理を、パース前に行っています。

PaPeRo i がしゃべっている間は、自分の言葉をマイクで拾わないようにする為、PaPeRo i にしゃべらせる前にJuliusに対し”PAUSE”コマンドを送信して音声認識を中断し、しゃべり終わったら”RESUME”コマンドで再開させています。

尚、julius_sample.py の起動の際に、下記のように -vraw オプションを指定する事により、上記のXMLでの受信メッセージを実際に確認する事ができます。

$ python3 julius_sample.py PaPeRoiのIPアドレス -vraw

0