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

<TABLE ALIGN=CENTER><TR><TD WIDTH=400px>
 <A HREF="http://qiita.com/momochan/items/0531d6cd7c1fe4546853" TARGET="_blank"><STRONG>Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita</STRONG></A>
 <A HREF="http://b.hatena.ne.jp/entry/http://qiita.com/momochan/items/0531d6cd7c1fe4546853" TARGET="_blank">
 <IMG BORDER="0" SRC="http://b.hatena.ne.jp/entry/image/http://qiita.com/momochan/items/0531d6cd7c1fe4546853" BORDER="0" ALT="" /></a>
 <BR>
 Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita... 
 </TD>
 <TD WIDTH=150px>
 <IMG STYLE="float:left;margin-right:.5em;" SRC="http://capture.heartrails.com/120x120/cool?http://qiita.com/momochan/items/0531d6cd7c1fe4546853" ALT="" WIDTH="120" HEIGHT="120" />
 </TD></TR>
</TABLE>
こちらで書いた記事は有力な情報が見つからず相変わらず同じ状態。
その状況を打破すべく、オレオレAmazonEchoを作ってみることにした。

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

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

# 環境

- Raspberry pi (Raspbian)
- git, python, node.js
- USBマイク

raspberry piでサウンド関係はちょっとめんどくさいので事前に調べて設定していただきたい。
ここがわかりやすいと思う。
<TABLE ALIGN=CENTER><TR><TD WIDTH=400px>
 <A HREF="http://qiita.com/linyixian/items/147d8476d808c84a09e0" TARGET="_blank"><STRONG>Raspbian(Jessie) のサウンド設定 - Qiita</STRONG></A>
 <A HREF="http://b.hatena.ne.jp/entry/http://qiita.com/linyixian/items/147d8476d808c84a09e0" TARGET="_blank">
 <IMG BORDER="0" SRC="http://b.hatena.ne.jp/entry/image/http://qiita.com/linyixian/items/147d8476d808c84a09e0" BORDER="0" ALT="" /></a>
 <BR>
 Raspbian(Jessie) のサウンド設定 - Qiita... 
 </TD>
 <TD WIDTH=150px>
 <IMG STYLE="float:left;margin-right:.5em;" SRC="http://capture.heartrails.com/120x120/cool?http://qiita.com/linyixian/items/147d8476d808c84a09e0" ALT="" WIDTH="120" HEIGHT="120" />
 </TD></TR>
</TABLE>
 

# 音声認識
<TABLE ALIGN=CENTER><TR><TD WIDTH=400px>
 <A HREF="http://qiita.com/momochan/items/1f83db7757a0d68f674b" TARGET="_blank"><STRONG>LINEBot SDK＋Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita</STRONG></A>
 <A HREF="http://b.hatena.ne.jp/entry/http://qiita.com/momochan/items/1f83db7757a0d68f674b" TARGET="_blank">
 <IMG BORDER="0" SRC="http://b.hatena.ne.jp/entry/image/http://qiita.com/momochan/items/1f83db7757a0d68f674b" BORDER="0" ALT="" /></a>
 <BR>
 LINEBot SDK＋Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita... 
 </TD>
 <TD WIDTH=150px>
 <IMG STYLE="float:left;margin-right:.5em;" SRC="http://capture.heartrails.com/120x120/cool?http://qiita.com/momochan/items/1f83db7757a0d68f674b" ALT="" WIDTH="120" HEIGHT="120" />
 </TD></TR>
</TABLE>
音声認識にはMicosoft Speech APIを利用しようかと思ったけれども、ローカルで動く [Julius](http://julius.osdn.jp/)というものを知ったので利用する事とした。

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

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

### ディクテーションキットとディクショナリーキットのインストール
```bash
$ 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'
```

そのまま使ってもよいが自分で辞書を作成するとそこに記載の言葉しか認識しないという仕様なので、今回の要件にはちょうど良いと思ったので辞書を作ることにした。
### 辞書の作成

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

### 辞書の変換

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

### julius conf作成

```bash
$ 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をモジュールモードで起動

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

# radiko

無事辞書にある言葉が認識したらraspberry piでradiko再生を行える環境を整える。
今回はこちらを利用させて頂いた。
<TABLE ALIGN=CENTER><TR><TD WIDTH=400px>
 <A HREF="http://blog.muchuu.net/post/110867859055/raspberry-pi%E3%81%A7radiko%E3%81%AE%E5%86%8D%E7%94%9F%E9%8C%B2%E9%9F%B3" TARGET="_blank"><STRONG>Raspberry Piでradikoの再生、録音 - Muchuu</STRONG></A>
 <A HREF="http://b.hatena.ne.jp/entry/http://blog.muchuu.net/post/110867859055/raspberry-pi%E3%81%A7radiko%E3%81%AE%E5%86%8D%E7%94%9F%E9%8C%B2%E9%9F%B3" TARGET="_blank">
 <IMG BORDER="0" SRC="http://b.hatena.ne.jp/entry/image/http://blog.muchuu.net/post/110867859055/raspberry-pi%E3%81%A7radiko%E3%81%AE%E5%86%8D%E7%94%9F%E9%8C%B2%E9%9F%B3" BORDER="0" ALT="" /></a>
 <BR>
 Raspberry Piでradikoの再生、録音 - Muchuu... 
 </TD>
 <TD WIDTH=150px>
 <IMG STYLE="float:left;margin-right:.5em;" SRC="http://capture.heartrails.com/120x120/cool?http://blog.muchuu.net/post/110867859055/raspberry-pi%E3%81%A7radiko%E3%81%AE%E5%86%8D%E7%94%9F%E9%8C%B2%E9%9F%B3" ALT="" WIDTH="120" HEIGHT="120" />
 </TD></TR>
</TABLE>

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

1. pythonからjuliusを起動させる。
- シシマルという言葉を認識すると待機状態に入る（Hey SiriやAlexaのようなこと）
- 待機状態で辞書に登録された言葉を認識すると各々の動作を行う（今回はラジオを再生する）


```python:sample_listner.py
#!/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)()
```

# 動作確認

```bash
$ python sample_listner.py
```

### 再生
<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr"><a href="https://t.co/0sMy6wdWYG">pic.twitter.com/0sMy6wdWYG</a></p>&mdash; momochan_qiita (@momochan_qiita) <a href="https://twitter.com/momochan_qiita/status/820046015055003648">2017年1月13日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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

### 停止
<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr"><a href="https://t.co/Tszh6WZE2j">pic.twitter.com/Tszh6WZE2j</a></p>&mdash; momochan_qiita (@momochan_qiita) <a href="https://twitter.com/momochan_qiita/status/820046427799683073">2017年1月13日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

停止には言葉ではなく以前ハックしたAmazon Dash Buttonを利用している。
理由はスピーカーの横にマイクを置いているため言葉を認識してくれなかったからだ。
![IMG_0482.JPG](https://qiita-image-store.s3.amazonaws.com/0/120983/4316a16e-dc72-82ef-69fe-d0fbfb68708b.jpeg)

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

```node:app.js
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);
            }
        }
    )
};
 
```

### 認識していないパターン
<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr"><a href="https://t.co/3TlEFVUUuw">pic.twitter.com/3TlEFVUUuw</a></p>&mdash; momochan_qiita (@momochan_qiita) <a href="https://twitter.com/momochan_qiita/status/820046529306116096">2017年1月13日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

# 追記
ラジオを聴いているときの誤認識が激しい。
誤認識するぐらいなら良いのだが二重にラジオが再生されてしまう。
ということで後日改良したいと思う。