2019年にSNIPSはSonosという会社に買収され、本記事の内容は実施できなくなっています。
過去にこういう環境があったという参考として記事は残してあります。
無料かつオフラインでの音声認識Juliusを以下の記事で紹介していますので、ご興味をお持ちの方は御覧ください。
Raspberry Pi+Juliusで音声認識
はじめに
ネットにつながず、ラズパイなどのエッジデバイスで音声認識ができるSNIPSを知り、試してみました。ハマりやすいポイントもありましたので、メモとして残します。
概要
ラズパイに搭載したSNIPSに音声認識をさせて外部機器を動かせたら様々ものに利用できると考え、試してみました。
今回作成したのは声で操作する計算機で、SNIPSの起動ワード「Hey SNIPS!」に続き、「2足す3は?」と尋ねると「5」と答えてくれて、「5」というデータをSPI接続したGR-CITRUSに送ってデジタル表示器で「5」を表示してくれるというものです。
使用したもの
最低限、ラズパイとマイク、スピーカがあればSNIPSを試せますが、今回はラズパイで音声認識させて何かを動かすことを目的としたので、動かす対象としてがじぇるねのGR-CITRUSを使用しました。
- Raspberry Pi 3 Model B
- USB MINI SPEAKER(ダイソーで300円)
- ミニUSBマイク(秋月で380円)
- GR-CITRUS
- 7セグメント表示器 A-551SRD(秋月で40円)
- 150Ω抵抗 × 8個
ラズパイはPi3が推奨されています。Pi Zeroは2018年6月現在、動作しませんでしたのでご注意ください。当初、Pi Zeroでトライしていましたがうまく行かず、SNIPSコミュニティに質問して、Pi ZeroのARMv6アーキテクチャには未対応であることがわかりました。
以下が回路図。
ラズパイの準備
SNIPSの推奨OSはコマンドライン環境のRaspbian Liteとなっているので、Raspbianのダウンロードサイトから”RASPBIAN STRETCH LITE”をダウンロードします。2018年6月末現在の最新は以下でした。
2018-06-27-raspbian-stretch-lite.zip
上記zipファイルを7zip等で展開すると以下のDISKイメージファイルができます。
2018-06-27-raspbian-stretch-lite.img
次にSDカードをSD Formatter等でフォーマットします。このときオプション設定がデフォルトの「クイックフォーマット」ではうまく行かない場合があるようなので、「上書きフォーマット」を選択してフォーマットします。
SDカードのフォーマットが終わったら、一度SDカードを抜いて差し直した方が良いようです。
最初にダウンロードしたRaspbianのDISKイメージをWin32 Disk Imager等を使用してSDカードに書き込みます。オプションはデフォルト状態で問題ありませんでした。
その後、「boot」という名前になっているSDカードのドライブの直下(config.txtなどが見える場所)に拡張子無し「ssh」という名称の空ファイルを置きます。これはラズパイをWifiにつないでパソコンからssh接続する際にこの処置が必要になります。ssh接続しない場合は不要です。
メモ帳などでsshという空ファイルを作った場合は「ssh.txt」となるので、ファイル名を編集して「ssh」に直します。
エクスプローラ等でSDカードにアクセスする際、「ドライブF:をフォーマットする必要があります」のようなポップアップが出ても無視してキャンセルします。
SDカードをラズパイに差し込み、キーボードとディスプレイをつないでラズパイの電源を入れ、デフォルトIDのpi(PW: raspberry)でログインし、パスワードの変更やLocale、Wifi等の設定をします。設定が終わったら再起動します。
$ sudo raspi-config $ sudo reboot
TeraTerm等を使ってssh接続する場合は、ifconfigでIPアドレスを確認してシャットダウンします。キーボードやディスプレイが不要な場合は外します。(HDMIディスプレイから音を出す場合はディスプレイは接続しておきます。)
$ ifconfig $ sudo shutdown -h now
ラズパイに直接ログインすると画面は1つですが、ssh接続なら複数画面を立ち上げられるのでお勧めです。
SNIPSのインストール
ラズパイ向けインストールのページの「Step 3: Install the Snips Platform」から実施します。(Step1~2は上記で設定が完了しているため)
$ sudo apt-get update $ sudo apt-get upgrade $ sudo apt-get install -y dirmngr $ sudo bash -c 'echo "deb https://raspbian.snips.ai/$(lsb_release -cs) stable main" > /etc/apt/sources.list.d/snips.list' $ sudo apt-key adv --keyserver pgp.mit.edu --recv-keys D4F50CDCA10A2849 $ sudo apt-get update $ sudo apt-get install -y snips-platform-voice
次に以下のコマンドでスピーカとマイクのcard番号とデバイス番号を確認し、viやnano等のテキストエディタで/etc/asound.confに設定します。
$ aplay -l $ arecord -l $ sudo vi /etc/asound.conf
私の場合は例と同じだったので/etc/asound.confの中身は以下にしました。
pcm.!default { type asym playback.pcm { type plug slave.pcm "hw:0,0" } capture.pcm { type plug slave.pcm "hw:1,0" } }
ここでスピーカとマイクのテストをします。このときsnips-audio-serverを一時的に止める必要があります。
$ sudo systemctl stop snips-audio-server $ arecord -f cd out.wav
ここで何かしゃべってCtrl+Cで止めます。
$ aplay out.wav
これで録音した音声が正常に聞ければOKです。忘れずにsnips-audio-serverを開始しておきます。
$ sudo systemctl start snips-audio-server
alsamixerでオーディオデバイスの設定を変えることができます。私の場合はスピーカの音量が小さかったので最大にしました。
$ alsamixer
続いてsnips-watch、npm、nodejs、snips-samをインストールしていきます。
$ sudo apt-get install snips-watch $ sudo apt-get update $ sudo apt-get upgrade $ sudo apt-get install -y nodejs npm $ sudo npm cache clean $ sudo npm install npm n -g $ sudo n stable $ sudo npm install -g snips-sam
途中、以下のようなWARNINGメッセージが出ましたが、問題ないようです。
/usr/local/bin/n -> /usr/local/lib/node_modules/n/bin/n npm WARN package.json path-is-inside@1.0.2 No README data npm WARN package.json sorted-object@2.0.1 No README data /usr/local/bin/npm -> /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx -> /usr/local/lib/node_modules/npm/bin/npx-cli.js npm WARN package.json config-chain@1.1.11 No license field. npm WARN package.json qrcode-terminal@0.12.0 No license field. n@2.1.12 /usr/local/lib/node_modules/n npm@6.1.0 /usr/local/lib/node_modules/npm
音声解釈機能の作成
Web上のSNIPSの開発ページから音声解釈機能を作成していきます。初めての場合、Emailとパスワードを登録してアカウントを作ります。
ログインしたら「Create a New Assistant」を押し、アシスタント名と使用言語を選択してアシスタントを作成します。この例では日本語を選択しています。(2018年6月現在、日本語の解釈のみ対応しており、日本語の発声はできません。)
続いて「Add a skill」→「Create a New Skill」でSkill名と説明を書いて「Create」を押します。
今作ったSkillをクリックして「Edit Skill」を押します。
「Create a New Intent」を押してIntentを作成します。
Intentの名前と説明を書いておきます。
以下は足し算をするSkillのSlotsとTraining Exampleの例です。
Slotsには数値や物の名称など、切り出してプログラムに渡して処理を行いたいものを定義します。足し算の場合、足される数と足す数がSlotになります。
今回は2つのSlotを数値として取り出したいため、TYPEはsnips/numberを選択します。
Training Exampleには想定する入力フレーズを書き、Slotに相当する部分を選択して右クリックでSlotを割り当てます。
右の方にテスト用のツールがあり、音声入力に対する解釈をJSON形式のデータとして見ることができるので、青いマイクのボタンを押して話しかけるか、文字列入力をして正しくSlotに値が入るか確認します。
入力フレーズ内の数値は1のみで表現していますが、Slotにすることで他の数値でも認識してくれることがわかります。
問題なければ「Save」を押します。
上記は足し算の例ですが、同様に引き算、掛け算、割り算も作成しました。
次にActionを作成します。画面上部に
Home > アシスタント名 > Skill名 > Intent名
と表示されている箇所のSkill名(私の例ではCalculator)をクリックし、「Actions」を押します。
Action Typeとして4つの選択肢がありますが、ここでは自由にPythonプログラムを書ける「Code Snippets」について説明します。
デフォルトではコメントのみ記入されているテキスト入力欄に以下のようにPythonプログラムを書き込みます。
以下は割り算の例です。
#import RPi.GPIO as GPIO import serial from time import sleep #GPIO.setwarnings(False) #GPIO.setmode(GPIO.BOARD) #GPIO.setup(12, GPIO.OUT) ser = serial.Serial("/dev/ttyACM0", baudrate = 9600, timeout = 2) ans = 0 if len(intentMessage.slots.first_number) > 0 and len(intentMessage.slots.second_number) > 0: ans = intentMessage.slots.first_number.first().value / intentMessage.slots.second_number.first().value if ans == int(ans): ans = int(ans); print("ans:", ans) result_sentence = "The answer is {}.".format(str(ans)) print("say:", result_sentence) ser.write(str(ans)) #if ans > 9: # ans = 9 #for i in range(ans): # GPIO.output(12, GPIO.HIGH) # sleep(0.15) # GPIO.output(12, GPIO.LOW) # sleep(0.15) else: result_sentence = "Sorry, I didn\'t understand." ser.write("E") current_session_id = intentMessage.session_id hermes.publish_end_session(current_session_id, result_sentence)
Slotとして定義したfirst_numberとsecond_numberが空でなければ割り算をして答えをansに格納し、SNIPSに話させるための文字列result_sentenceを作り、hermes.publish_end_session()に渡します。
ansは割り切れて整数になったときでも「5.0」のように少数となるのが嫌だったのでint()で整数にしています。
#から始まるコメント部分はGPIOを動かしてLEDを点灯させる場合のメモです。
うまくSlotを取り出せなかった場合は、聞き取れなかったというコメントと共にシリアルポートにはEの文字を返すようにしています。
2018年6月現在、日本語での発話はできないため、話す内容は英語で書いています。
なお、ここでは書き込んだCode Snippetのデバッグはできず、またPythonのフォーマットとして不正であっても何もチェックしてくれません。デバッグはラズパイにインポートしてから行うことになります。
音声解釈機能のインストール
上記で作成した音声解釈機能をsamというSNIPSを操作するための便利ツールを使ってラズパイにインストール(Deploy)します。
$ sam login
ここでSNIPS開発ページのログインEmailとパスワードを使います。次にsamをラズパイに接続します。マシン名は特に変更していなければ、raspberrypiになっているはずで、それに.localを付けて以下のようにします。
$ sam connect raspberrypi.local
環境によってはraspberrypi.localの部分をIPアドレスにしないとつながらないかもしれません。その場合はifconfigでラズパイのIPアドレスを調べて使用します。
いよいよAssistantをインストールします。
$ sam install assistant $ sam install actions
複数Assistantを作成した場合はここで選択可能な状態になりますので、矢印キーでインストールしたいAssitantを選びEnterを押します。
SNIPSからGPIOやSerial通信を利用するために以下の手順でvenv上のPythonにrpi.gpio、pyserialをインストールする必要があります。
$ sudo usermod -a -G spi,gpio _snips-skills $ source /var/lib/snips/skills/ユーザ名.Skill名/venv/bin/activate $ pip install rpi.gpio
$ pip install pyserial $ deactivate
actionは_snips-skillsユーザで実行されるため、シリアルポートにアクセスできる権限を付けます。
$ sudo usermod -a -G dialout _snips-skills
シリアルポートを有効にします。以下はssh通信でラズパイを操作し、ログインにシリアルポートを使用せず、GR-CITRUSとの通信として使用する場合の設定を示します。
$ sudo raspi-config
さて、actionのインストール後は/var/lib/snips/skills/ユーザ名.Skill名/action-*のファイル内action_wrapperメソッドにCode Snippetが入っているのがわかります。
なお、SNIPSのコミュニティによると、一旦ラズパイにインストールした後は、Web上のCode Snippetではなく、このファイルを直に修正するのがお勧めとのこと。Web上のCode Snippetではデバッグもできないため、初期環境立ち上げの際に必要となる仮プログラムとして捉えれば良いようです。
Actionを直に修正したら以下のようにsnips-skill-serverを再起動します。以下のようにsnips-skill-serverのログを表示させるとprint等で表示した内容も確認でき、デバッグに役立ちます。
$ sam service restart snips-skill-server $ sam service log snips-skill-server
また、以下sam watchも音声がどのように解釈されたかを確認することができるので、上記ログと別のWindowを立ち上げて実行しておくのがお勧めです。
$ sam watch
ここまで来たら、いよいよラズパイにつなげたマイクに向かって話しかけます。「Hey SNIPS!」、「3たす4は?」のように話しかけてみてください。「Hey SNIPS!」がうまく認識されると「ピコーン」という音が鳴ります。
うまくいかない場合は、snips-skill-serverのログやsam watchの内容を確認します。
sam statusも状況を知るのに役立ちます。問題がなければ以下のように表示されます。
$ sam status Connected to device raspberrypi.local OS version ................... Raspbian GNU/Linux 9 (stretch) Installed assistant .......... JP_SNIPS Language ..................... ja Hotword ...................... hey_snips ASR engine ................... snips Status ....................... Live Service status: snips-analytics .............. 0.56.4 (running) snips-asr .................... 0.56.4 (running) snips-audio-server ........... 0.56.4 (running) snips-dialogue ............... 0.56.4 (running) snips-hotword ................ 0.56.4 (running) snips-nlu .................... 0.56.4 (running) snips-skill-server ........... 0.56.4 (running) snips-tts .................... 0.56.4 (running)
また、/var/log/syslogにも有用な情報が出力されます。
$ tail -f /var/log/syslog
Actionをインストールし忘れたり、Actionの内容に問題があったりするとsnips-skill-serverが停止と開始を繰り返すことがあり、そのような状況はこのファイルで確認できます。
GR-CITRUSのプログラミング
ラズパイからシリアル通信で送信した数値を表す文字列をGR-CITRUSで受け取り、7セグメント表示器にデジタル表示するプログラム例です。
マイナスや小数点、エラー時のEを表示できるようにしました。
電源投入直後はtest_digit()で全文字列を順に表示します。入力がないときは∞の字をなぞるような動きをして入力待ちであることを表現しています。
これで概要にある動画のように答えを英語で話してくれると共にデジタル表示されるようになります。
const int WAIT = 500; const int CLEAR_WAIT = 50; const int MOTION_WAIT = 100; const int leftBottom = 9; const int bottom = 8; const int rightBottom = 7; const int dot = 6; const int center = 10; const int leftTop = 11; const int top = 12; const int rightTop = 13; char inByte = '.'; int motionCnt = 0; int motionPtn[9] = {8, 7, 10, 11, 12, 13, 10, 9, 8}; void setup() { Serial.begin(9600); pinMode(leftBottom, OUTPUT); pinMode(bottom, OUTPUT); pinMode(rightBottom, OUTPUT); pinMode(dot, OUTPUT); pinMode(center, OUTPUT); pinMode(leftTop, OUTPUT); pinMode(top, OUTPUT); pinMode(rightTop, OUTPUT); test_digit(); clear_digit(); } void loop() { if (Serial.available() > 0) { inByte = Serial.read(); Serial.write(inByte); Serial.write('\n'); digit(inByte); motionCnt = -1; }else{ motion(); } } void motion(){ if(motionCnt == -1){ delay(WAIT * 5); motionCnt = 0; } clear_digit(); digitalWrite(motionPtn[motionCnt], LOW); digitalWrite(motionPtn[motionCnt + 1], LOW); delay(MOTION_WAIT); motionCnt++; if(motionCnt == 8) motionCnt = 0; } void clear_digit() { digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, HIGH); digitalWrite(dot, HIGH); digitalWrite(center, HIGH); digitalWrite(leftTop, HIGH); digitalWrite(top, HIGH); digitalWrite(rightTop, HIGH); } void digit(char num) { clear_digit(); delay(CLEAR_WAIT); if(num == '0'){ digitalWrite(leftBottom, LOW); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, HIGH); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '1'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, HIGH); digitalWrite(leftTop, HIGH); digitalWrite(top, HIGH); digitalWrite(rightTop, LOW); }else if(num == '2'){ digitalWrite(leftBottom, LOW); digitalWrite(bottom, LOW); digitalWrite(rightBottom, HIGH); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, HIGH); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '3'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, HIGH); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '4'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, HIGH); digitalWrite(rightTop, LOW); }else if(num == '5'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, HIGH); }else if(num == '6'){ digitalWrite(leftBottom, LOW); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, HIGH); }else if(num == '7'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, HIGH); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '8'){ digitalWrite(leftBottom, LOW); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '9'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, LOW); digitalWrite(rightBottom, LOW); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, LOW); }else if(num == '-'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, HIGH); digitalWrite(dot, HIGH); digitalWrite(center, LOW); digitalWrite(leftTop, HIGH); digitalWrite(top, HIGH); digitalWrite(rightTop, HIGH); }else if(num == '.'){ digitalWrite(leftBottom, HIGH); digitalWrite(bottom, HIGH); digitalWrite(rightBottom, HIGH); digitalWrite(dot, LOW); digitalWrite(center, HIGH); digitalWrite(leftTop, HIGH); digitalWrite(top, HIGH); digitalWrite(rightTop, HIGH); }else if(num == 'E'){ digitalWrite(leftBottom, LOW); digitalWrite(bottom, LOW); digitalWrite(rightBottom, HIGH); digitalWrite(dot, LOW); digitalWrite(center, LOW); digitalWrite(leftTop, LOW); digitalWrite(top, LOW); digitalWrite(rightTop, HIGH); }else{ clear_digit(); } delay(WAIT); } void test_digit() { digit('0'); digit('1'); digit('2'); digit('3'); digit('4'); digit('5'); digit('6'); digit('7'); digit('8'); digit('9'); digit('-'); digit('.'); digit('E'); }
おわりに
ここまでの機能を実現するのに何度もハマりましたが、その度、SNIPSのコミュニティで質問すると真摯に回答してくれて解決することができ、感謝しています。大変有用なコミュニティなのでSNIPSで何か問題が起きたら利用することをお勧めします。
なお、他の無料で使える音声認識ツールJuliusについても試しましたので参考にしてください。
Raspberry Pi+Juliusで音声認識
参考文献
- SNIPS (https://snips.ai/)
- SNIPSコミュニティ (https://discordapp.com/invite/3939Kqx)
- GR-CITRUS (http://gadget.renesas.com/ja/product/citrus.html)