気の向くままに辿るIT/ICT/IoT
webzoit.net
IoT・電子工作

Raspberry Pi/ESP8266・ESP32/Julius/Open JTalkでスマートスピーカーを作る

ウェブ造ホーム前へ次へ
サイト内検索
カスタム検索
Raspberry Piって?

Raspberry Pi/ESP8266・ESP32/Julius/Open JTalkでスマートスピーカーを作る

Raspberry Pi/ESP8266・ESP32/Julius/Open JTalkでスマートスピーカーを作る

2018/10/01

 Raspberry Pi、ESP8266/ESP-WROOM-02/ESP-WROOM-32/ESP32、音声認識エンジンJulius、デフォルトでは日本語の音声合成Open JTalkでスマートスピーカーを自作してみるページ。

2018/11/21

 後述のように執筆時点では、サーバとしているもの以外、ラズパイを持っていなかった為、ラズパイ/Raspbianの代わりにPC/Debianで検証した。

 が、いざ、Rapsberry Pi 3 B+を買ってDebian環境からJuliusディレクトリをコピー(scp)して実装してみるとラズパイならでは、Raspbian Jessie/Stretchならでは、ラズパイ3B+ならではの違いがあり、代替・追加作業が必要となった。

 具体的には、以下で他は、同じ。

  1. sudo apt install binutils-arm-none-eabi
  2. sudo apt install osspd-alsa libasound2-dev
    cd julius
    ./configure --with-mictype=alsa
    make
    以後の手順[sudo modprobe snd-pcm-oss]は実行しない
    参考リンク:raspi最新カーネルでjuliusを動かす
  3. julius/mkgshmm/mkgshmmの配置・確認
    make install
  4. 2項に関連し、環境変数ALSADEVのみ設定(AUDIODEVは設定不要)
    ちなみに[/etc/modprobe.d/alsa-base.conf]の順序変更設定も不要
  5. 2項に関連し、aplay *.wav(-Dオプション不要/Open JTalkスクリプト内)
2018/12/07

 マイクでも想定外の状況に遭遇した。

 検証に使った2つあるUSB接続のWebカメラ内蔵マイクは、パソコンと内蔵スピーカーや簡易ヘッドセットのイヤホン、3.5mmミニジャック接続の100均スピーカーでは、音質はクリアに録音・再生でき、ラズパイではこれら組み合わせで僅かにノイズものりつつも気にならないレベルで正常に再生できた。

 だが、オーディオ・サウンドカードとUSBサウンドアダプタ音量調整付きUSBサウンドアダプタにある2つのUSBサウンドアダプタと先のWebカメラに付属の簡易ヘッドセットのマイク、同ヘッドセットのイヤホンや3.5mmミニジャック接続の100均スピーカーとの組み合わせだと、何れもラズパイUSBポートに直挿しでもUSB延長ケーブルを介してもマイク入力時のノイズが激しすぎて話にならなかった。

 また、ラズパイに元々入っている(/usr/share/sound/alsa/の)音源は、簡易ヘッドセットのイヤホンや100均スピーカーをUSBサウンドアダプタに挿しても、ラズパイの3.5mmミニジャックに挿しても音質はクリアなので少なくともUSBサウンドアダプタとスピーカーやイヤホンの組み合わせに起因するものではないと考えてよさそう。

 となるとマイク入力においてラズパイUSBポート付近の何かに影響を受けているようだが、それでもWebカメラ内蔵マイクは十二分に許容範囲内であり、ならば簡易ヘッドセットのマイクが原因かと思いきや、『2つのWebカメラ』のリンク先の流れでDebian/Fedora/NetBSDでの通話テストでは、簡易ヘッドセットはマイク・イヤホンともに、またWebカメラと合わせて正常に使えたので、性能というか、あるとしたらノイズへの耐性の違い...?はたまた、USBサウンドアダプタに起因?

 そもそも、マイクは、別途購入した3.5mmミニプラグのものを、スピーカーもできれば手持ちの100均の3.5mmミニジャックを予定し、USBサウンドアダプタを介すこと前提で想定していたのだが...。

 ただ、マイクが届いてみると4極プラグでパソコンでは完璧に機能したのだが、3.5mmミニジャック(及びプラグ)が先端からLEFT/RIGHT/GND/映像の4極らしきラズパイでは、プラグから分岐しているスピーカー出力用ジャックは機能するも肝心のマイクは残念ながら機能しなかった。

 と思ったら、3極である簡易ヘッドセットのマイクをラズパイの3.5mmミニジャックに挿してみたところ、録音できない...、そもそもラズパイの3.5mmミニジャックは映像とあることもあって、マイク入力に対応していないのか...?

 これらのことからするとラズパイについては、マイクはUSB接続のものが良さげ、とすると、これを別途調達するなり、Webカメラ内蔵マイクを使うなりといった計画変更が必要になりそう。

 尤も100均のスピーカーも、音が小さすぎ、PulseAudioで最大(153%)にしてみたところ、そこそこで音量調節せずに使うならよいかなと思わなくもないが、やはりアンプ?でもかまさないといけないかとは思っている。

 何れもスマホやタブレット、パソコンなどからWiFi操作で壁や家具越しにも操作可能なESPモジュールによる自作IR・赤外線リモコンでリモコン対応家電や自作スマートプラグ(スマートコンセント)に挿した非リモコン家電を日本語の音声でWiFi越しにON/OFF操作できるのは、もちろんのこと、内蔵時計(localtime)、天気APIとも連携し、日付、時刻、今日の天気、明日の天気、明日の(最高/最低)気温も日本語で聞けば教えてくれる、まさにスマートスピーカーは、公開されている情報の組み合わせで比較的簡単にできた。

 Amazon EchoにおけるAlexaの日本語版発売が、2017年11月と考えると、そういう意味では、そこそこ最新っぽい(AI使ってないけど?ん?Juliusで使ってるHMM/Hidden Markov Model/隠れマルコフモデルもAI?)。

 自作IoTガジェットで非IoTな自宅がスマートホームに...。

Echo Plus (エコープラス)、 スマートホームハブ内蔵 - スマートスピーカー with Alexa、ブラック

Echo Spot (エコースポット) - スマートスピーカー with Alexa、ブラック

Echo (エコー) - スマートスピーカー with Alexa、チャコール (ファブリック)

Echo Dot (エコードット) - スマートスピーカー with Alexa、ブラック

【Amazon Alexa認定取得製品】 TP-Link WiFi スマートプラグ 遠隔操作 直差しコンセント Echo シリーズ Googleホーム対応 音声コントロール コンパクト ハブ不要 3年保証 HS105

ラトックシステム スマート家電コントローラ スマホで家電コントロール [Works with Alexa認定製品] RS-WFIREX3

LinkJapan eRemote IoTリモコン 家でも外からでもいつでもスマホで自宅の家電を操作 【Works with Alexa認定製品】

LinkJapan eRemote mini IoTリモコン 家でも外からでもいつでもスマホで自宅の家電を操作【Works with Alexa認定製品】MINI

Nature Remo

スマートリモコン Nature Remo mini【Amazon Echo/Google Home対応】

OHM リモコンセント OCR-05

電機器具専用 リモコンコンセント [品番]07-8251 OCR-05W

 一方、ESPチップ・モジュールでも設定次第で外からも操作可能だし、esp8266-alexa-wemo-emulatorを作って下さった強者もおり、おかげでAlexa定型アクションでも使えるが、ここでは、そのスマートスピーカーまで作ってしまう。

 今回、ESPモジュールについては、スマートリモコンとして赤外線LEDから信号を発信するだけということもあり、ESP-01を使ったが、これでできるということは、上位機種のESP-02...、ESP-12...、ESP-WROOM-02、ESP32などESP-xxでも、これら開発ボードでもできるが、ESPシリーズの内、日本の技適を通っているのは、いまのところ、ESP-WROOM-02/ESP-WROOM-32(とこれらを搭載した開発ボード)のみ。

 ただし、Aliexpress等、海外の通販で買う場合、ESP32でも技適を通っていない(通る前の古い?)ものもあるので技適マークの確認は必要。

Rasbee ESP8266 ESP-01 LWIP AP+STA シリアル WIFI無線モジュール

HiLetgo ESP32 ESP-32S NodeMCU開発ボード2.4GHz WiFi + Bluetoothデュアルモード

waves ESP32-DevKitC ESP-WROOM-32 ESP32 DevKitC V2 WiFi BLE 技適取得済 国内発送

waves ESP-WROOM-32 + 開発基盤 セット 国内発送

waves ESP8266 WiFiモジュール(技適取得済み) ESP-WROOM-02 キット 赤基盤

[スイッチサイエンス] ESP-WROOM-02ピッチ変換済みモジュール《シンプル版》

[スイッチサイエンス] ESP-WROOM-02ピッチ変換済みモジュール《フル版》

HiLetgo 2個セット ESP8285 ESP-M2 CH340開発ボード WIFIシリアルポートモジュール CH340 ESP8266に対応

HiLetgo® 2個セット ESP8266 NodeMCU LUA CP2102 ESP-12E モノのインターネット開発ボード ESP8266 無線受信発信モジュール WIFIモジュール Arduinoに適用 [並行輸入品]

HiLetgo OTA WeMos D1 CH340 WiFi 開発ボード ESP8266 ESP-12F Arduino IDE UNO R3に対応 

Rasbee WeMos D1 R3 簡易デザイン WiFi 開発ボード NODEMCU LUA ESP8266 Arduino UNO 適用 1個 [並行輸入品]

Rasbee OTA WeMos D1 CH340 WiFi 開発ボード ESP8266 ESP-12F For IDE UNO R3 Arduino用 1個 [並行輸入品]

Rasbee アップグレード版 WeMos D1 R2 WiFi UNO 開発板 ESP8266 Arduino 1個 [並行輸入品]

 前後したけど、メインは、Raspberry Pi。

 Raspberry Piのモデルに合わせた出力のUSB充電器、microSDカードやUSBメモリ、microUSB-USB Aケーブルあたりは必要。

 また、スマートスピーカーと言えば、マイクとスピーカーは必須。

 どんなものでも良いが、ただ、ラズパイにオーディオジャックは一つしかなく、スピーカーは、オーディオプラグのみか、USB+オーディオプラグのものしかないだろう、USBオーディオ変換アダプタだとマイクとスピーカーをそれぞれのデバイスとして認識させられなさそうだから、自ずとマイクはUSB接続のものを選ぶことになるか。

 あ、忘れてたけど、USBサウンドアダプタを使えば、マイクとスピーカーがどっちもオーディオジャック出しでも同時にUSBに変換することもできるんだった。

2018/12/07

 冒頭に追記した通り、ラズパイで使う限りにおいては、少なくともマイクはUSB接続のものを調達すべきな模様。

 スマートスピーカーの自由度を上げるなら有線より無線、ラズパイ3B/3B+は内蔵してるけど、ラズパイ2 Bなら無線LANドングル、有線環境はあるけど無線環境自体ないなら有線ルータにつないでアクセスポイントにする用の無線LANルータも。

 あると便利なのは、個別スイッチ付き電源タップとか、電圧・電流チェッカーあたり。

Raspberry Pi2 Model B ボード&ケースセット (Standard, Clear)-Physical Computing Lab

Raspberry Pi 3 Model B+ スターターセット BASIC

Transcend microSDHCカード 4GB Class4 TS4GUSDHC4

Transcend microSDカード 2GB TS2GUSD

シリコンパワー USBメモリ 32GB USB2.0 キャップ式 永久保証 Ultima U02シリーズ ブラック SP032GBUF2U02V1K

Anker 24W 2ポート USB急速充電器 【急速充電 / iPhone&Android対応 / 折畳式プラグ搭載】 (ホワイト)

ELECOM タブレット用USB2.0ケーブル A-microB 2A出力 0.8m ブラック TB-AMB2A08BK

ルートアール USB 簡易電圧・電流チェッカー ストレート型 3.4V~8.0V,0A~3A クリア RT-USBVA2C (Rev.2)

iBUFFALO ツメの折れないLANケーブル UTP Cat6a ストレート フラットタイプ 1m ブラック BSLS6AFU10BK

BUFFALO エアステーション 11n対応 11g/b USB2.0用 無線LAN子機 親機・子機同時モード対応 WLI-UC-GNM2S

ELPA エルパ スイッチ付タップ 6個口 2m WLS-N62EB(W)

エレコム WiFi ルーター 無線LAN WRC-F300NF 11n 300Mbps 1LDK1階建向け 接続推奨3台

iGOKUピンマイク コンピュータ用マイク 無指向性USBミニクリップマイク マイクロフォン PC用マイクMacbook、インタビュー、Skype、オーディオビデオレコーディング、ポッドキャストなど対応 1.5m延長コード付き 一年間品質保証

iBUFFALO PC用スピーカー USB電源 ホワイト BSSP29UWH

前提

 USB充電器、USBケーブル、microSDカードやUSBメモリなどラズパイ一式の他、マイクとスピーカーを用意、これらが使える状態にあること。

 OSもインストール済みの利用可能なラズパイにホスト名.localでアクセスできるようにOSに応じてAvahiかBonjour、音声認識用にJulius、音声合成用にOpen JTalkをインストールや必要に応じてgit cloneかダウンロード・展開し、動作確認してあること。

 と言っても自身は、現在、サーバに使っているもの以外にラズパイを持っていないので、とりあえず、パソコン上でRaspbianのベースでもあるDebian(Linux)で代用したが。

 ラズパイとは別に、ここでは、Arduino IDEを使うのでパソコンにおいてESP8266を使う場合、Arduino IDEの[ツール] => [ボード]から[Generic ESP8266 Module]を選択、ESPにスケッチをアップロードできる状態であること。

 ESP32を使う場合には、Arduino IDEの[ツール] => [ボード]から[espressif/arduino-esp32]を選択、ESPにスケッチをアップロードできる状態であること。

 何れにしてもフラッシュメモリ上にファイルを置きたい場合には、SPIFFSを使う準備をしておく。

 また、スクリプトやプログラム言語は、何でも良いが、ここでは、PerlスクリプトとPython(2.7)スクリプトを使ったので、これらがインストールされていること。

 参考までに自身が今回使用したOSは、Debian Stretch amd64、Arduino IDEのバージョンは、1.8.7。

概算

 基本、ラズパイ一式の価格プラスアルファといった価格構成になると思われ、持ち運びを考慮するとWiFi必須、処理速度からしてRaspberry Pi 2B/3B/3B+が無難、2B+WiFiドングルか、WiFi内蔵3B/3B+か、何れにするにしてもラズパイケース、USB充電器、microSDカードかUSBメモリ、USBケーブル等々一式でAmazon相場で1万円前後かと。

 ESPモジュールは、ESP32開発ボードならAmazon相場で1500円前後、今回、映像関連機能をも見据えて?マイク内蔵USBカメラを使ったが、マイク、スピーカー、その他諸々、Amazon&Amazonマーケットプレイス&100円ショップ相場で一部電子部品などは単価割したとして約2000〜3000円程度?

 締めて1万4千〜1万5千円前後くらいにはなるのかな。

 Aliexpress相場だとこれの6割くらい〜半額程度といったところか。

 あんまり深く考えてないけど、一から買い揃えるとなると、やはり、どうやっても量産された市販のスマートスピーカーを買ったほうが安いよね、きっと、しかも、より機能豊富だったり。

ESPモジュール関連の準備

 冒頭の自作IRリモコン、自作スマートコンセントを必要に応じて用意する。

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <Arduino.h>
#include <FS.h>
 
const char* path_root  = "/index.html";
 
const char *ssid = "ssid";
const char *password = "password";
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
ESP8266WebServer server ( 80 );
IRsend irsend(2);
#define SOFTAP_SSID "XXXXX"
#define SOFTAP_PW "YYYYY"
 
boolean readHTML() {
 File htmlFile = SPIFFS.open(path_root, "r");
 if (!htmlFile) {
  Serial.println("Failed to open index.html");
  return false;
 }
 size_t size = htmlFile.size();
 if (size >= BUFFER_SIZE) {
  Serial.print("File Size Error:");
  Serial.println((int)size);
 } else {
  Serial.print("File Size OK:");
  Serial.println((int)size);
 }
 htmlFile.read(buf, size);
 htmlFile.close();
 return true;
}
 
void handleRoot() {
 Serial.println("Access");
 char temp[100];
 int sec = millis() / 1000;
 int min = sec / 60;
 int hr = min / 60;
 
 snprintf ( temp, 100, "", hr, min % 60, sec % 60 );
 server.send(200, "text/html", (char *)buf);
}
 
void tv_on_off() {
 Serial.println("Power");
 irsend.sendPanasonic(0x555A,0x555AF148688B);
 delay(10);
 irsend.sendPanasonic(0x555A,0x555AF148688B);
 delay(2000);
 server.send(200, "text/html", "Power ON/OFF");
}
// ...必要に応じ、関数追加
 
void handleNotFound() {
 
 String message = "File Not Found\n\n";
 message += "URI: ";
 message += server.uri();
 message += "\nMethod: ";
 message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
 message += "\nArguments: ";
 message += server.args();
 message += "\n";
 
 for ( uint8_t i = 0; i < server.args(); i++ ) {
  message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
 }
 server.send ( 404, "text/plain", message );
}
 
void setup() {
 Serial.begin(115200);
 
 SPIFFS.begin();
 if (!readHTML()) {
  Serial.println("Read HTML error!!");
 }
 
 WiFi.begin(ssid, password);
 irsend.begin();
 Serial.println("");
 // AP+STAモードの設定
 WiFi.mode(WIFI_AP_STA);
 // WiFi.mode(WIFI_STA);
 // APとして振る舞うためのSSIDとPW情報
 WiFi.softAP(SOFTAP_SSID, SOFTAP_PW);
 Serial.print("Connecting to ");
 Serial.println(SOFTAP_SSID);
 Serial.println("----------");
 
 //wait for connection
 while ( WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
 }
 Serial.println("");
 Serial.print("Connected to ");
 Serial.println(ssid);
 Serial.print("IP address: ");
 Serial.println(WiFi.localIP());
 
 if (!MDNS.begin("esptv")) {
  Serial.println("Error setting up MDNS responder!");
  while (1) {
   delay(1000);
  }
 }
 Serial.println("mDNS responder started");
 
 server.on("/", handleRoot);
 server.on("/TV/Power", tv_on_off);
// ...必要に応じ、アクセス時の処理を追加
 
 server.onNotFound(handleNotFound);
 
 server.begin();
 Serial.println("HTTP server started");
 
 // Add service to MDNS-SD
 MDNS.addService("http", "tcp", 80);
}
 
void loop() {
 server.handleClient();
}
 

 今回使ったラフスケッチは、これ。

 仮にこのまま使う場合、少なくとも宅内・社内無線LAN用のssid/password、ESPモジュール・アクセスポイント用のSOFTAP_SSID/SOFTAP_PWは、設定すること。

 信号出力ピンは、スケッチにIRsend irsend(2);とあるようにGPIO2を使った。

 とりあえず、テレビ(検証したのはSHARP AQUOS)のON/OFF用だけ書いたが、エアコンでもファンでも何でも必要に応じて追記すればよいし、個別に作るなら、GPIOピンは1本で足り、併用するなら、その数に応じてESPモジュールを選ぶと良いだろう。

 ちなみに声で操作もできるが、例えば、http://esptv.local/TV/Powerのようにブラウザからアクセスするだけで機能するし、SPIFFS関連コードも一部書いてはあるも実装していないが、handleルートにアクセスしたらindex.htmlを展開すれば、声でもブラウザからでもON/OFF以外の各種操作やESPリモコン回路を併用するなら他機器への操作を併用もできるだろう。

 どっちでも機能するのは、(Web|USB)カメラなどを使って赤外線発射の動作確認する際にも便利。

Open JTalkの準備

$ echo "あい藍アイi" | open_jtalk -m /usr/share/hts-voice/mei/mei_normal.htsvoice -ow output.wav -x /var/lib/mecab/dic/open-jtalk/naist-jdic && aplay output.wav && rm output.wav
$ echo "一二三" | open_jtalk -m /usr/share/hts-voice/mei/mei_normal.htsvoice -ow output.wav -x /var/lib/mecab/dic/open-jtalk/naist-jdic && aplay output.wav && rm output.wav
$ echo "24500" | open_jtalk -m /usr/share/hts-voice/mei/mei_normal.htsvoice -ow output.wav -x /var/lib/mecab/dic/open-jtalk/naist-jdic && aplay output.wav && rm output.wav
$ ...
$

 Open JTalkは、日本語を標準とするテキストを音声に変換し、読み上げてくれるソフトウェアであり、デフォルトで漢字、ひらがな、カタカナ、アルファベット、算用数字、漢数字、また、日付時刻など意味のある漢字数字混じりの並びの文は、それに合わせて相応に読んでくれる。

 より最新に近いLinuxのリポジトリや、公式サイトなどから、より最新のOpen JTalkパッケージを取得した場合、冒頭や後段のリンク先に書いた通り、インストールや展開するだけで簡単に試してみることができる。

 より具体的には、端末(コンソール・ターミナル)上で必要に応じたオプション付きのopen_jtalkコマンドにテキストをechoしてパイプ経由で渡したり、テキストファイルをcatやリダイレクトしたりするだけで日本語で音読してくれる(読み上げてくれる)。

$ cat jsay
#!/bin/bash
WAV_FILE=~/tmp/sound/jsay_${RANDOM}.wav
#cd /usr/share/hts-voice/nitech-jp-atr503-m001
#cd /usr/share/hts-voice/mei/mei_happy
#cd /usr/share/hts-voice/mei/mei_normal
cd /usr/share/hts-voice/mei/
echo "$1" | open_jtalk \
-x /var/lib/mecab/dic/open-jtalk/naist-jdic \
-m mei_happy.htsvoice \
-ow $WAV_FILE && \
aplay --quiet $WAV_FILE
rm -f $WAV_FILE
$

 Open JTalkに関しては、後述のPerlスクリプトから利用できるようにスクリプトを作成、実行権限を与えておく。

 スクリプトは何でも良いが、ここでは、Raspberry Piを使ってスマホ・音声で家電を制御するのbashスクリプトを使わせて頂いた。

 ただ、中間にあったオプションがことごとくエラーとなり、調べても今ひとつよくわからなかったため、省いた。

 また、メイさんの音声ファイルの格納ディレクトリ構成が変わったようなので修正したついでに、声色は、normalではなく、happyなメイさん(mei_happy.htsvoice)にお願いすることにした。

 これで入力された単語や文章を元気はつらつなメイさんが、しゃべってくれるようになる。

Juliusの準備

 Juliusは、標準では、日本語の音声をテキストに変換してくれるソフトウェア。

 Juliusは、より最新に近いLinuxのリポジトリや、公式サイトなどから、より最新のJuliusパッケージを取得した場合、冒頭や後段のリンク先に書いた通り、インストールするだけで簡単に使ってみることができる。

$ julius -C config_file -C am-gmm.jconf -module
...

 端末(コンソール・ターミナル)上で必要に応じたオプション付き(例えば、最も簡潔なものの1つは、dictation kitのディレクトリに移動し、[julius -C config_file]のようにする)のjuliusコマンドにモジュールモードでの起動を指定する-moduleオプションを付けて([julius -C config_file -module])サーバとして待機させておく。

<sil>           []              silB
<sil>           []              silE
            []              sp
スタンバイ     [スタンバイ]    s u t a N b a i
ニュートラル    [ニュートラル]  n u t o r a r u
照明起動        [照明起動]      sh o u m e i k i d o u
ライト点ける    [照明起動]      r a i t o t u k e r u
ライト点けて    [照明起動]      r a i t o t u k e t e
電気点けて      [照明起動]      d e N k i t u k e t e
照明停止        [照明停止]      sh o u m e i t e i sh i
ライト消す      [照明停止]      r a i t o k e s u
ライト消して    [照明停止]      r a i t o k e s i t e
電気消して      [照明停止]      d e N k i k e s i t e
暖房起動        [暖房起動]      d a N b o u k i d o u
暖房つける      [暖房起動]      d a N b o u t u k e r u
暖房つけて      [暖房起動]      d a N b o u t u k e t e
暖房停止        [暖房停止]      d a N b o u t e i sh i
暖房止めて      [暖房停止]      d a N b o u t o m e t e
暖房消す        [暖房停止]      d a N b o u k e s u
暖房消して      [暖房停止]      d a N b o u k e s i t e
冷房起動        [冷房起動]      r e: b o: k i d o u
冷房つける      [冷房起動]      r e: b o: t u k e r u
冷房つけて      [冷房起動]      r e: b o: t u k e t e
冷房停止        [冷房停止]      r e: b o: t e i sh i
冷房消す        [冷房停止]      r e: b o: k e s u
冷房消して      [冷房停止]      r e: b o: k e sh i t e
冷房止めて      [冷房停止]      r e: b o: t o m e t e
除湿して        [除湿起動]      j o s i t u sh i t e
除湿消して      [除湿停止]      j o sh i t u k e sh i t e
除湿止めて      [除湿停止]      j o sh i t u t o m e t e
エアコン消して  [エアコン停止]  e a k o N k e s i t e
テレビつけて    [テレビ起動]    t e r e b i t u k e t e
テレビ消して    [テレビ停止]    t e r e b i k e s i t e
今何時          [時刻報告]      i m a n a N j i
今日何日        [日付報告]      ky o n a N n i t i
今日何曜日      [曜日報告]      ky o n a N y o: b i
今日の天気は    [今日天気]      ky o n o t e N k i w a
今日の天気      [今日天気]      ky o n o t e N k i
明日の天気は    [明日天気]      a sh i t a n o t e N k i w a
明日の天気      [明日天気]      a sh i t a n o t e N k i
今日の気温は    [今日気温]      ky o n o k i o N w a
今日の気温      [今日気温]      ky o n o k i o N
明日の気温は    [明日気温]      a sh i t a n o k i o N w a
明日の気温      [明日気温]      a sh i t a n o k i o N

 ただ、そのconfig_file内で指定する辞書については、標準のものも使えるが、今回のようなリモコン操作など使いみち(使う言葉)が限定されればされるほど自作した方が格段に認識率があがるので、自身の用途に応じて作成する必要があるだろう。

 今回は、これまた、先のjsayスクリプトでお世話になったリンク先から拝借したものをベースに追加する恰好でこんな辞書を作ってみた。

 dictation kitの場合、おおまかに最小限で言うと命令に当たる日本語のフレーズと、ちょっとした作法に基づいて、これの読みとなるアルファベットの並びを一行ずつ書いたファイルのエンコードをeuc-jpに変換したもの。

 「ちょっとした作法」というのは、無音部分を表わすらしき、<sil>行は、とりあえず、そのまま書いておく、少なくとも母音と子音との間は半角スペースで区切る、大文字の[N]は[ん]を表わす、[:](コロン)は、音をのばすときに使うなど(半角スペース区切りは、先の決まりごと以外の部分でも使え、ここでは基本半角スペース区切りを多用している)。

 ここでは、3列あるが、2列めは、1列めやこれに準ずる音声が発せられたとき、Juliusがテキスト表示する際の文字列となり、後述のPerlスクリプトの条件分岐では、この2列めのフレーズで判定している。

 ちなみに「冷房」については、後述のPerlスクリプトの方で冷房の条件分岐を書き忘れ、認識しないな...というボケをかまして、ちょっとハマり、[r e i b o u]、[r e: b o:]はどっちでも、良かったはずだが、認識しないのと勘違い...[r e I]としたら、[I]がエラーではじかれたりしつつ、書き忘れに気づき、その時書いてあった[r e: b o:]とすることにした経緯がある。

$ iconv -f utf-8 -t euc-jp word.list.utf8 > word.list.eucjp
$

 先で例示した辞書は、エンコーディングがUTF-8だが、このようにeuc-jpにエンコード・変換したファイルを辞書として指定しないとエラーになったり、原因不明と無駄にあたふたする羽目になったりする。

 ちなみにgrammar kitの方の.vocaは、SJISだったりして、ちょっと、ややこしい...。

$ cat config_file
-w mysmartspeaker.eucjp
-v model/lang_m/bccwj.60k.htkdic
-h model/phone_m/jnas-tri-3k16-gid.binhmm
-hlist model/phone_m/logicalTri
-n 5
-output 1
-input mic
-input oss
-rejectshort 600
-charconv euc-jp utf8
-lv 1500
$

 例えば、dictation kitのconfig_fileにおいて辞書は、-wオプション付きで指定する。

 この中でmysmartspeaker.eucjp以外の言語モデルや音響モデルは、Julius標準のものを使っているだけ。

$ julius -C config_file -C am-gmm.jconf -module
...

 このように実行した状態で自マシン上の他の端末(や設定によっては他のマシン上の端末)から相応の手順を書いたスクリプトでアクセスし、音声を発すると他の端末やマシンに結果をテキストで返してくれる(というのが、モジュールモード)。

 オウム返しでは、芸がない(ボイスチェンジャーか遠方に音声を届けるくらいしか使いみちを思いつかない)が、発声した言葉に応答してくれる(ようにスクリプトに書くことができる)となると格段に発想が広がる...いや、そうでもない...か?

#!/usr/bin/env perl
use utf8;
#use strict;
use warnings;
 
use 5.10.0;
 
use Encode;
use IO::Socket;
 
use LWP::Simple;
use LWP::UserAgent;
use HTTP::Request::Common( "GET" );
 
# 接続先情報にJuliusサーバを指定する
my $socket = IO::Socket::INET->new(
 PeerAddr => 'localhost', # 接続先
 PeerPort => 10500,  # Port 番号
 Proto => 'tcp',  # Protocol
 TimeOut => 5    # タイムアウト時間
);
 
die("Could not create socket: $!") unless($socket);
 
# テレビON/OFFサーバー
local $esp_tv_server='http://esptv.local'
 
# ローカルタイム設定
local ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime;
$year += 1900;
$mon++;
local @wdays = qw/日 月 火 水 木 金 土/;
 
# 待機モードのループ
while(1){
 my $msg = $socket->getline();
 my ($word, $cm) = &get_parameter($msg);
 
 # 誤認識による誤作動防止のための合言葉を判定
 # 認識の信憑性もCM値で確認する
 if($word eq "スタンバイ" && $cm >= 0.8){
  system("~/tmp/sound/jsay アクティブモードを開始します");
 
  eval{
   local $SIG{ALRM} = sub { die "timeout" };
 
   # タイムアウトする時間(秒)の設定
   my $timer = 30;
 
   # タイムアウト処理-開始-
   alarm($timer);
 
   # 音声コマンドの受付のループ
   while(1){
    my $msg = $socket->getline();
    my ($word, $cm) = &get_parameter($msg);
 
    # 認識の信憑性が一定である場合はコマンドを識別し実行する
    if($cm >= 0.8){
     given($word){
      when("ニュートラル"){
       system("~/tmp/sound/jsay アクティブモードを終了します");
       last;
      }
      when("照明起動"){
       system("~/tmp/sound/jsay 照明を起動します");
#       system("sudo bto_ir_cmd -e -t 022000E70C976800000000000000000000000000000000000000000000000000000000");
      }
      when("照明停止"){
       system("~/tmp/sound/jsay 照明を停止します");
#       system("sudo bto_ir_cmd -e -t 022000E70C8B7400000000000000000000000000000000000000000000000000000000");
      }
      when("暖房起動"){
       system("~/tmp/sound/jsay 暖房を起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("暖房停止"){
       system("~/tmp/sound/jsay 暖房を停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("冷房起動"){
       system("~/tmp/sound/jsay 冷房を起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("冷房停止"){
       system("~/tmp/sound/jsay 冷房を停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("除湿起動"){
       system("~/tmp/sound/jsay じょしつを起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("除湿停止"){
       system("~/tmp/sound/jsay ジョシツを停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("エアコン停止"){
       system("~/tmp/sound/jsay エアコンを停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("テレビ起動"){
       system("~/tmp/sound/jsay テレビをつけます");
      print "$esp_tv_server/TV/Power ON\n";
 
      #### インスタンスの生成
      my $ua = new LWP::UserAgent;
      $ua->timeout( 10 );
 
      #### 要求条件を生成
      my $req = GET( "$esp_tv_server/TV/Power" );
      my $res = $ua->request( $req );
      print $res->as_string;
      }
      when("テレビ停止"){
       system("~/tmp/sound/jsay テレビを消します");
      print "$esp_tv_server/TV/Power OFF\n";
 
      #### インスタンスの生成
      my $ua = new LWP::UserAgent;
      $ua->timeout( 10 );
 
      #### 要求条件を生成
      my $req = GET( "$esp_tv_server/TV/Power" );
      my $res = $ua->request( $req );
      }
      when("時刻報告"){
       print "時刻は、 $hour 時 $min 分 $sec 秒です。";
       my $speechtime = sprintf("時刻は、%04d時%02d分%02d秒です。", $hour ,$min ,$sec);
 
       system("~/tmp/sound/jsay $speechtime");
      }
      when("日付報告"){
       print "日付は、 $year 年 $mon 月 $mday 日です。";
       my $speechdate = sprintf("日付は、%04d年%02d月%2d日です。", $year ,$mon ,$mday);
       system("~/tmp/sound/jsay $speechdate");
      }
      when("曜日報告"){
       print "$wdays[$wday]曜日です。";
       my $speechwday = sprintf("%s曜日です。", $wdays[$wday]);
       system("~/tmp/sound/jsay $speechwday");
      }
      when("今日天気"){
       system("~/tmp/sound/today_weather.py");
      }
      when("明日天気"){
       system("~/tmp/sound/tomorrow_weather.py");
      }
      when("今日気温"){
       system("~/tmp/sound/jsay 今日の気温は、");
      }
      when("明日気温"){
       system("~/tmp/sound/tomorrow_temperature.py");
      }
     }
    }
   }
 
   # タイムアウト処理-終了-
   alarm(0);
  };
 
  if($@){
   print $@ . "\n";
   system("~/tmp/sound/jsay ディアクティベートモードになります");
  }
 }
}
 
# 渡されたXMLにUTF-8フラグを付けてWORDとCMを取得する関数
sub get_parameter(){
 my $msg = shift;
 
 my $text = decode_utf8($msg);
 
 if($text =~ /.+WORD="(\S+)".+CM="(\S+)"/){
  return ($1, $2);
 }else{
  return ("", 0);
 }
}
 

 今回、Perlを使った、そのスクリプトがこれでベースは、bashスクリプトjsayやJulius辞書でもお世話になったリンク先のPerlスクリプトを、ESPモジュールへHTTPアクセスするにあたっては、Perl/Webアクセスから拝借した。

 また、天気APIから情報を得るスクリプトについては、livedoorの天気予報API『Weather Hacks』を使ったというPythonで書かれたtalk_weather.pyをhttp://raspi.seesaa.net/article/415530289.htmlから拝借。

 入力に応じた分岐条件を追記・編集してもよかったのかもしれないが、Python初心者の自身は、用途ごとにスクリプトファイルを分割し、ベースと成るPerlスクリプトから、それらPythonスクリプトを直接呼び出す方法をとった。

 このpythonスクリプト内では、奇しくも発話用にjsayという同じ名前のスクリプトを指定しているが、登録済み実行パスに存在するコマンドとして書いてあるようなので、そうでない場合は、環境に応じてjsayスクリプトのパスを合わせておく必要がある。

 日付と時刻は、天気予報APIからも取得できることを後で知ることになるが、先にPerlでは、最も古い部類と思われるlocaltime()関数から取得する方法をとっていたため、その値を加工して使った。

 今回作った一連の回路では、家電の操作・制御をより遠くから行なうことができるようにWiFiを介して操作するものを想定し、実際、そのようにした。

 ベースとさせて頂いたPerlスクリプトでは、音声に反応してラズパイから赤外線信号が届く範囲で操作する前提で直接信号出力するように書かれている。

 一方、今回は、プログラムを書き込むことができるWiFiモジュールであるESP8266/ESP32にリモコンとなる赤外線LEDを回路として組んである(というほどでもないが)ため、壁や家具・家電、何ならフロアを越えてもWiFi信号が届く範囲で利用可能であり、このPerlスクリプトからは、WebサーバでもあるESPの特定のパスにアクセスさせることにした(そうすることでESP側で赤外線信号を発信するようにした)。

 このサンプルスクリプトのリモコン操作に関しては、テレビのON/OFFの部分しか編集しておらず、他の分岐も一部追記はしたものの、発信信号を含めたコピペでしかない。

 尚、Perlスクリプト冒頭でJuliusのアクセス先を'localhost'としたので今回は、自マシンの他の端末からモジュールモードで起動したJuliusにアクセスすることになる。

 とは言え、コンセント接続、電源ONボタンでラズパイのOSが起動、マイクは構成ファイル上で環境変数に登録するなどしておき、無線LANに接続、cron登録されたJulius、Perlスクリプトを順次起動、準備完了LED点灯...といった完成品らしきものを作る場合でも自己完結させるなら'localhost'でよいだろう。

実行

 半完成品状態の今回は、実行する場合、一部順不同な部分もあるも概ね以下のような手順となる。(JuliusやOpen JTalkについての設定詳細は、前述、もしくは後段関連リンクの各リンク先参照。)

  1. 宅内・社内の無線LANにラズパイやESPが接続可能な状態であることを確認
  2. ESPモジュールを当該無線LANルータやアクセスポイントに接続・確認
  3. ラズパイを起動、同じネットワークドメイン上にあることを確認
  4. ラズパイに接続したマイクが認識され、音声入力が行えることを確認
  5. マイクが、Juliusから使える状態になっていることを確認(事前に環境変数に設定か引数で指定)
  6. Juliusをモジュールモードで起動
  7. (他)端末から先のPerlスクリプトを実行
  8. マイクから辞書にある[スタンバイ]と発声
  9. Juliusから[アクティブモードに入ります]との応答を確認
  10. マイクから辞書にある任意の命令を発声
  11. Juliusからこれに応じた応答があれば、そのとおり、実行される
  12. タイムアウト時間が経過すると[ディアクティベートモードになります]とアナウンスされる
  13. 再度、操作する場合は、8に戻る

 終了する場合は、どちらも端末から起動した場合は、[Ctrl]+[C]。

 場合によっては、スクリプト側は、ps aux | grep するなどしてkillせざるを得ないこともあるかも。

 ここでは、操作の合言葉として元のスクリプトのフレーズ「スタンバイ」を使わせて頂いているが、「ヘイ、Siri ...」、「OK、グーグル ...」、「アレクサ ...」のように、それっぽくするのも簡単で、辞書とスクリプトの当該箇所を変更するだけ。

実行結果

 先のサンプルスクリプトにおいて、家電については「テレビをつけて」「テレビを消して」という声に反応して「テレビをつけます」「テレビをけします」という女声メイさんの日本語音声による応答とともに既に電源が入ってついているか、リモコンで電源を切ってあるテレビのON/OFF操作ができる。(リモコン信号は、テレビに限らず、各電気製品のメーカーや機種のリモコンに合わせたものに変更する必要がある。)

 また、「今日何日」「今日何曜日」「今何時」とか、「今日の天気」「明日の天気」「明日の気温」にも日本語で答えてくれる。

 もちろん、「照明」/「冷房」/「暖房」/「除湿」+「つけて」/「かけて」/「して」/「起動」/「消して」/「消す」/「停止」などと既に辞書登録、Perlスクリプトに追記してあるものも、ESP回路を併用したり、別個に作ったりしつつ、それに合わせたコマンドなどをPerlスクリプトに追記するだけで使えるようになる。

 更なる機能追加もできるが、どれだけ無線でぶら下げられるかは、WiFiルーターやアクセスポイントの性能次第な部分もある?

 ちなみに、びっくりするほど認識してくれるOpen JTalkも漢字の「除湿」を「じょしめ」と読んでしまうが、スクリプトjsayに渡すときに「じょしつ」や「ジョシツ」とひらがなやカタカナにすれば、とりあえず、回避できる。

感想

 スマートスピーカーを買ってみようと思うほど興味はなかった自身も基本、完成品の組み合わせで手を加えたのは、ほんの少しながら、自作できてみると迂闊にもちょっと感激。

注意

 とりあえずHTTPアクセスした部分は、HTTPSアクセスにする他、あらゆる面でセキュリティは注意の上にも注意が必要だろう。

ぎょっ

  ラズパイ、Julius、Open JTalkのみならず、コンピュータビジョンやAIも取り込んでいるらしき、OpenCVやTensorFlow/Kerasまで使ったスマートスピーカーを公開している方がいた...。

おっ!?

  やっぱり買うには少し高いな。スマートスピーカーの作り方教えちゃいますってコレもなかなかおもしろいかも。

  ラズパイ+Google AIYで日本語対応スマートスピーカーRaspberry Piで日本語Alexaの方がスマートか。

  いや待てよ?Raspberry pi3でAIスピーカーをガッツリ自作が、すごいか。

備考

2018/10/05

  Chainerも入れたりしたけど、TensorFlow、Kerasを使ってみるべく、pyenv、virtualenvなどと比較検討した結果、検討時点で想定はしていたことではあるが、anaconda3を介したら、Pythonバージョンが上がってシステム側もPython3を強制使用するようになってしまう関係で自作スマートスピーカーにおいて呼び出していたPython2ベースのスクリプトでエラーになり、スクリプトをPython3ベースに修正する必要があった(anacondaを削除すれば、強制されないため、これを削除するっていう手もあるが...)。

2018/10/07

  結局、Dockerを使うことにし、anacondaを削除したのでホストOS上のPythonバージョンに影響はなく、問題は解決。

2018/11/21

 冒頭追記のようにRaspberry Pi 3 Model B+を買って実装、3B/3B+ならではのUSBブートしているわけだが、スマートスピーカー機能とは別にどういう訳か、人気があるらしきWeb|USBカメラLogicool C270をラズパイに挿しておくとブートするはずのUSBが起動しないという事象に遭遇。

 試しにUSBサウンドアダプタ、もう1つ手持ちのUSBカメラELECOM UCAM C0220FE(C0220FEWH)、更には、レスキュー用Live USBを挿してみたが、何れも正常にラズパイブート用USBからRaspbianが起動することを確認済み。

 なぜだ...例えば、C270の消費電流が他に比べ、一時的にでも相当に高くなり、電流不足に陥っているのか...?、はたまた、他のUSB機器との違いとしてはC270には、ケーブル上にフェライトコアが付いているが、これの影響があるのか?ちなみにUSB延長ケーブルを介してみても変わらない...。

 尚、C270もラズパイ起動後に挿せば、内蔵マイクも使えるし、以前、ラズパイ2Bでmotionを試したときもカメラとして使えたはず...。

 ん?2Bならいけるのか?と現在サーバとして使っているRaspberry Pi 2 Model Bで試してみるとC270をつないでおいてもラズパイが起動する...。

 ということは、フェライトコアの可能性は消えたと考えてよいだろう、すると3B+との相性...か、なんら異常は見られず、機能しているが、手持ちの3B+の不具合か...、高性能になり消費電力が高くなった3B+に加え、C270が他より高い、結果ブートに影響...という可能性もなくもないか...?

 幸い、これとは別にラズパイと古いパソコン周辺機器を組み合わせてパソコン化を検証してみる為、Aliexpressではありながらも違う店にRaspberry Pi 3 B+を発注してあるので届き次第、試してみようと思う。

関連リンク

ウェブ造ホーム前へ次へ