ネスカフェ バリスタを音声コントロール

ネスカフェ バリスタを使って、"ツイッター投稿"、"スマホでコントロール" に、続いて第3弾。
今度は、音声認識を使って、バリスタのスイッチをコントロールしてみた。

ネスカフェ バリスタを音声コントロール
ハードウェアの方は、USBマイクをつける以外は、"スマホでコントロール"の時と同じで、今回は、ソフトウェアの設定が主になる。
音声認識は、Juliusというシステムを使わせていただいた。
Juliusについてもたくさんの参考サイトが説明してくれているので、非常に助かる!

以下、その詳細。
では、まず、USBマイクを接続して、認識されていることと、優先順位が最初に来ているかを確認

$ sudo lsusb
Bus 001 Device 004: ID 0411:01ee BUFFALO INC. (formerly MelCo., Inc.) WLI-UC-GNM2 Wireless LAN Adapter [Ralink RT3070]
Bus 001 Device 005: ID 0d8c:013c C-Media Electronics, Inc. CM108 Audio Controller
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

$ sudo cat /proc/asound/modules
 0 snd_usb_audio
 1 snd_bcm2835

私の場合、USBには、BUFFALOのWiFIドングルのWLI-UC-GNM2 と、USBマイクのBSHSMO5BKというのが刺さっている。
上記リストのうちC-Media というのがマイクで、ちゃんと認識されていた。
優先順位のほうは、0のほうが優先順位が高いそうなのだが、そのままだと、SND_BCM2835のほうが優先になっていた。
で、順位を変更するのだけれど、ラズパイのOSのバージョンによって少し、動きが違っていた。
NOOBS v1.4.1 をインストールしたときは、"/etc/modprobe.d/alsa-base.conf" を編集すれば上記のようなったのだけど、NOOBS v1.5.0 をインストールしたときは、そもそも、この"alsa-base.conf"というファイル自体が見つからなかった。
で、いろいろググって見ると、こちらのサイトで助けられた。
簡単にできる!音声認識と音声合成を使ってRaspberrypiと会話 - Qiita

v1.4.1のときは、だいたい、以下のサイトに習って設定すればうまくいった。
Raspberry Piで音声認識 - Qiita

上記のサイトを参考にさせてもらって、Juliusのインストールとテストまでは、問題なく進んだ。
インストールできたら、自分に必要なワードの辞書を作成して辞書ファイルに変換。
以下のサイトも大変参考になる。
ラズパイで音声認識をしてみる | うしこlog
私の場合は、以下のように辞書ファイル作成

~$ nano word.yomi

ブラックにして  ぶらっくにして
ラテにして      らてにして
カプチーノにして        かぷちーのにして
エスプレッソにして      えすぷれっそにして
マグにして      まぐにして
コーヒーいれて  こーひーいれて
バリスタくん    ばりすたくん
おしまい        おしまい

$ cd ~/julius-4.3.1/gramtools/yomi2voca
$ iconv -f utf8 -t eucjp ~/word.yomi | ./yomi2voca.pl > ~/julius-kits/dictation-kit-v4.3.1-linux/word.dic

辞書ファイルができたら、上記サイトを参考に設定ファイルを作成

$ nano ~/julius-kits/dictation-kit-v4.3.1-linux/word.jconf

うまく認識されていることを確認。

認識できることが確認されたら、認識した文字でバリスタのスイッチをコントロールするプログラムをPythonで作成。
その前に、先ほどの”Word.jconf”のファイルの最後に "-module" という行を追加して、Julius をモジュールモードで実行するように設定
Pythonのプログラムは、以下のサイトを参考にさせていただいた。
Raspberry Pi

こちらのサイトの2015/08/30の記事のPythonプログラムをベースに下記のように、自分用にアレンジした。

~$ nano barista01.py

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

'''
  ファイル名 : barista01.py
  起動方法
  (1) Julius をモジュールモードで起動
      julius -C julius-kits/dictation-kit-v4.3.1-linux/word.jconf 

  Julius(フリーの高性能音声認識ソフトウェア)

  ・TCP/IP で認識結果を受信し認識結果を表す<RECOGOUT>...</RECOGOUT> 部分を抽出する

  参考 URL
    http://moosoft.jp/index.php?option=com_content&view=article&id=99&Itemid=134
    http://julius.osdn.jp/
    http://kuhjaeger.co.jp/laboratory/raspberrypi/julius/
'''

import threading
import RPi.GPIO as GPIO
import socket
import time
import sys

# モジュールトップレベル変数設定 Start
SOCK      = None      # TCP/IP ソケット
ENCODING  = 'utf-8'  # codec

# GPIO ピン番号設定
# GPIO.setmode(GPIO.BOARD) P1 ヘッダ上のピン番号を用いる
RELAY_1   = 24        # コーヒー   GPIO8 = 24 ピン
RELAY_2   = 11        # ブラック   GPIO17 = 11 ピン
RELAY_3   = 12        # マグ     GPIO18 = 12 ピン
RELAY_4   = 13        # エスプレッソ  GPIO27 = 13 ピン
RELAY_5   = 15        # カプチーノ    GPIO22 = 15 ピン
RELAY_6   = 16        # ラテ   2   GPIO23 = 16 ピン
P0 = 18               # Aques bit0
P1 = 22               # Aques bit1
P2 = 19               # Aques bit2

HOST_NAME = 'localhost'
PORT_NUM  = 10500
BUF_SIZE  = 4096
DELIM_STR = '.\n'
START_STR = '<RECOGOUT>'
END_STR   = '</RECOGOUT>'
FIND_STR0 = ['コーヒー'  , 'いれて']
FIND_STR1 = ['ブラック', 'にして']
FIND_STR2 = ['マグ', 'にして']
FIND_STR3 = ['エスプレッソ', 'にして']
FIND_STR4 = ['カプチーノ', 'にして']
FIND_STR5 = ['ラテ', 'にして']
FIND_STR6 = ['バリスタ','くん']
FIND_STR7 = ['おし', 'まい']

# モジュールトップレベル変数設定 End

# GPIO 初期化
def gpio_init():
    try:
        GPIO.setwarnings(False)                 # Warning 表示停止
        #GPIO.setmode(GPIO.BCM)                 # GPIO ピン番号を用いる
        GPIO.setmode(GPIO.BOARD)                # P1 ヘッダ上のピン番号を用いる
        GPIO.setup(RELAY_1,GPIO.OUT)
        GPIO.setup(RELAY_2,GPIO.OUT)
        GPIO.setup(RELAY_3,GPIO.OUT)
        GPIO.setup(RELAY_4,GPIO.OUT)
        GPIO.setup(RELAY_5,GPIO.OUT)
        GPIO.setup(RELAY_6,GPIO.OUT)
        GPIO.setup(P0,GPIO.OUT)
        GPIO.setup(P1,GPIO.OUT)
        GPIO.setup(P2,GPIO.OUT)
        GPIO.setup(RELAY_1,                     # SW 1
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(RELAY_2,                     # SW 2
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(RELAY_3,                     # SW 3
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(RELAY_4,                     # SW 4
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(RELAY_5,                     # SW 5
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(RELAY_6,                     # SW 6
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_DOWN)
        GPIO.setup(P0,                     # Bit0
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_UP)
        GPIO.setup(P1,                     # Bit1
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_UP)
        GPIO.setup(P2,                     # Bit2
                   GPIO.OUT,
                   pull_up_down=GPIO.PUD_UP)
    except Exception as e:
        print('gpio_init:{0}'.format(e))

# 受信解析 SW ON 制御クラス
class tcp_recv(threading.Thread):

    # コンストラクタ
    def __init__(self):
        threading.Thread.__init__(self)
        self.daemon = True               # デーモンスレッド

    # 受信解析 SW ON 制御
    def run(self):
        global SOCK

        try:
            SOCK = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            SOCK.connect((HOST_NAME, PORT_NUM))
            rv_str = ''
            while SOCK is not None:
                rv = SOCK.recv(BUF_SIZE).decode(ENCODING)
                rv_str += rv
                if DELIM_STR in rv:                               # '.\n' : メッセージの区切り
                    s0 = rv_str.find(START_STR)                   # '<RECOGOUT>'
                    s1 = rv_str.find(END_STR)                     # '</RECOGOUT>'
                    if (s0 > -1) and (s1 > -1):
                        rec = rv_str[s0 : s1 + len(END_STR) + 1]  # 文字列抽出
                        print(rec)

                        if FIND_STR0[0] in rec:                   # Start SW
                            if FIND_STR0[1] in rec:
                                isON = "1.000" in rec
                            GPIO.output(P0, False)            # コーヒー入れて
                            GPIO.output(P1, False)            # Aques
                            GPIO.output(P2, False)
                            time.sleep(1)
                            GPIO.output(P0, True)
                            GPIO.output(P1, True)
                            GPIO.output(P2, True)
                            time.sleep(2)
                            GPIO.output(RELAY_1, isON)        #スイッチON
                            time.sleep(1)
                            GPIO.output(RELAY_1,False)

                        if FIND_STR1[0] in rec:               #ブラック
                            isON = FIND_STR1[1] in rec
                            GPIO.output(P1, False)            # Aques
                            GPIO.output(P2, False)
                            time.sleep(1)
                            GPIO.output(P1, True)
                            GPIO.output(P2, True)
                            GPIO.output(RELAY_2, isON)        # ブラックSet
                            time.sleep(1)
                            GPIO.output(RELAY_2,False)

                        if FIND_STR2[0] in rec:               # マグ
                            isON = FIND_STR2[1] in rec
                            GPIO.output(RELAY_3, isON)        # マグSet
                            GPIO.output(P0, False)            # Aques
                            GPIO.output(P2, False)
                            time.sleep(1)
                            GPIO.output(P0, True)            
                            GPIO.output(P2, True)            
                            GPIO.output(RELAY_3,False)

                        if FIND_STR3[0] in rec:               # エスプレッソ
                            isON = FIND_STR3[1] in rec
                            GPIO.output(P2, False)            # Aques
                            GPIO.output(RELAY_4, isON)        # エスプレッソSet
                            time.sleep(3)
                            GPIO.output(P2, True)
                            GPIO.output(RELAY_4,False)

                        if FIND_STR4[0] in rec:               # カプチーノ
                            isON = FIND_STR4[1] in rec
                            GPIO.output(RELAY_5, isON)        # カプチーノSet
                            GPIO.output(P0, False)            # Aques
                            GPIO.output(P1, False)            # Aques
                            time.sleep(1)
                            GPIO.output(RELAY_5,False)
                            GPIO.output(P0, True)            # Aques
                            GPIO.output(P1, True)            # Aques

                        if FIND_STR5[0] in rec:               # ラテ
                            isON = FIND_STR5[1] in rec
                            GPIO.output(RELAY_6, isON)        # ラテSet
                            GPIO.output(P1, False)            # Aquos
                            time.sleep(1)
                            GPIO.output(RELAY_6,False)
                            GPIO.output(P1, True)            # Aques

                        if FIND_STR6[0] in rec:               # バリスタくん
                            isON = FIND_STR6[1] in rec
                            GPIO.output(P0, False)            # Aques
                            time.sleep(1)
                            GPIO.output(P0,True)

                        if FIND_STR7[0] in rec:               # おしまい
                           # isON = FIND_STR7[1] in rec
                           if FIND_STR7[1] in rec:
                              if SOCK is not None:
                                   SOCK.shutdown(socket.SHUT_RDWR)
                                   SOCK.close()
                              GPIO.output(RELAY_1, GPIO.LOW)       # SW OFF
                              GPIO.output(RELAY_2, GPIO.LOW)       # SW OFF
                              GPIO.output(RELAY_3, GPIO.LOW)       # SW OFF
                              GPIO.output(RELAY_4, GPIO.LOW)       # SW OFF
                              GPIO.output(RELAY_5, GPIO.LOW)       # SW OFF
                              GPIO.output(RELAY_6, GPIO.LOW)       # SW OFF
                              GPIO.output(P0, GPIO.HIGH)       # Bit OFF
                              GPIO.output(P1, GPIO.HIGH)       # Bit OFF
                              GPIO.output(P2, GPIO.HIGH)       # Bit OFF
                              GPIO.cleanup()
                              sys.exit()                            # exit

                    rv_str = ''
        except Exception as e:
            if SOCK is not None:
                SOCK.shutdown(socket.SHUT_RDWR)
                SOCK.close()
                SOCK = None
            print('run:{0}'.format(e))

# メインループ
def main():
    try:
        gpio_init()                          # GPIO 初期化
        tcp_recv().start()                   # 受信解析クラス
        while True: time.sleep(1000)
    except KeyboardInterrupt:
        if SOCK is not None:
            SOCK.shutdown(socket.SHUT_RDWR)
            SOCK.close()
        GPIO.output(RELAY_1, GPIO.LOW)       # SW OFF
        GPIO.output(RELAY_2, GPIO.LOW)       # SW OFF
        GPIO.output(RELAY_3, GPIO.LOW)       # SW OFF
        GPIO.output(RELAY_4, GPIO.LOW)       # SW OFF
        GPIO.output(RELAY_5, GPIO.LOW)       # SW OFF
        GPIO.output(RELAY_6, GPIO.LOW)       # SW OFF
        GPIO.output(P0, GPIO.HIGH)       # Bit OFF
        GPIO.output(P1, GPIO.HIGH)       # Bit OFF
        GPIO.output(P2, GPIO.HIGH)       # Bit OFF
        GPIO.cleanup()
        raise
    except Exception as e:
        print('main:{0}'.format(e))

# ============================================== #
if __name__ == '__main__':
    main()

続いて、音声認識コントロールを開始するためのスクリプトを作成

$ nano barista.sh

#!/bin/sh
amixer sset Mic 10
julius -C /home/pi/julius-kits/dictation-kit-v4.3.1-linux/word.jconf &
sleep 3
sudo python3 barista01.py &

exit 0

これで、実行するときは、

$ sudo bash barista.sh

と、やれば、音声認識コントロールが開始される。

誤認識もあるけど、期待した以上の精度でスイッチがコントロールできる。 ただ、音声合成で発声しているのだけど、この音をマイクがひろってしまい、ループしてしまうことがあるので、センテンスをひろったら、しばらくマイクのレベルをゼロにするとか何かの対策が必要かも・・・。
もう少し、改良のポイントは、あるかもね・・・。