Raspberry Pi でオレオレAmazonEcho(もしくはGoogleHomeもしくはSiri)を作ってみた
Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita
Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita... |
こちらで書いた記事は有力な情報が見つからず相変わらず同じ状態。 その状況を打破すべく、オレオレAmazonEchoを作ってみることにした。
オレオレAmazonEchoにやらせたいこと
とにかくラジオが好きだ。よく聞いている。朝はTBSのスタンバイを聞くことが最近の日課だ。 6:42から始まる歌のない歌謡曲は最高だ。 しかし寝起きにそのあたりに転がっているiPhoneを取り出し、radikoアプリを起動し、TBSを選択し、Airplayを設定するのはとてもめんどくさい。 となればラズパイをradikoサーバにでもしてcronで自動起動させればよいじゃないかと思うのだが、自動は嫌だ。
と言う事で話しかければradikoが鳴ることを最終目標にオレオレAmazonEchoを作ることにした。
環境
- Raspberry pi (Raspbian)
- git, python, node.js
- USBマイク
raspberry piでサウンド関係はちょっとめんどくさいので事前に調べて設定しておく。
音声認識
LINEBot SDK+Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita
LINEBot SDK+Microsoft 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 piでradiko再生を行える環境を整える。 今回はこちらを利用させて頂いた。
Raspberry Piでradikoの再生、録音 - Muchuu
Raspberry Piでradikoの再生、録音 - Muchuu... |
プログラム
juliusが音声認識を行ってくれるが、それを受けるプログラムを作ることにする。 PHPで書きたかったが情報があまりなかったのでpythonを利用することにした。 ただし僕はpythonを書けないので何人かの方のサンプルを組合させていただいた。 (ちなみにシシマルとは昔飼ってた犬の名前。日常生活もしくはラジオ・テレビから発せられることはあまりないと思い、誤認識はしないと思ったから) 流れ
- pythonからjuliusを起動させる。
- シシマルという言葉を認識すると待機状態に入る(Hey SiriやAlexaのようなこと)
- 待機状態で辞書に登録された言葉を認識すると各々の動作を行う(今回はラジオを再生する)
#!/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
再生
— momochan_qiita (@momochan_qiita) 2017年1月13日
思った通りに動作しているようだ。 シシマルを認識したらポーンという音を鳴らすことにした。わかりやすいから。
停止
— momochan_qiita (@momochan_qiita) 2017年1月13日
停止には言葉ではなく以前ハックしたAmazon Dash Buttonを利用している。 理由はスピーカーの横にマイクを置いているため言葉を認識してくれなかったからだ。
念のため停止するスクリプト。 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); } } ) };
認識していないパターン
— momochan_qiita (@momochan_qiita) 2017年1月13日
以上で無事オレオレAmazonEchoが作れた。 あとはこれを昨日覚えたsupervisorでデーモン化すれば完了。
ラズパイの使い方としては満足なものができた。