Raspberry Pi でオレオレAmazonEcho(もしくはGoogleHomeもしくはSiri)を作ってみた

Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita
Raspberry piAmazon Echo(AVS)を動かしてみる未遂 - Qiita...

こちらで書いた記事は有力な情報が見つからず相変わらず同じ状態。 その状況を打破すべく、オレオレAmazonEchoを作ってみることにした。

オレオレAmazonEchoにやらせたいこと

とにかくラジオが好きだ。よく聞いている。朝はTBSのスタンバイを聞くことが最近の日課だ。 6:42から始まる歌のない歌謡曲は最高だ。 しかし寝起きにそのあたりに転がっているiPhoneを取り出し、radikoアプリを起動し、TBSを選択し、Airplayを設定するのはとてもめんどくさい。 となればラズパイをradikoサーバにでもしてcronで自動起動させればよいじゃないかと思うのだが、自動は嫌だ。

と言う事で話しかければradikoが鳴ることを最終目標にオレオレAmazonEchoを作ることにした。

環境

raspberry piでサウンド関係はちょっとめんどくさいので事前に調べて設定しておく。

音声認識

LINEBot SDK+Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita
LINEBot SDKMicrosoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita...

音声認識にはMicosoft Speech APIを利用しようかと思ったけれども、ローカルで動く Juliusというものを知ったので利用する事とした。

Juliusのインストール

Juliusをgithubからインストールし、ラズパイで音声認識できるまでもっていく。

# 依存ライブラリインストール
$ sudo apt-get install libasound2-dev
# Juliusインストール
$ git clone https://github.com/julius-speech/julius.git
$ cd julius
$ ./configure 
$ make
$ sudo make install

ディクテーションキットとディクショナリーキットのインストール

$ wget -O dictation-kit-v4.3.1-linux.tgz
 http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60416%2Fdictation-kit-v4.3.1-linux.tgz'
$ wget -O grammar-kit-v4.1.tar.gz
 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F51159%2Fgrammar-kit-v4.1.tar.gz'

そのまま使ってもよいが自分で辞書を作成するとそこに記載の言葉しか認識しないので、今回の要件にマッチするため辞書を作成する。

辞書の作成

$ vi /home/pi/julius_sample/sample.yomi
$ cat /home/pi/julius_sample/sample.yomi                                                                                                                
シシマル ししまる
エヌエチケー えぬえちけー
JFM じぇーうぇーぶ
TBS てぃーびーえす
INT いんたーえふえむ
消して けして

辞書の変換

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

julius conf作成

$ vi /home/pi/julius_sample/sample.jconf
$ cat /home/pi/julius_sample/sample.jconf
-w sample.dic
-v model/lang_m/bccwj.60k.bingram
-h model/phone_m/jnas-tri-3k16-gid.binhmm
-hlist model/phone_m/logicalTri
-n 5
-output 1
-input mic
-rejectshort 500
-charconv euc-jp utf8
-lv 1000

juliusをモジュールモードで起動

$ julius -C /home/pi/julius-kits/dictation-kit-v4.4/sample.jconf -module

radiko

無事辞書にある言葉が認識したらraspberry piradiko再生を行える環境を整える。 今回はこちらを利用させて頂いた。

Raspberry Piでradikoの再生、録音 - Muchuu
Raspberry Piradikoの再生、録音 - Muchuu...

プログラム

juliusが音声認識を行ってくれるが、それを受けるプログラムを作ることにする。 PHPで書きたかったが情報があまりなかったのでpythonを利用することにした。 ただし僕はpythonを書けないので何人かの方のサンプルを組合させていただいた。 (ちなみにシシマルとは昔飼ってた犬の名前。日常生活もしくはラジオ・テレビから発せられることはあまりないと思い、誤認識はしないと思ったから) 流れ

  1. pythonからjuliusを起動させる。
  2. シシマルという言葉を認識すると待機状態に入る(Hey SiriやAlexaのようなこと)
  3. 待機状態で辞書に登録された言葉を認識すると各々の動作を行う(今回はラジオを再生する)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import socket
import requests
import re
import subprocess
import shlex
import time
import threading

julius_path = '/home/pi/src/julius-master/julius/julius'
jconf_path = '/home/pi/julius-kits/dictation-kit-v4.4/sample.jconf'
julius = None
julius_socket = None


def invoke_julius():
    print 'INFO : invoke julius'
    args = julius_path + ' -C ' + jconf_path + ' -module '
    p = subprocess.Popen(
            shlex.split(args),
            stdin=None,
            stdout=None,
            stderr=None
        )
    print 'INFO : invoke julius complete.'
    print 'INFO : wait 2 seconds.'
    time.sleep(3.0)
    print 'INFO : invoke julius complete'
    return p


def kill_julius(julius):
    print 'INFO : terminate julius'
    julius.kill()
    while julius.poll() is None:
        print 'INFO : wait for 0.1 sec julius\' termination'
        time.sleep(0.1)
    print 'INFO : terminate julius complete'


def get_OS_PID(process):
    psef = 'ps -ef | grep ' + process + ' | grep -ve grep -vie python |head -1|awk \'{print($2)}\''
    if sys.version_info.major == 3:
        PID = str(subprocess.check_output(psef, shell=True), encoding='utf-8').rstrip ()
    else:
        PID = subprocess.check_output(psef, shell=True).rstrip ()
    return PID


def create_socket():
    print 'INFO : create a socket to connect julius'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('localhost', 10500))
    print 'INFO : create a socket to connect julius complete'
    return s


def delete_socket(s):
    print 'INFO : delete a socket'
    s.close()
    print 'INFO : delete a socket complete'
    return True


def invoke_julius_set():
    julius = invoke_julius()
    julius_socket = create_socket()
    sf = julius_socket.makefile('rb')
    return (julius, julius_socket, sf)

STATUS_WAITING   = 0
STATUS_READY     = 1
STATUS_EXECUTING = 2
STATUS_FINISH    = 3
 
status = STATUS_WAITING
 
def status_change(new_status):
  global status
  if status == STATUS_WAITING:
    if new_status == STATUS_READY:
      status = new_status
      print "Status is READY." 
      threading.Timer(15, status_change, args=[STATUS_WAITING]).start()
  elif status == STATUS_READY:
    if new_status == STATUS_WAITING:
      status = new_status
      print "Status is WAITING." 
    elif new_status == STATUS_EXECUTING:
      status = new_status
      print "Status is EXECUTING." 
      threading.Timer(30, status_change, args=[STATUS_FINISH]).start()
  elif status == STATUS_EXECUTING:
    if new_status == STATUS_WAITING:
      print "Command is executed now, so I do NOT change status."
    if new_status == STATUS_FINISH:
      status = STATUS_WAITING
      print "Status is WAITING." 

      
def play_mp3(sound_file):
    args =  "mplayer --quiet " + sound_file
    print args
    p = subprocess.Popen(
            shlex.split(args),
            stdin=None,
            stdout=None,
            stderr=None
        )
    time.sleep(0.5)
    return True

def call_jsay(text):
    cmd = "/usr/local/bin/jsay %s" % text
    subprocess.call(cmd, shell=True)

def radio_on(channel):
    cmd ="/home/pi/radiko/radiko.sh -p %s >/dev/null 2>&1 &" %channel
    subprocess.call(cmd, shell=True)

def nhk_on(channel):
    cmd ="/home/pi/radiko/nhk.sh %s >/dev/null 2>&1 &" %channel
    subprocess.call(cmd, shell=True)


def radio_off():
    # off
    cmd ="killall mplayer"
    subprocess.call(cmd, shell=True)

def main():
    global julius
    global julius_socket
    julius, julius_socket, sf = invoke_julius_set()

    while True:
        if julius.poll() is not None:   # means , julius dead
            print 'ERROR : julius dead'
            delete_socket(julius_socket)
            julius, julius_socket, sf = invoke_julius_set()
        else:
            line = sf.readline().decode('utf-8')
            if line.find('WHYPO') != -1:
               if line.find(u'シシマル') != -1:
                     if status == STATUS_WAITING:
                           print "Julius is ready to your command."
                           status_change(STATUS_READY)
                           play_mp3("/home/pi/julius_sample/sound/ok.mp3") 
                           print "Shishimaru is wating."
               elif line.find(u'TBS') != -1:
                        if status == STATUS_READY:
                           call_jsay('ティービーエスを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('TBS')
                           print "Call TBS"
               elif line.find(u'INT') != -1:
                        if status == STATUS_READY:
                           call_jsay('インターエフエムを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('INT')
                           print "Call INTERFM"
               elif line.find(u'JFM') != -1:
                        if status == STATUS_READY:
                           call_jsay('ジェーウェーブを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('FMJ')
                           print "Call JWAVE"
               elif line.find(u'NHK') != -1:
                        if status == STATUS_READY:
                           call_jsay('エヌエチケーを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           nhk_on('fm')
                           print "Call NHK"
               elif line.find(u'消して') != -1:
                        if status == STATUS_READY:
                           call_jsay('ラジオを消すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_off()
                           print "Call Radio Off"
               
    print 'WARN : while loop breaked'
    print 'INFO : exit'


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print 'Interrupted. Exit sequence start..'
        kill_julius(julius)
        delete_socket(julius_socket)
        print 'INFO : Exit sequence done.'
        sys.exit(0)()

動作確認

$ python sample_listner.py

再生

思った通りに動作しているようだ。 シシマルを認識したらポーンという音を鳴らすことにした。わかりやすいから。

停止

停止には言葉ではなく以前ハックしたAmazon Dash Buttonを利用している。 理由はスピーカーの横にマイクを置いているため言葉を認識してくれなかったからだ。 IMG_0482.JPG

念のため停止するスクリプトAmazon Dash Buttonが押されたら、killall mplayerしてるだけ。

var dash_button = require('node-dash-button');                                                                                                                 
var dash = dash_button("xx:xx:xx:xx:xx:xx", null, null, 'all'); //address from step above                                                                      
dash.on("detected", function (){                                                                                                                               
var exec = require('child_process').exec
, cmd;
 
cmd = 'killall mplayer';
 
killmplayer = function() {
    return exec(cmd, {timeout: 1000},
        function(error, stdout, stderr) {
            console.log('stdout: '+(stdout||'none'));
            console.log('stderr: '+(stderr||'none'));
            if(error !== null) {
                console.log('exec error: '+error);
            }
        }
    )
};
 

認識していないパターン

以上で無事オレオレAmazonEchoが作れた。 あとはこれを昨日覚えたsupervisorでデーモン化すれば完了。

ラズパイの使い方としては満足なものができた。