今回は前回作成したJuliusのイベントを処理するプログラムを作成します。
まず、Juliusを起動して常駐させますが、最後に-moduleを付けてWEBサービス状態で起動します。
julius -C r2d2mic.jconf -modulejulius -C r2d2mic.jconf -module
Juliusのイベントはlocalhost:10500のWEBサービスとして取得します。
イベントが発生するとレスポンスとして以下のようなXMLを返します。
<RECOGOUT>
<SHYPO RANK="1" SCORE="-2736.358154" GRAM="0">
<WHYPO WORD="[s]" CLASSID="13" PHONE="silB" CM="1.000"/>
<WHYPO WORD="アールツー" CLASSID="0" PHONE="a: r u ts u:" CM="1.000"/>
<WHYPO WORD="[/s]" CLASSID="14" PHONE="silE" CM="1.000"/>
</SHYPO>
</RECOGOUT>
イベントが発生したらXMLパーサーでパースして取得した言葉ごとに処理を行うようにします。
とりあえず「R2」と呼びかけたらR2の返事の音声MP3ファイルを再生して返事するようにします。
ちなみにMP3ファイルは R2D2 Translator からいただきました。
import socket
import xml.etree.ElementTree as et
import pygame
from mutagen.mp3 import MP3 as mp3
import time
def main():
host = 'localhost'
port = 10500
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
try:
data = ''
while True:
# print(data) # 認識した言葉を表示して確認
if '\n.' in data:
root = et.fromstring('\n\n' + data[data.find(''):].replace('\n.', '') + "\n")
for whypo in root.findall('.//SHYPO/WHYPO'):
word = whypo.get('WORD')
print(word)
if word == "アールツー":
try:
filename = 'r2d2_yes.mp3' # 再生したいmp3ファイル
pygame.mixer.init()
pygame.mixer.music.load(filename) # 音源を読み込み
mp3_length = mp3(filename).info.length # 音源の長さ取得
pygame.mixer.music.play(1) # 再生開始。1の部分を変えるとn回再生(その場合は次の行の秒数も×nすること)
time.sleep(mp3_length + 0.25) # 再生開始後、音源の長さだけ待つ(0.25待つのは誤差解消)
pygame.mixer.music.stop() # 音源の長さ待ったら再生停止
except Exception as e:
print(str(e))
data = ''
else:
# dataが空のときjuliusからdataに入れる
data += str(client.recv(1024).decode('utf-8'))
except KeyboardInterrupt:
client.close()
if __name__ == "__main__":
main()
音声入力された処理を画面表示するようにしたいと思います。
今後のことを考えてGUIのプログラムをベースにして、その上に各機能を実装していこうと思います。
GUIフレームワーク上に画面を作成して、その処理内のスレッドでJuliusとの通信機能を実装します。
そうすることでプログラムが立ち上がっていることが見た目で分かるようになり、GUIの終了=サービスの終了とすることができます。
Juliusイベント処理は別の専用の別ファイルで実装する構造にします。
まずは、ベースとなるGUIプログラムを作成します。
以前、GUI用のフレームワークとしてkivyをインストールしたが標準ライブラリのtkinterの方がよさそうなので、そちらを使用します。
import socket
import tkinter as tk
import threading
import xml.etree.ElementTree as et
import pygame
from mutagen.mp3 import MP3 as mp3
import time
import r2d2_julius_event
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.pack()
self.create_widgets()
self.root = master
self.julius_thread = None
def create_widgets(self):
self.julius_thread = threading.Thread(target=self.julius_process)
self.julius_thread.setDaemon(True)
self.julius_thread.start()
self.msg_label = tk.StringVar()
self.msg_label.set("")
self.msg_label_obj = tk.Label(self, textvariable=self.msg_label, width=20, height=10, font=('Helvetica', '24', 'bold'))
self.msg_label_obj.pack()
self.quit = tk.Button(self, text="QUIT", fg="red", command=self.window_terminate)
self.quit.pack(side="bottom")
def window_terminate(self):
self.root.destroy()
def julius_process(self):
host = '192.168.1.15'
port = 10500
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
try:
data = ''
while True:
if '\n.' in data:
root = et.fromstring(
'\n\n'
+ data[data.find(''):].replace('\n.', '')
+ "\n"
)
for whypo in root.findall('.//SHYPO/WHYPO'):
word = whypo.get('WORD')
print(word)
if word in r2d2_julius_event.julius_event_dict:
event_func = r2d2_julius_event.julius_event_dict[word]
mp3_file, disp_msg = event_func(self)
try:
if mp3_file != "":
filename = mp3_file
pygame.mixer.init()
pygame.mixer.music.load(filename) # 音源を読み込み
mp3_length = mp3(filename).info.length # 音源の長さ取得
pygame.mixer.music.play(1) # 再生開始。1の部分を変えるとn回再生(その場合は次の行の秒数も×nすること)
time.sleep(mp3_length + 0.25) # 再生開始後、音源の長さだけ待つ(0.25待つのは誤差解消)
pygame.mixer.music.stop() # 音源の長さ待ったら再生停止
if disp_msg != "":
self.msg_label.set(disp_msg)
except Exception as e:
print(str(e))
data = ''
else:
# dataが空のときjuliusからdataに入れる
data += str(client.recv(1024).decode('utf-8'))
except KeyboardInterrupt:
client.close()
def main():
root = tk.Tk()
app = Application(master=root)
app.mainloop()
if __name__ == "__main__":
main()
Juliusイベント処理は r2d2_julius_event に実装します。
画面にはメッセージ表示ラベルと終了(quit)ボタンのみの単純な構造です。
※tkinterの細かい説明は省きます。
画面の初期化時にJulius処理をスレッドで起動します。
ただし、親スレッド(画面)が終了したらJulius処理も終了するようにします。
r2d2_julius_event 内に、言葉をキー、処理(ファンクション)を値に持った辞書を用意し、Juliusイベントの言葉に紐付いた処理を取得して実行します。
イベント処理は再生するMP3ファイル名、メッセージを返すので、MP3の再生を行い、メッセージを画面に表示しています。
今回作成した r2d2_julius_eventは以下の通り。
import requests
class R2d2JuliusEvent:
kyou_ashita = 0
@staticmethod
def r2(thinter_obj):
return 'r2d2_yes.mp3', "はーい"
@staticmethod
def kyo_no(thinter_obj):
R2d2JuliusEvent.kyou_ashita = 1
return '', ''
@staticmethod
def ashita_no(thinter_obj):
R2d2JuliusEvent.kyou_ashita = 2
return '', ''
@staticmethod
def tenki_ha(thinter_obj):
url = 'http://weather.livedoor.com/forecast/webservice/json/v1'
payload = {'city': '230010'}
data = requests.get(url, params=payload).json()
print("title : " + data['title'])
for weather in data['forecasts']:
if R2d2JuliusEvent.kyou_ashita == 1 and weather['dateLabel'] == "今日":
return 'r2d2_yes.mp3', "今日の\n\n" + data['title'] + "\n\n" + weather['telop']
if R2d2JuliusEvent.kyou_ashita == 2 and weather['dateLabel'] == "明日":
return 'r2d2_yes.mp3', "明日の\n\n" + data['title'] + "\n\n" + weather['telop']
if R2d2JuliusEvent.kyou_ashita == 2 and weather['dateLabel'] == "明後日":
return 'r2d2_yes.mp3', "明後日の\n\n" + data['title'] + "\n\n" + weather['telop']
print("data : " + weather['dateLabel'] + ':' + weather['telop'])
R2d2JuliusEvent.kyou_ashita = 0
return '', ""
julius_event_dict = {
"アールツー": R2d2JuliusEvent.r2,
"今日の": R2d2JuliusEvent.kyo_no,
"明日の": R2d2JuliusEvent.ashita_no,
"天気は": R2d2JuliusEvent.tenki_ha,
}
とりあえず、「R2」と呼びかけたら返事をすることと、今日の天気と明日の天気を表示できるようにしました。
天気の情報はLivedoorのWEBサービスから取得しています。
r2d2.py、r2d2_julius_event.py、r2d2_yes.mp3 を ~/julius-4.4.2/julius-kit/grammar-kit-4.3.1 にコピーして実行します。
GUI上でコマンドプロンプトを2つ起動し、1つはJuliusを、1つはr2d2.pyを実行します。
julius -C r2d2mic.jconf -module
python3 r2d2.py
こんな感じで動きました。
音声認識の精度が悪いのでご認識が多いです。
精度を上げることはできるかもしれませんが、音声でモーター制御などをはしない方がいいかもしれませんね。
今回はラズパイ内部でのみの処理を実装しましたが、今後はラズパイとArduinoの連携や顔認証などを実装して全体のコントロールを実装していく予定です。