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

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

ホーム前へ次へ
Raspberry Piって?

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

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

2020/05/14 YouTubeアップロード
2018/10/01

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

 内蔵時計、天気APIとも連携し、日付、時刻、今日の天気、明日の天気、明日の(最高/最低)気温も日本語で聞けば教えてくれます。

 また、スマホやタブレット、パソコンなどからWiFi操作で壁や家具越しにも操作可能なESPモジュールによる自作IR・赤外線リモコンでリモコン対応家電や自作スマートプラグ(スマートコンセント)に挿した非リモコン家電を日本語の音声でWiFi越しにON/OFF操作(スマートリモコン化)できます。

 今のところ、これくらいですが、思いついたものは、どんどん機能追加していく予定のこのスマートスピーカーは、公開されている情報の組み合わせで比較的簡単にできました。

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

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

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

スマートスピーカー用ハードウェアにRaspberry Pi

 さて、自作スマートスピーカーのメインとなるハードウェアは、Raspberry Pi。

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

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

 どんなものでも良いですが、ただ、ラズパイにオーディオジャックは一つしかなく、スピーカーは、オーディオプラグのみか、USB+オーディオプラグのものしかないでしょうから、自ずとマイクはUSB接続のものを選ぶことになるでしょう。

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

2018/12/07

 ラズパイにはマイク入力端子がないとのことで後述のようにラズパイで使う限りにおいては、少なくともマイクはUSB接続のものを調達すべきな模様。

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

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

前提

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

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

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

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

 参考までに自身が今回使用した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万円前後かと。

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

 締めて1万2千〜1万3千円前後くらいかと。

 Aliexpress相場だとこれの7割くらい〜半額程度といったところでしょうか。

 あんまり深く考えていませんが、一から買い揃えるとなると、量産された市販のスマートスピーカーを買ったほうが安いかも?とも思いましたが、そうでもなく、いろいろメリットもありそうです。

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やリダイレクトしたりするだけで日本語で音読してくれます(読み上げてくれます)。

$ chmod +x jsay
$ cat jsay
#!/bin/sh
WAV_FILE=/home/xxx/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スクリプトを使わせて頂きました(自身はシェバンを#!/bin/shとしました)。

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

 また、メイさんの音声ファイルの格納ディレクトリ構成が変わったようなので修正したついでに、声色は、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:]とすることにした経緯があります。

 ただ、詳細は割愛しますが、別途スクリプトで判定する文字列をどうしたらよいのかはさておき、辞書としては、grammer kitで.grammarと.vocaファイルを作成、mkdfa.plで変換し、.dfa/.dict/.termファイルを作成、併用する方が、「今日」とか「明日」とか、「つけて」「けして」など重複するものをまとめられるため、スマートだし、こうした用途の場合、本来の使い分けとして想定されているのは、この方法である模様。

$ 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
...

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

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

$ chmod +x voicereceive.pl
$ cat voicereceive.pl
#!/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("/home/xxx/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("/home/xxx/sound/jsay アクティブモードを終了します");
       last;
      }
      when("照明起動"){
       system("/home/xxx/sound/jsay 照明を起動します");
#       system("sudo bto_ir_cmd -e -t 022000E70C976800000000000000000000000000000000000000000000000000000000");
      }
      when("照明停止"){
       system("/home/xxx/sound/jsay 照明を停止します");
#       system("sudo bto_ir_cmd -e -t 022000E70C8B7400000000000000000000000000000000000000000000000000000000");
      }
      when("暖房起動"){
       system("/home/xxx/sound/jsay 暖房を起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("暖房停止"){
       system("/home/xxx/sound/jsay 暖房を停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("冷房起動"){
       system("/home/xxx/sound/jsay 冷房を起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("冷房停止"){
       system("/home/xxx/sound/jsay 冷房を停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("除湿起動"){
       system("/home/xxx/sound/jsay じょしつを起動します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043422EDE230068000001000055000000000000000000000000000000");
      }
      when("除湿停止"){
       system("/home/xxx/sound/jsay ジョシツを停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("エアコン停止"){
       system("/home/xxx/sound/jsay エアコンを停止します");
#       system("sudo bto_ir_cmd -e -t 0188004000148043412EDE030068000001000052000000000000000000000000000000");
      }
      when("テレビ起動"){
       system("/home/xxx/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("/home/xxx/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("/home/xxx/sound/jsay $speechtime");
      }
      when("日付報告"){
       print "日付は、 $year 年 $mon 月 $mday 日です。";
       my $speechdate = sprintf("日付は、%04d年%02d月%2d日です。", $year ,$mon ,$mday);
       system("/home/xxx/sound/jsay $speechdate");
      }
      when("曜日報告"){
       print "$wdays[$wday]曜日です。";
       my $speechwday = sprintf("%s曜日です。", $wdays[$wday]);
       system("/home/xxx/sound/jsay $speechwday");
      }
      when("今日天気"){
       system("/home/xxx/sound/today_weather.py");
      }
      when("明日天気"){
       system("/home/xxx/sound/tomorrow_weather.py");
      }
      when("今日気温"){
       system("/home/xxx/sound/jsay 今日の気温は、");
      }
      when("明日気温"){
       system("/home/xxx/sound/tomorrow_temperature.py");
      }
     }
    }
   }
 
   # タイムアウト処理-終了-
   alarm(0);
  };
 
  if($@){
   print $@ . "\n";
   system("/home/xxx/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を使った、そのスクリプトがこれ(voicereceive.pl)でベースは、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()関数から取得する方法をとっていたため、その値を加工して使いました。

$ cat ~/script/res_date.sh
#!/bin/sh
nowdate=`date | awk '{print $1 $2 $3}' | sed -e "s/$/です/"`
$HOME/sound/jsay $nowdate
$ cat ~/script/res_day.sh
#!/bin/sh
nowday=`date | awk '{print $4}' | sed -e "s/$/です/"`
$HOME/sound/jsay $nowday
$ cat ~/script/res_time.sh
#!/bin/sh
nowtime=`date | awk '{print $5}' | sed -e "s/:/時/" | sed -e "s/:/分/" | sed -e "s/$/秒です/"`
$HOME/sound/jsay $nowtime
$ chmod +x ~/script/res_date.sh
$ chmod +x ~/script/res_day.sh
$ chmod +x ~/script/res_time.sh
2018/12/20

 が、スクリプト内でlocaltime()を使ったところで、これだと実行時から更新されない為、Web APIから取得するか、オリジナルスクリプトで対応する必要がありました。

 ここでは、日付、曜日、時間用にdate、awksedjsayスクリプトを使ったshellスクリプトをそれぞれ作って自作スマートスピーカー用perlスクリプトvoicereceive.plに反映させることにしました。

 ただし、時・分・秒共にちょうど(00)の際は、読み飛ばされるので、気になる場合は、別途処理を要します。

 今回作った一連の回路では、家電の操作・制御をより遠くから行なうことができるように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'でよいでしょう。

$ chmod u+x ~/sound/voicereceive.py
$ cat ~/sound/voicereceive.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
 
import sys
import socket
import time
import subprocess
 
host = '127.0.0.1'
port = 10500
datasize = 1024
 
class Julius:
 
 def __init__(self):
 
  self.sock = None
 
 def run(self):
  subprocess.call("/path/to/any/jsay 音声操作準備中 &", shell=True)
  subprocess.call("/path/to/any/script/sp_chk.sh &", shell=True)
 
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as self.sock:
   self.sock.connect((host, port))
   strWakeWord = ""
   cmval = 0
   end_flg = False
 
   while True:
    strWakeWord = ""
    data = self.sock.recv(datasize).decode('utf-8')
  
    for line in data.split('\n'):
     index = line.find('WORD="')
     if index != -1:
      strWakeWord = strWakeWord + line[index+6:line.find('"',index+6)]
     index = line.find('CM="')
     if index != -1:
      cmval = line[index+4:line.find('"',index+4)]
     if '' in line:
      end_flg = True
   
    if (end_flg == True and 'ボイス' == strWakeWord and float(cmval) >= 0.8):
     subprocess.call("/path/to/any/jsay 音声操作を開始します &", shell=True)
     start_time = time.perf_counter()
     strWakeWord = ""
     strOrderWord = ""
     cmval_o = 0
     end_flg_o = False
 
     while True:
      strOrderWord = ""
      #print("loop 2")
      data_o = self.sock.recv(datasize).decode('utf-8')
      for line in data_o.split('\n'):
       index_o = line.find('WORD="')
       if index_o != -1:
        strOrderWord = strOrderWord + line[index_o+6:line.find('"',index_o+6)]
       index_o = line.find('CM="')
       if index_o != -1:
        cmval_o = line[index_o+4:line.find('"',index_o+4)]
       if '' in line:
        end_flg_o = True
 
      if (end_flg_o == True and float(cmval_o) >= 0.8):
       print(strOrderWord + " : " + str(cmval_o))
       #==================
       # 休憩
       #==================
       if 'ニュートラル' == strOrderWord:
        subprocess.call("/path/to/any/jsay 休憩します &", shell=True)
        break
       #==================
       # カメラ
       #==================
       elif 'カメラ表示' == strOrderWord:
        subprocess.call("/path/to/any/jsay カメラを表示します &", shell=True)
        subprocess.call("/path/to/any/script/getcamview.sh 2>/dev/null &", shell=True)
       elif 'カメラ表示終了' == strOrderWord:
        subprocess.call("/path/to/any/jsay カメラ表示を終了します &", shell=True)
        subprocess.call("pkill chromium &", shell=True)
       #==================
       # 会議・ボイチャ
       #==================
       elif '内線コール' == strOrderWord:
        subprocess.call("/path/to/any/jsay 内線コールを開始します &", shell=True)
        subprocess.call("/path/to/any/script/my_house_meeting.sh inside 2>/dev/null &", shell=True)
       elif '内線コール終了' == strOrderWord:
        subprocess.call("/path/to/any/jsay 内線コールを終了します &", shell=True)
        subprocess.call("/path/to/any/script/stop_meeting_browser.sh &", shell=True)
       elif 'ミーティング' == strOrderWord:
        subprocess.call("/path/to/any/jsay プロジェクトエーミーティング &", shell=True)
        subprocess.call("/path/to/any/script/my_company_meeting.sh projectA 2>/dev/null &", shell=True)
       elif 'ミーティング終了' == strOrderWord:
        subprocess.call("/path/to/any/jsay ミーティングを終了します &", shell=True)
        subprocess.call("/path/to/any/script/browser_end.sh &", shell=True)
       #==================
       # リモコン
       #==================
       #==================
       # マイルーム
       #==================
       elif 'MY_ROOM_LIGHT_SW' == strOrderWord:
        subprocess.call("/path/to/any/jsay 照明を操作します &", shell=True)
        subprocess.call("/path/to/any/script/my_pendant_light.py 1 &", shell=True)
       elif 'MY_CURTAIN_OPEN' == strOrderWord:
        subprocess.call("/path/to/any/jsay カーテンを開けます &", shell=True)
        subprocess.call("/path/to/any/script/my_curtain_http_get.py OPEN &", shell=True)
       elif 'MY_CURTAIN_CLOSE' == strOrderWord:
        subprocess.call("/path/to/any/jsay カーテンを閉めます &", shell=True)
        subprocess.call("/path/to/any/script/my_curtain_http_get.py CLOSE &", shell=True)
       #==================
       # ダイニング
       #==================
       elif 'DINING_LIGHT_ON' == strOrderWord:
        subprocess.call("/path/to/any/jsay ダイニングの照明をつけます &", shell=True)
        subprocess.call("/path/to/any/script/dk_light.py 1 &", shell=True)
       elif 'DINING_LIGHT_OFF' == strOrderWord:
        subprocess.call("/path/to/any/jsay ダイニングの照明を消します &", shell=True)
        subprocess.call("/path/to/any/script/dk_light.py 2 &", shell=True)
       elif 'KITCHEN_LIGHT_ON' == strOrderWord:
        subprocess.call("/path/to/any/jsay キッチンの照明をつけます &", shell=True)
        subprocess.call("/path/to/any/script/dk_light.py 3 &", shell=True)
       elif 'KITCHEN_LIGHT_OFF' == strOrderWord:
        subprocess.call("/path/to/any/jsay キッチンの照明を消します &", shell=True)
        subprocess.call("/path/to/any/script/dk_light.py 4 &", shell=True)
       #==================
       # エアコン
       #==================
       elif 'エアコン自動MODE起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay エアコン自動モードを起動します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl AUTO_DRIVE &", shell=True)
       elif 'エコMODE起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay エコモードでエアコンを起動します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl ECO_MODE &", shell=True)
       elif '暖房起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay 暖房を起動します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl HEATER &", shell=True)
       elif '冷房起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay 冷房を起動します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl HEATER &", shell=True)
       elif '除湿起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay ジョシツを起動します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl DRY &", shell=True)
       elif 'エアコン空気清浄' == strOrderWord:
        subprocess.call("/path/to/any/jsay エアコンのクウセイをオンオフします &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl AIR_CLEANER &", shell=True)
       elif 'エアコン停止' == strOrderWord:
        subprocess.call("/path/to/any/jsay エアコンを停止します &", shell=True)
        subprocess.call("/path/to/any/script/aircon.pl STOP &", shell=True)
       #==================
       # 空気清浄機
       #==================
       elif '空清ON/OFF' == strOrderWord:
        subprocess.call("/path/to/any/jsay 空気清浄機の電源をオンオフします &", shell=True)
        subprocess.call("/path/to/any/script/aircleaner.pl power &", shell=True)
       elif '空清風量UP/DOWN' == strOrderWord:
        subprocess.call("/path/to/any/jsay 空気清浄機の風量を調節します &", shell=True)
        subprocess.call("/path/to/any/script/aircleaner.pl wind_amount &", shell=True)
       elif '空清タイマー' == strOrderWord:
        subprocess.call("/path/to/any/jsay 空気清浄機のタイマーを設定します &", shell=True)
        subprocess.call("/path/to/any/script/aircleaner.pl timer &", shell=True)
       elif '空清ターボ' == strOrderWord:
        subprocess.call("/path/to/any/jsay ターボモードをオンオフします &", shell=True)
        subprocess.call("/path/to/any/script/aircleaner.pl turbo &", shell=True)
       #==================
       # 扇風機1
       #==================
       elif '扇風機1ON/OFF' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1の電源をオンオフします &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl power &", shell=True)
       elif '扇風機1首振り' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1の首振りをオンオフします &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl neck_swing &", shell=True)
       elif '扇風機1風量UP' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1の風量を上げます &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl wind_power_plus &", shell=True)
       elif '扇風機1風量DOWN' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1の風量を下げます &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl wind_power_minus &", shell=True)
       elif '扇風機1 ONタイマー' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1のオンタイマーを設定します &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl on_timer &", shell=True)
       elif '扇風機1 OFFタイマー' == strOrderWord:
        subprocess.call("/path/to/any/jsay 扇風機1のオフタイマーを設定します &", shell=True)
        subprocess.call("/path/to/any/script/cool_fan1.pl off_timer &", shell=True)
       #==================
       # テレビ
       #==================
       elif 'テレビ起動' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビをつけます &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl power &", shell=True)
       elif 'テレビ停止' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビを消します &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl power &", shell=True)
       elif '1ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 1チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 1ch &", shell=True)
       elif '2ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 2チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 2ch &", shell=True)
       elif '3ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 3チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 3ch &", shell=True)
       elif '4ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 4チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 4ch &", shell=True)
       elif '5ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 5チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 5ch &", shell=True)
       elif '6ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 6チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 6ch &", shell=True)
       elif '7ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 7チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 7ch &", shell=True)
       elif '8ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 8チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 8ch &", shell=True)
       elif '9ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 9チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 9ch &", shell=True)
       elif '10ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 10チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 10ch &", shell=True)
       elif '11ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 11チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 11ch &", shell=True)
       elif '12ch' == strOrderWord:
        subprocess.call("/path/to/any/jsay 12チャンネル &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl 12ch &", shell=True)
       elif 'TV音量アップ' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビの音量を上げます &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl vol_up &", shell=True)
       elif 'TV音量2アップ' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビの音量を2つ上げます &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl vol_up_2 &", shell=True)
       elif 'TV音量ダウン' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビの音量を下げます &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl vol_down &", shell=True)
       elif 'TV音量2ダウン' == strOrderWord:
        subprocess.call("/path/to/any/jsay テレビの音量を2つ下げます &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl vol_down_2 &", shell=True)
       elif '番組表' == strOrderWord:
        subprocess.call("/path/to/any/jsay 番組表 &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl prog_list &", shell=True)
       elif '番組情報' == strOrderWord:
        subprocess.call("/path/to/any/jsay 番組情報 &", shell=True)
        subprocess.call("/path/to/any/script/tv_ctrl.pl prog_info &", shell=True)
       #==================
       # ロールスクリーン
       #==================
       elif 'ロールスクリーンUP' == strOrderWord:
        subprocess.call("/path/to/any/jsay スクリーンを上げます &", shell=True)
        subprocess.call("/path/to/any/script/rollscreen.py 1 &", shell=True)
       elif 'ロールスクリーンDN' == strOrderWord:
        subprocess.call("/path/to/any/jsay スクリーンを下げます &", shell=True)
        subprocess.call("/path/to/any/script/rollscreen.py 2 &", shell=True)
       #==================
       # カレンダー
       #==================
       elif '十二支' == strOrderWord:
        subprocess.call("/path/to/any/jsay 干支 &", shell=True)
        subprocess.call("/path/to/any/script/eto.sh &", shell=True)
       elif '旧暦' == strOrderWord:
        subprocess.call("/path/to/any/jsay 旧暦の月 &", shell=True)
        subprocess.call("/path/to/any/script/old_month.sh &", shell=True)
       #==================
       # 日付時刻
       #==================
       elif '時刻報告' == strOrderWord:
        subprocess.call("/path/to/any/script/res_time.sh &", shell=True)
       elif '日付報告' == strOrderWord:
        subprocess.call("/path/to/any/script/res_date.sh &", shell=True)
       elif '曜日報告' == strOrderWord:
        subprocess.call("/path/to/any/script/res_day.sh &", shell=True)
       #==================
       # タイマー
       #==================
       elif '1分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 1 &", shell=True)
       elif '3分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 3 &", shell=True)
       elif '5分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 5 &", shell=True)
       elif '10分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 10 &", shell=True)
       elif '15分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 15 &", shell=True)
       elif '20分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 20 &", shell=True)
       elif '25分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 25 &", shell=True)
       elif '30分タイマー' == strOrderWord:
        subprocess.call("/path/to/any/script/timer.pl 30 &", shell=True)
       elif 'タイマー停止' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_all_timer.pl &", shell=True)
       #==================
       # ニュース
       #==================
       elif 'ニュース' == strOrderWord:
        subprocess.call("/path/to/any/jsay ニュース &", shell=True)
        subprocess.call("/path/to/any/script/get_yahoo_news.py &", shell=True)
       elif 'BBCWorldNews' == strOrderWord:
        subprocess.call("/path/to/any/jsay BBCワールドニュース &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("mplayer http://stream.live.vc.bbcmedia.co.uk/bbc_radio_one &", shell=True)
       elif 'ABCAustraliaNews' == strOrderWord:
        subprocess.call("/path/to/any/jsay ABCニュース &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("mplayer -playlist http://abc.net.au/res/streaming/audio/aac/news_radio.pls &", shell=True)
       #==================
       # 天気
       #==================
       elif '今日天気' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 1`\" &", shell=True)
       elif '明日天気' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 2`\" &", shell=True)
       elif '明後日天気' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 3`\" &", shell=True)
       elif '今日気温' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 4`\" &", shell=True)
       elif '明日気温' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 5`\" &", shell=True)
       elif '明後日気温' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 6`\" &", shell=True)
       elif '今日降水確率' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 7`\" &", shell=True)
       elif '明日降水確率' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 8`\" &", shell=True)
       elif '明後日降水確率' == strOrderWord:
        subprocess.call("/path/to/any/jsay \"`/path/to/any/script/yahoo_weather_scraping.py 8`\" &", shell=True)
       #==================
       # 音声メモ
       #==================
       elif 'メモ' == strOrderWord:
        subprocess.call("/path/to/any/script/add_memo.pl &", shell=True)
       elif 'メモ再生' == strOrderWord:
        subprocess.call("/path/to/any/script/play_memo.sh &", shell=True)
       elif 'メモ消去' == strOrderWord:
        subprocess.call("/path/to/any/script/del_memo.pl &", shell=True)
       #==================
       # 伝言メッセージ
       #==================
       elif '伝言' == strOrderWord:
        subprocess.call("/path/to/any/script/add_msg.pl &", shell=True)
       elif '伝言再生' == strOrderWord:
        subprocess.call("/path/to/any/script/play_msg.sh &", shell=True)
       elif '伝言消去' == strOrderWord:
        subprocess.call("/path/to/any/script/del_msg.pl &", shell=True)
       #==================
       # My CD Collection
       # My CASSETTE Collection
       #==================
       elif '音楽再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/mymusic.sh &", shell=True)
       elif 'myJPOP再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myjpop.sh &", shell=True)
       elif '演歌再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myenka.sh &", shell=True)
       elif 'フォーク再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myfolk.sh &", shell=True)
       elif 'myPOPS再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/mypops.sh &", shell=True)
       elif '映画音楽再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/mymovie_music.sh &", shell=True)
       elif 'myJAZZ再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myjazz.sh &", shell=True)
       elif 'myCLASSIC再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myclassic.sh &", shell=True)
       elif '楽器演奏再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myinstrumental_music.sh &", shell=True)
       elif '日本民謡再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myjapan_minyo.sh &", shell=True)
       elif 'ロシア民謡再生' == strOrderWord:
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("/path/to/any/script/myrussian_minyo.sh &", shell=True)
       #==================
       # Radiko
       #==================
       elif 'JWAVE' == strOrderWord:
        subprocess.call("/path/to/any/jsay ジェイウェイブ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay FMJ &", shell=True)
       elif 'InterFM897' == strOrderWord:
        subprocess.call("/path/to/any/jsay インターエフエム &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay INT &", shell=True)
       elif 'TokyoFM' == strOrderWord:
        subprocess.call("/path/to/any/jsay 東京エフエム &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay FMT &", shell=True)
       elif 'bayfm78' == strOrderWord:
        subprocess.call("/path/to/any/jsay ベイエフエム &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay BAYFM78 &", shell=True)
       elif 'NACK5' == strOrderWord:
        subprocess.call("/path/to/any/jsay ナックファイブ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay NACK5 &", shell=True)
       elif 'FMヨコハマ' == strOrderWord:
        subprocess.call("/path/to/any/jsay エフエムヨコハマ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay YFM &", shell=True)
       elif 'TBSラジオ' == strOrderWord:
        subprocess.call("/path/to/any/jsay ティービーエスラジオ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay TBS &", shell=True)
       elif 'ニッポン放送' == strOrderWord:
        subprocess.call("/path/to/any/jsay ニッポン放送 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay LFR &", shell=True)
       elif 'ラジオ日本' == strOrderWord:
        subprocess.call("/path/to/any/jsay ラジオにっぽん &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay JORF &", shell=True)
       elif '文化放送' == strOrderWord:
        subprocess.call("/path/to/any/jsay 文化放送 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay QRR &", shell=True)
       elif 'ラジオNIKKEI第1' == strOrderWord:
        subprocess.call("/path/to/any/jsay ニッケイ第1 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay RN1 &", shell=True)
       elif 'ラジオNIKKEI第2' == strOrderWord:
        subprocess.call("/path/to/any/jsay ニッケイ第2 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay RN2 &", shell=True)
       elif '放送大学' == strOrderWord:
        subprocess.call("/path/to/any/jsay 放送大学 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay HOUSOU-DAIGAKU &", shell=True)
       #==================
       # らじるらじる
       # と思ったら
       # 試験放送ながらradiko版があった
       #==================
       elif '東京NHK第1' == strOrderWord:
        subprocess.call("/path/to/any/jsay NHK第1 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay JOAK &", shell=True)
       elif '東京NHK第2' == strOrderWord:
        # NHK第2 はRadikoでの試験放送終了したっぽい
        subprocess.call("/path/to/any/jsay NHK第2 &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay JOAB &", shell=True)
       elif '東京NHKFM' == strOrderWord:
        subprocess.call("/path/to/any/jsay NHKえふえむ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        subprocess.call("sleep 1", shell=True)
        subprocess.call("/path/to/any/radio/radiko.py -p ffplay JOAK-FM &", shell=True)
       #==================
       # ICECAST
       # internet-radio.com
       #==================
       elif 'JAZZ' == strOrderWord:
        subprocess.call("/path/to/any/jsay ジャズ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("/path/to/any/script/internetradiocomjazz.sh &", shell=True)
       elif 'CLASSIC' == strOrderWord:
        subprocess.call("/path/to/any/jsay クラシック &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("/path/to/any/script/internetradiocomclassical.sh &", shell=True)
       elif 'BLUES' == strOrderWord:
        subprocess.call("/path/to/any/jsay ブルース &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("/path/to/any/script/internetradiocomblues.sh &", shell=True)
       #==================
       # YouTube
       #==================
       elif 'POPS' == strOrderWord:
        subprocess.call("/path/to/any/jsay ポップス &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh %", shell=True)
        time.sleep(3.8)
        subprocess.call("/path/to/any/script/youtube_pops_playlist.sh &", shell=True)
       elif 'JPOP' == strOrderWord:
        subprocess.call("/path/to/any/jsay ジェイポップ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("/path/to/any/script/youtube_jpop_playlist.sh &", shell=True)
       elif 'SKIP' == strOrderWord:
        subprocess.call("/path/to/any/jsay スキップ &", shell=True)
        subprocess.call("/path/to/any/script/skip_playlist.sh &", shell=True)
       #==================
       # サイマルラジオ
       #==================
       elif 'ラジオニセコ' == strOrderWord:
        subprocess.call("/path/to/any/jsay ラジオニセコ &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
        time.sleep(3.8)
        subprocess.call("mplayer -playlist http://www.simulradio.info/asx/radioniseko_24k.asx &", shell=True)
       #==================
       # ラジオ停止
       #==================
       elif 'ラジオ停止' == strOrderWord:
        subprocess.call("/path/to/any/jsay ラジオを停止します &", shell=True)
        subprocess.call("/path/to/any/script/stop_radio.sh &", shell=True)
 
       else:
        pass
       strOrderWord = ""
       if time.perf_counter() - start_time >= 30:
        end_flg_o = False
        strOrderWord = ""
        subprocess.call("/path/to/any/jsay 音声操作を一時停止します &", shell=True)
        break
 
if __name__ == "__main__":
 
 julius = Julius()
 julius.run()
2021/11/13

 思うところあって応答スクリプトをPerlからPython(3.x)に書き換え・変更しました。

 まだ、思うところについては実装していませんが。

 たまにですが、意図せず、あれこれ立て続けに複数の応答があって過剰認識気味な時がある気がするので、これをPCで、ラズパイスマートスピーカーの方は、従来のPerlで様子を見ることにします。

 尚、Perlスクリプトではそうしたことはありませんでしたが、Pythonスクリプトだと、なぜか、以下のような対処が必要でした。

 YouTube、Internet-radio.comなど音楽ストリーミングやサイマルラジオに関しては、応答スクリプト内でmplayerなどを使って直接か、スクリプトを介してかに関わらず、再生直前に(3秒ジャストだと足りない)スリープをかます必要が。

 ABCワールドニュースやBBCワールドニュースなどニュース系は大丈夫でした。

 全て直前にpkillなど各種停止コマンド、ローカルサーバ用のumountなどを仕込んだスクリプト実行との絡みかと思われます。

 当該スクリプト行をコメントアウトするとスリープを入れる必要もなく音楽系のストリーミング再生もされるので。

2021/11/14

 過剰認識気味、思うところ、更にABCワールドニュースやBBCワールドニュースでもスリープ必要だったので、この3点は勘違いでした。

 time.sleep()に変更はしませんでしたが、よく見たら、そもそもradikoも1秒ではありますが、スリープ入れてましたね。

 尚、if文評価で'値' == 変数となっているのは、Perlのwhen文からPythonのif文にスクリプトを一括変換した際の名残です(またの名を手抜きとも言う)。

2022/05/17

 ラズパイスマートスピーカー機能が起動しなくなり、調べてみると、なぜか、Perlの応答スクリプトでuseやrequireしているモジュールがことごとく、"did not return a true value at"となる、末尾に1;は入っている、他の原因を探すのも億劫。

 よって深追いせず、ラズパイスマートスピーカーの応答スクリプトもPerlからPython(3.x)に書き換えたものに変更することにしました。

Raspberry Pi/ESPボード/WebSocketクライアントの組み合わせについての注意・特記事項

2020/04/27

 ESP8266/ESP32などのESPボードをWebSocketサーバ、pip/pip3(Python2.x/Python3)でインストールしたwebsocket-client-py/websocket-client-py3ライブラリを使ったPythonスクリプトをクライアントとし、スマートスピーカーとしてのみならず、パソコンとして使っているものからの検証なども含むRaspberry Piから操作をする場合の注意事項について。

 websocket-client-py/websocket-client-py3には、スクリプトの生存期間としてのShort-lived connectionとLong-lived connectionという2つのバージョンが紹介されていますが、単にshort/longの差のみならず、使い分けが必要なケースがあったよという話です。

 というか、ライブラリ云々ではなく、PCに入れた自作スマートスピーカー機能からの音声操作やスクリプト実行ではショートバージョンでも100発100中で問題なく、スマートスピーカーとして、また、サブパソコンとして使っているRaspberry Pi(ちなみに何れも3 B+)に共通するのでラズパイに起因するようです。

 ロングバージョンを使った方が良い(安定して操作できる)のは、どうやら、Rapsberry Piから操作するESP32ボードの全てと一部のESP8266ボードが対象となると思われます。

 ESP8266ボードについては、予想の域を出ませんが、スマートスピーカーから一定以上の距離(ラズパイのWiFiチップだと無線が微妙に届きにくいところ)にあるものかなと思っていますが、あれ?ESP32も全てじゃなくて同じ原因かも?

 ラズパイスマートスピーカーからショートバージョンでもいけてるのは、ダイニングキッチンにあるスマートスピーカー近くにあるESP8266ボード1つ、ロングバージョンでないといけないESP8266ボード1つとESP32ボード3つのこれら計4つは、別部屋に割とまとまって設置してあるので...。

 とは言え、ちなみに同じスマートスピーカー機能を入れてあるメインPCも同じ部屋内の近くにあるものの、ダイニングキッチンにあるラズパイスマートスピーカー近くにあるESP8266も余裕で操作できている為、やはり、ラズパイのWiFiチップは、電波キャッチするの苦手なのかも!?

 何れにせよ、この時、ここでスマートスピーカーの応答用として使用しているスクリプトvoicereceive.plにもちょっとした工夫が必要となります(他にも方法はあるかもしれませんが)。

 具体的に言うと、これらでショートバージョンを使うとスクリプト実行段階で下手をすると数%成功するか否かというほどに成功率が格段に下がり、とても実用的とは言えない状態になり、ロングバージョンだと確実に操作できるので後者を使うのが賢明です(ここに気づくに至るまでが長かった)。

 ただし、ロングバージョンでは、スクリプトの最後にws.run_forever()といかにも永遠に存続する気満々そうなメソッドがあり、スクリプトを直接起動するとジョブが、音声操作経由だとプロセスが残り、なぜか、対象デバイスへの操作が2件ほどたまると以後、対象となるESPボードを使ったガジェットの操作ができなくなり、kill/pkillすると操作できるようになります。

 かと言ってpythonスクリプト内でどうにかする方法をにわかには思いつきませんでした。

 そこでスマートスピーカー応答用スクリプトvoicereceive.plの当該操作行(Perlスクリプトでシェルや他スクリプトを実行できる[system("...")])に続けて実行スクリプト自体のプロセスをkillできるようにperlの関数なのでそのまま、sleep(2);、更に続けて次行にsystem("pkill -f スクリプト名");などとし、これらの変更を有効にするために当該デーモンをsystemctl stop/daemon-reload/startしました。

 sleepを入れたのは、ショートと同じようなことになるようで入れないと機能しなかった(成功率がめちゃめちゃ下がって検証にもならなかった)からです。

 あとPC/Debian Busterには入っていた一方、Raspberry Pi/Raspbian Busterには入っていなかったlibwebsockets8を一応インストール(apt install)しておきました。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import websocket
import _thread
import time
import sys
 
def on_message(ws, message):
  print(message)
 
def on_error(ws, error):
  print(error)
 
def on_close(ws):
  print("### closed ###")
 
def on_open(ws):
  def run(*args):
    arg = sys.argv
    ws.send(arg[1])
    time.sleep(1)
    ws.close()
    #print("thread terminating...")
  _thread.start_new_thread(run, ())
 
 
if __name__ == "__main__":
  # websocket.enableTrace(True)
  ws = websocket.WebSocketApp("ws://ESP_GADGET.local:81/ws",
        on_message=on_message,
        on_error=on_error,
        on_close=on_close)
  ws.on_open = on_open
  ws.run_forever()

 ロングバージョン(Long-lived connection)は、こんな感じになります。

 引数をとるべく、sysをimport、Python3では、threadに前置詞アンスコを付けた_threadになった模様、自身は3しか使わないのでこのように変更(あれ、importしても省略できないんだ...)、threading...の出力、トレースは不要なのでコメントアウトしました。

 前述のようにスクリプトを直接実行(./any_script.py 1 &)した場合、プロセスが残るのでkill/pkillしておく必要があります。

 音声操作時には、その必要がないように例えば、voicereceive.plにsleepとpkillを追記します。

2022/07/17

 気づけば、websocket-client-py/websocket-client-py3がバグっているのか、大幅に仕様が変わったのか、ショートバージョンはことごとく効かず、ロングバージョンは投げたら返ってこなかったり、エラーを吐くようになっていました。

USER@raspberrypi~$ pip install -U websockets
USER@raspberrypi~$ chmod +u rollscreen.py
USER@raspberrypi~$ cat rollscreen.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import asyncio
import websockets
import sys
 
arg = sys.argv
async def ws():
  uri = "ws://IP_ADDR_OR_mDNS:81/"
  async with websockets.connect(uri) as websocket:
    await websocket.send(arg[1])
    #return
 
asyncio.run(ws())
USER@raspberrypi~$

 そこでpip(pip3)でインストールできるwebsocketsバージョンにしたところ、returnを入れても入れなくても、いけました。

 ちなみに自身は、スマートロールスクリーン、スマートカーテン、スマートペンダントライト、スマート化した壁スイッチのシーリングライトなどをこれに入れ替えました。

 このところ、家電についてはスマホやパソコンからパネル操作ばかりで音声操作していなかったので気づきませんでしたが、いつ頃からだったんでしょうね?

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/TV1/Powerのようにブラウザからアクセスするだけで機能しますし、SPIFFS関連コードも一部書いてはあるも実装していませんが、handleルートにアクセスしたらindex.htmlを展開すれば、声でもブラウザからでもON/OFF以外の各種操作やESPリモコン回路を併用するなら他機器への操作を併用もできるでしょう。

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

実行

 半完成品状態の今回は、実行する場合、一部順不同な部分もあるも概ね以下のような手順となります。(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/11/21

 本文では、スマートスピーカー用のラズパイを買う前にLinuxパソコンでの話でした。

 が、いざ、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スクリプト内)
2019/06/02

 ALSAではなく、JuliusでPulseAudioを使うこともできました。

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%)にしてみたところ、そこそこで音量調節せずに使うならよいかなと思わなくもありませんが、やはりアンプ?でもかまさないといけないかとは思っています。

2018/12/20

 ラジオ機能追加に伴い、試しにここで使った手持ちのLogicool Z120BWを接続してみたら、音量問題もすっきりクリア、やっぱり、ある程度、ちゃんとしたスピーカーじゃないと...と思いました。

2018/12/20

 しばらく使ってみたら、スクリプト内で書くと実行時から更新されないということに気づくドジっぷり...日付時刻は、今回使ったWheather Hacks含め、Web APIから取得したものを使うか、dateコマンドから加工したスクリプト使った方がよさ気。

機能追加・修正

2018/12/22
2018/12/23

 ラズパイはまだもノートパソコンで自作スマートスピーカーの自動起動をやってみたら、思いの外、ハマりましたが、結果、/etc/rc.local等々を使う方法でできました。(詳細はリンク先参照)

2018/12/27

 ノートか、ラズパイか、何れのスマートスピーカーで使うか、使えるかは、まだわかりませんが、侮れないらしきダイソー300円スピーカーを買ってみたら確かに良品でした。(詳細はリンク先参照)

2018/12/28
Yahoo!ニュース読み上げ機能追加
RSSから取得のテキストとOpen JTalkラッパスクリプトによるpythonスクリプト
2019/01/04

 自作ラズパイスマートスピーカー自動起動設定完了、ノート同様にはできず、systemdで実装。

 今、自作スマートスピーカーは、リビングダイニング、キッチン用にしてあります。

 とりあえず、スマートスピーカ構成品の筐体として少し大きめのダンボール(Amazon発注品が入っていた片開きタイプ WxDxH:330x225x120mm)に入れてあります。

 ダンボール内には、転倒防止、保護クッション、遮熱?として滑り止めマットを敷き、USBメモリブートで冷却ファン付きケース入り、各所にヒートシンク装着のラズパイ3B+、スピーカLogicool Z120BWを並べ、後述の理由からマイクとしてマイク内蔵WebカメラELECOM UCAM-220FEは外出しにしてあります。

 スピーカーとマイクが反対(スピーカーの背面にマイクが来る)方向になるよう冷蔵庫の上にスピーカー、マイクが側面になるよう横向き?にし、キッチン側にマイクが来るように置いてあります。

 冷蔵庫は壁を背に上方、前面、側面は開放状態、冷蔵庫正面から見て右方向がキッチン、左方向がLD。

 音が篭もらないようスピーカーホーン部と前面にあるボリュームボタン部のダンボールは切り欠き、できる限り音声認識感度を広くとれるよう試行錯誤した結果、マイクとしてのWebカメラは、箱に入れず、冷蔵庫側面にぶら下げてあります。

 比較的室内の反響が良いためか、リビングでテレビがついていて、キッチンで換気扇が動いている程度の環境でウェイクワードを発する場合、それなりに声を張れば、3m程度離れても音声操作可能となっています。

 ただ、マイクと一定の距離があるとは言え、スピーカー音量を一定以上上げると操作音声を認識できず、反応しないし、相応のボリュームにしておき、マイクと反対になるスピーカー側から音声操作できることもある一方、原因不明も何かの折には、極力マイク付近に近寄らないと音声操作できないこともあります。

 少し甘めの評価かもしれませんが、それでも実用的と言ってよいレベルと認識しています。

 こうした設置場所の場合、例えば、献立考え中、調理中や洗い物中、食器の出し入れ中、掃き掃除や拭き掃除中、部屋、バス、トイレ、玄関に行く・戻る途中、もの思いにふけりつつ、家電操作したり、ニュースや天気、ラジオを聴くために音声操作できます。

 前述の通り、PCにもスマートスピーカ機能を入れていますが、記事やプログラム作成中、調べものなどネット閲覧中等々、キーボード、マウス操作中でも、ながら音声操作できます。

 もちろん、PCを起動すれば、SSHやVNCでラズパイスマートスピーカにアクセスし、新機能を搭載したり、編集したりと自由自在。

 当初、そんなもん要らないでしょと思っていた自身ですら、実際使ってみると、こうしたあらゆるシーンで何かしながら音声操作できることに利便性を感じ、とても重宝しています。

 試行錯誤中はそうもいきませんが、ある程度、一定の機能性を保持できるよう調整できた後は、時に感度がよくないことがあるとややストレスが溜まることもないわけでもありませんが、そこは自作した満足感も手伝ってか、かわいいものだと思えます。

 自作、自作と言っても様々な素晴らしい出来合いのものを組み合わせたに過ぎませんが。

2019/01/08

 ラズパイ起動/再起動/シャットダウン物理ボタンを追加。

 モニタやマウス・キーボードのないIoTデバイスとしてのラズパイ用に、別途PCを起動しアクセスしなくても起動、再起動、シャットダウンできる物理ボタンがあると便利。

 例えば、メインスクリプトは起動したのにJuliusの起動に失敗した(スマートスピーカーがうまく応答しないといった)時などには、ラズパイに専用の再起動ボタンがあると、コンセントの挿抜やスイッチON/OFFで電源を切るのは乱暴なのでシャットダウンボタンがあると、シャットダウンしてみたけど、やっぱり起動という時、コンセントの挿抜・スイッチOFF => ONするまでもなく、シャットダウンと同じボタンを押すことで起動できると...など。

 併せてラズパイACアダプタをスイッチ付きコンセントに挿し、シャットダウン後、コンセントのスイッチでOFF(電源断)できるようにしました。

 もちろん、スイッチ付きコンセントのスイッチONでもラズパイは起動します。

2019/01/24

 ラズパイスマートスピーカーでUPnP/DLNAサーバ上のメディア再生機能を追加。

 例えば、手持ちの音楽CDなどをリッピングしてサーバにアップしておけば、自分だけのオリジナルの音楽ライブラリを任意の音声操作で再生・停止できます。

 もちろん、メディアサーバのストレージが許す限り、音源追加は自由にできます。

 自身の場合、他サーバ機能含め、メディアサーバもラズパイ、現在、ストレージは、HDD 2TB。

 今は、「音楽かけて」と呼びかけると「CDコレクションを再生します」と応答後、再生が始まる(シャッフルし、再生する度に曲順が変わる)ようにしてあります。

2019/01/30

 ラジオ再生において、これまでの、らじる★らじるを含むradikoやサイマルラジオ各局の他、IcecastストリームからJazz、Classicに加え、Blues放送局を追加、呼びかけワードは、まんま「ブルース」。

 気がつけば、言いよどみがあり、記憶が曖昧だったのでjsayスクリプトで単に"睦月・如月・弥生..."といった旧暦の月を読ませるold_month.shを作成、追加、コールワードは、「旧暦の月」。

 尚、Open JTalkの読み上げにおいて「文月」の「ふづき」は、「ふみづき」と併せ、これも正しいと思われるので漢字のままも、「神無月」は「かみなづき」と読み、間違いではない気もしますが、一応、これだけ平仮名で「かんなづき」としました。

 これらを反映させるため、何れも応答スクリプト、Julius辞書への追記、Juliusオリジナル辞書ファイルエンコーディングをeucjpへ変換、systemctlでstop/disable/daemon-reload/enable/start。

 ちなみに先日、テレビでスマートホーム三昧の方が出ていてGoogle Homeの音声もチラッと流れていましたが、それを聞いた限りにおいては、明らかにOpen JTalkの方が、イントネーションに違和感がないように感じました。

2019/02/04

 仕組みからして元々、定型アクションを登録・実行できることを明記。

2019/02/05

 自作ラズパイスマートスピーカーでテレビを音声操作する方法の概要を追記。

2019/02/06

 ほぼ全て既存機能ながら以下の機能のページを起こしました。

天気予報読み上げ
Web APIから取得のテキストとOpen JTalkラッパスクリプトによるpythonスクリプト
日付時刻読み上げ
date/awk/sedコマンドとOpen JTalkラッパスクリプトによるshellスクリプト
十二支の読み上げ
テキストとOpen JTalkラッパスクリプトによるshellスクリプト
旧暦の月読み上げ
テキストとOpen JTalkラッパスクリプトによるshellスクリプト
2019/04/14

 自作ラズパイスマートスピーカーでエアコンを音声操作する方法の概要を追記。

2019/04/15

 リモコン付きラズパイスマートスピーカーで空気清浄機を音声操作と同じくスマートスピーカーでリモコン付き扇風機を音声操作する方法の概要を追記。

2019/04/29

 何れもリモコン非対応の自作スマートスピーカでサーキュレーターを音声操作自作ラズパイスマートスピーカーで扇風機を音声操作自作ラズパイスマートスピーカで蚊取器を音声操作する方法の概要を追記。

2019/05/22

 時・分指定(時間指定)、分指定(カウントダウン)できるタイマー機能追加

2019/05/23

 Julius、Juliusが起動済みであること前提の応答スクリプト共に起動したかと思いきや、たまに応答スクリプト起動直後にJuliusが落ちる現象に遭遇。

$ chmod +x julius_chk.sh
$ cat julius_chk.sh
#!/bin/sh
 
sleep 5
pgrep -f julius
if [ $? -ge 1 ]; then
  /path/to/jsay "ジュリアスを再起動しています"
  echo "Julius drop out Error"
  systemctl restart mysmartspeaker.service
else
  /path/to/jsay "準備ができました"
  echo "Julius OK"
fi
$

 運用でごまかして先送りしていましたが、向き合ってみたら、なんてことはない応答スクリプトから簡単なシェルスクリプト作って呼び出すことで対処できました。

 これまで応答スクリプトが起動した時点で応答スクリプト内で「音声操作の準備ができました」というメッセージを流していましたが、これを「音声操作の準備中」とし、直後に(Perlスクリプトなので)system("/path/to/julius_chk.sh &");としてチェックスクリプトを呼び、sleepで適当な秒数待ってpgrep -fでプロセスの有無を確認、ない場合、systemdサービスを再起動するだけ。

 もっと早くやっておけばよかった...。

2019/05/28
ラズベリーパイ 3 B+自作スマートスピーカーに音声メモ機能追加
音声メモ用perlスクリプト/shellスクリプト
ラズパイ 3 B+自作スマートスピーカーに伝言メッセージ機能追加
伝言メッセージ用perlスクリプト/shellスクリプト

 音声メモ機能と伝言メッセージ機能を追加しました。

 音声メモは、なんでもメモる、伝言メッセージは、伝言に限る、何れも複数件登録可能、削除時は、全件削除。

 実際には、保存先のファイルパスが違うだけで、どちらも同じですが、運用で使い分ければよいかなと。

2019/06/02
Julius / Open JTalkスマートスピーカーで OSS/ALSA/PulseAudio
Juliusを再.configure/make/make install

 音声メモと伝言機能追加に伴い、Juliusを少なくとも4.4では選べるPulseAudioを使うべく再コンパイルしました(かかる時間は極わずか)。

 OSSやALSAは、マイクやスピーカーといったハードウェアを占有してしまう性質があり、仮想デバイスとして同時に複数の使い道で使い分けることができませんが、PulseAudioなら、これらを使用しつつ、仮想デバイス化できます。

 これまで(OSSや)ALSAを使っていたため、音声メモや伝言メッセージ機能を追加したはいいのですが、いざ、使おうと思うとマイクとスピーカーを専有され、なんとスマートスピーカー機能側で使えないという状況が発生したわけです。

2019/10/24
ラズパイ自作スマートスピーカーでYouTube音楽のストリーミング
YouTubeの音楽プレイリストをストリーミング

 ログインすることなく、YouTubeの任意の音楽動画のプレイリストをyoutube-dlでダウンロード・保存、これをランダムに並べ替えつつ、youtube-dl/mplayerで各動画をストリーミングすることでシャッフル再生、副産物としてスキップ機能も実装することにしました。

 RadikoやICECASTで満足していたのですが、音楽に限らず、何か追加する機能ないかなと思いにふけった結果、なんとなく。

2019/10/30
PyGTK/Gladeでラズパイ自作スマートスピーカー用操作パネル作成
Python 3/GTK+でGUI操作パネルを実装

 ラズパイスマートスピーカーにモニタは搭載していませんが、ノートPCならPCに搭載のスマートスピーカー機能はもとより、sshでラズパイ側のスマートスピーカー機能をGUI操作できるわけで、あってもよいかなと。

2019/11/07
PyQt5/Qt Designerで自作スマートスピーカー用操作パネルを作成
Python 3/PyQt5/Qt DesignerでGUI操作パネルを実装

 より複雑なGUIパネルを作りたいと思い、GTKより、やや情報が多かったQtで作ってみました。

2020/01/09

 以下は、後述の2つの要因により、PC版ではなく、ラズパイ版スマートスピーカーのみで結果的に消失したfeedparserのインストールとmplayerの代替としてmpvを使うべくスクリプト修正することになった話です。

 たった今まで再生できていたICECASTやYouTubeの音楽の再生が突如としてできなくなったり、livedoor天気が聞ける一方、Yahooニュースが聞けなくなったりする事態に遭遇しました。

 当事象は、ラズパイ(OS:Raspbian)スマートスピーカーのみであり、PC(OS:Debian)に入れたスマートスピーカーでは何れも機能する、ラズパイスマートスピーカーにssh/scpアクセスはできる、ということで自作systemdサービスファイルやそこから呼ぶファイルの確認やsystemctl stop/disable/daemon-reload/start/enableを何度か試すも相変わらず。

 とりあえず、Yahooニュース取得スクリプトを眺めていたところ、そう言えば、これが、apt update/upgrade後に起きたことから、Debian StretchからBusterにアップグレードした際、pip3でインストール済みのfeedparserが、なぜか、消えていたことを思い出しました。

 思ったとおり、pip3 list | grep -i feedparserしてもなく、pip3 install feedparserしたところ、これを使っていたYahooニューススクリプトが機能するようになりました。

 続いてICECASTやYouTubeの取得再生スクリプトを眺め、何気なく、端末で直接mplayer URLしてみると[mplayer: relocation error: mplayer: symbol av_alloc_vdpaucontext version LIBAVCODEC_58 not defined in file libavcodec.so.58 with link time reference]なる見慣れないエラーが。

 検索してみると昔からたまに起きていたmplayerのバグらしく、一瞬、ダウングレード...とも思い、Raspbianながら、DebianのAPT_PREFERENCES(5)を見ると積極的にやりたい方法じゃないな...と。

 そう思いつつ、そう言えば、mpvってMPlayer/MPlayer2ベースの動画含むメディアプレイヤーだったような...。

 ならば代替が利くのでは...。

 ということで各種スクリプトのmplayer部分をmpv(前者の-novideoオプションは、後者の--no-videoオプション)に替えてみると無事、機能するようになりました。

 また、ラジオ等停止スクリプトにpkill -f mpvを追記、YouTubeプレイリストのスキップ用スクリプトは、mplayerではプロセスがyoutube-dlでしたが、mpvでは、mpvとしてあったため、こちらもpkill -f mpvとしました。

 というわけで、これらの事象は、アップグレード中になぜか、feedparserが消失したこと、本体もしくは依存関係にあるパッケージがアップグレードしたからかmplayer([arch]armhf/[バージョン]2:1.3.0-8+b3)のバグの2点が原因でした。

 ちなみに問題のなかったPC/Debian amd64の方のmplayerのバージョンは、2:1.3.0-8+b4でした。

2020/05/23

 今日、PC版スマートスピーカー機能においてもICECAST、YouTube共にmplayerだと正常に再生できず、mpvに変更することになりましたが、以前確認したバージョン(2:1.3.0-8+b4)と変更がないことから、mplayerのバグに起因するものではないようです。

 尚、スクリプト修正後、systemctl restart ...が必要でした。

2020/02/25

 自作ラズパイスマートスピーカーで自作電動ロールスクリーンを音声操作する方法の概要を追記。

2020/03/01

 自作ラズパイスマートスピーカーで壁面照明スイッチを音声操作する方法の概要を追記。

2020/03/07

 自作ラズパイスマートスピーカーでペンダントライトを音声操作する方法の概要を追記。

2020/04/23

 自作ラズパイスマートスピーカで電動部自作カーテンを音声操作する方法の概要を追記。

2021/09/20
Julius/Open JTalkスマートスピーカーからIPカメラの映像を表示
カメラ映像表示機能追加

 ラズパイ+ZoneMinderによるカメラ映像を音声操作とGUIスクリプトパネルから操作できるようにしました。

2021/09/23
自作スマートスピーカーからビデオ会議や内線通話...etc.を開始
リモート会議画面・内線通話画面表示機能追加

 スマートスピーカー機能を入れたLinuxパソコンやラズパイから音声操作や自作Qt GUIパネル操作でJitsi Meetによるビデオ会議、内線ビデオ通話画面(ブラウザ)を起動できるようにしてみました。

 インターネット経由では自端末のみ、内線通話を意図して同一ネットワーク内であれば、自端末だけでなく、起動中の他端末のJitsi Meet画面を相互に開くようにしています。

 スマホやタブレットもJitsi Meetアプリ必須ではありますが、もちろん参加可。

2021/11/14

 YouTube、radiko、ABC World News BBC World News、internet-radio.com、ICECAST、自前カセット、CDやMDなどをリッピングしたものなどのストリーミング再生において、なるべく均一になるように音量を調整してみました。

 先日、カセットテープをリッピングしてサーバにアップしてみたら、相応に出力調整はしたのですが、他と比し、出力音量が小さめだったので、これに合わせようかと。

 他のオプションもありそうですが、mpvは[--volume=val]、mplayerやffplayは[-volume val]で調整しました。

2021/11/26

 機能追加というわけではないのですが、スマートスピーカーの設置場所はそのままに、Bluetoothスピーカーを使って電波到達範囲において聴取範囲を変更できるようにしました。

 原因不明のBluetooth接続後、即切れ対策のスクリプトをcronで走らせることでBluetoothスピーカーのON/OFFでBluetoothスピーカーとスマートスピーカー側スピーカーの自動切り替え、Bluetoothスピーカーの電源が入った状態でスマートスピーカー再起動後にBluetoothスピーカーで再生も可能になりました。

2022/06/27

 ラズパイスマートスピーカーのシステムディスクとして使っていたUSBメモリ内のシステムがおかしなことになり、Raspberry Pi Imagerで同じUSBメモリに焼き直し、イチから構築し直しました。

 同じUSBメモリへの焼き直しは、若干迷いましたが、apt upgradeをさぼって対象が100件超えていたところでlibc6の依存関係エラーからの/var/lib/dpkg/info内の*.listファイルの最後に改行がないエラー、1つ1つ入れていき、これを解消するも他のエラー...でお手上げとなり、USBメモリ自体というより、システムだよねということで。

 Juliusについては現時点の最新バージョン4.6をセットアップ、UTF-8に統一されており、dictationキットの辞書も小細工は不要、他方、マイナーチェンジがあったのか、Raspberry Pi OSにおいてデフォルトのサウンドサーバがpulseaudioからalsaに戻っており、一時消滅していたconfigureオプション[--with-mictype=]が復活していました。

 結果、Juliusのコンパイルは、次のようにしました。

$ ./configure --enable-words-int --build=aarch64-unknown-linux-gnu --with-mictype=pulseaudio

 [--enable-words-int]は、githubにあったので、[--build=aarch64-unknown-linux-gnu]は、./app/julius/support/config.guessの出力結果、そして復活していた[--with-mictype=pulseaudio]。

 当初、[--enable-words-int]だけでやってみると[configure: error: cannot guess build type; you must specify one]、途中のメッセージに沿ってconfig.guessとconfig.subの対処するも、再実行すると先のエラーに加え、[configure: error: ./configure failed for jcontrol]。

 How to resolve configure guessing build type failure?の通り、automakeをインストール、/usr/share/automake*/にあるconfig.guessとconfig.subに差し替えても変わらず、と思いきや、そのリンク先の[For arm64]にある通り、先の値を[--build]オプションに渡してやってみたところ解消・解決、加えてpulseaudio対応した次第。

2023/06/03

 先日、赤外線リモコン付きから買い替えたスマート家電の空気清浄機、必須となったという会員登録・ログイン・クラウドを回避しつつ、ブラウザ操作パネルと操作スクリプトを自作してみました。

 というか、このJulius/Open JTalkスマートスピーカー用スクリプトからスマート空気清浄機用の自作スクリプトをファイル名+引数で操作できるようにし、Amazon AlexaやGoogle Homeでなく、Julius/Open JTalkスマートスピーカー専用機からでも、同機能を入れた2台のパソコンからでも音声操作できるように、そうしました。

 また、スマート家電空気清浄機用の自作操作パネルはWebサーバを建てIPアドレスやmDNSで呼べるようにしてJulius/Open JTalkスマートスピーカー用に作ったQt操作パネルやESP32ボード上のスマートホーム操作パネルからも簡単に呼べるように作ってみた次第。

2023/10/08

 購入から5年ちょい、なんちゃらカメラ用に仕込んでいたラズパイ3B+を1台ショートさせ壊してしまったのでスマートスピーカーの3B+を回すべく、Orange Pi Zero 3にJulius/Open JTalkスマートスピーカーを移行しました。

自作スマートスピーカーのメリットになり得そうなこと

2019/01/10

 あ、家電操作を含めるとスマートリモコンも買わないといけないのか...すると更に1.5〜2倍くらいだとして3万円前後になる?

 そうだとすると自作は価格メリットもあるか...。

 それに実際使ってみると自作のメリットは意外と多いかも。

 Google AsistantやAmazon Alexaなどもラズパイに実装できるため、ラズパイに注目すると焦点がズレる可能性があり、スマートスピーカーについては、音声認識・音声合成がローカル上のJulius・Open JTalkかクラウド上のAIかの比較といえるかも。

  • スマートスピーカー・スマートリモコンやスマートコンセント(全て自作)を合わせると市販品と遜色ないか安価な可能性
  • ラズパイとESPモジュールなどハードウェアを除き、ソフトウェアは全て無料のフリーソフトウェアを選択可能
    • その場合、クラウドのような使用回数や時間制限超過による課金もなく、何を気にすることなく安心して使える
    • Web APIやRSSを利用した機能拡張が可能
    • API/RSS以外にも自身で作ることさえできれば機能拡張は、自由自在で柔軟に対応可能(AIも例外ではない)
      • ちなみにJulius自体は、DNN(Deep Neural Network≒Deep learning)にも対応済み
  • 自作スマートスピーカー機能はパソコンにも搭載可能なのでUSBマイクとUSBスピーカーさえあれば(内蔵スピーカーでも良いのですが、マイクが音を拾ってしまいやすいなど位置関係が微妙)、PCの電源を入れてある間は、その部屋では、(別途)ハードウェアとしてのスマートスピーカを用意する必要はない
  • ウェイクワードや操作文言・フレーズの制約はなく自由に設定可能
  • クラウドを使わない仕様にすれば、余計なセキュリティリスクの可能性を回避できる
  • あえて買い物機能を搭載しない限り、子どもがダダをこねて発した音声など誤操作で不要なものを買ってしまったというような事態を回避できる
  • 家電操作にESPモジュール回路は必要も別途市販スマートリモコン機器を買う必要はない
    • 家電数台なら市販スマートリモコンより、ESPモジュール回路の方が、安価に調達できる可能性あり
    • 市販スマートリモコンは本体から信号を出力する為、その周辺の赤外線リモコン家電対象ですが、ESPモジュール回路なら部屋やフロアが違っても操作可能
    • もちろん市販品 or 自作スマートコンセントを使えば、非赤外線リモコン家電操作も可能
    • ESPモジュール回路なら音声操作だけでなく、WiFiなどでつながり、同一ネットワーク上にあるパソコン・スマホ・タブレット上のブラウザから操作することも可能
  • 筐体や構成部品の制約はない為、マイクやスピーカーの選定、マイク感度含め、試行錯誤しつつも思い通りにできる

 どっちにしても誤操作については、考慮しておく必要がありますが、在宅時は対処できるとして外出時に電源をOFFにするという運用をすれば問題ないでしょう。

 一方、スマートスピーカーに限らず、外からの家電操作を行なうことも不可能ではないし、スマートロックとカメラによる確認は便利かもと思うものの、特段必要性を感じないという以前に疑問符もあり、現時点では、実装していません。

  • 家にはたいてい誰かしら人がいて、そもそも外から家電操作する必要がない可能性(見守りカメラを除く)
  • 転倒時、電源OFF機能があっても、そもそも転倒の可能性がある家電や火元や事故の原因になり得る家電を外出先から操作するのは、怖い為、対象から除外する必要がある
  • エアコンの運転は、外窓や室内ドアなどの開閉状態によっては、無駄になる可能性
  • エアコンの運転状態(自動・除湿・冷房・暖房、温度設定)など実際の運転状態の確認方法は?(真夏に暖房・真冬に冷房とか怖すぎる)
  • 明示的に電源ONしたテレビ・ステレオ・ラジオなどの音量確認には、監視カメラ内蔵マイクを使う?
  • 誤動作で電源ONしたテレビ・ステレオ・ラジオなどの音量が大きすぎて近所迷惑になる、無駄な電源ONの可能性の確実な排除方法は?
  • セキュリティリスク
    • 勝手に操作されるリスク
    • 巡り巡ってクラウドや自宅回線へ侵入されるリスク
    • GPSを使う機能の場合、居場所を特定されるリスク(自宅付近にいるのか、いないのか...いないなら泥棒...とか)
    • 見守りカメラや監視カメラを悪用されるリスク(誰かに盗聴されていたり、覗かれていたりしたら最悪)
    • ...etc.

 この中の一部については、不在時のタイマー起動にもあてはまるものも。

備考

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をつないでおいてもラズパイが起動する...。(いや、勘違い2B+のブートはmicroSDで自身の場合、システムはUSBもmicroSDから固有のものを呼んでいるからUSB内のシステムが起動するのは当たり前...。)

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

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

 後日、試してみましたが同じでした...、となるとRaspberry Pi 3 Model B+とUSB HDDブートでも触れたように電源容量関係かもしれません。

2019/08/27

 今回、ARM クアッドコアCPU 1.4GHz、1GB RAMのRaspberry Pi 3 Model B+でスマートスピーカーを作ったわけですが、一方でIntel Celeron デュアルコアCPU、4GB RAMのノートPCにも同じスマートスピーカー機能を入れています。

 そんな中、ふと気づいたのが、ノートPCに比し、ラズパイの方は、インターネットラジオ再生に若干遅延がある(同じ局を再生すると輪唱のようになる)こと。

 だからと言って再生速度が遅い(スロー再生される)わけではないので困ることは一つもないですけど。

 ちなみに同じ光ONUに有線接続した無線LANルーターを介し、ルーターから同じ距離に置いたPCとラズパイ(スマートスピーカー)で比較してみた結果、WiFi電波到達範囲内においては距離は関係ないようです。

 内蔵無線LANドライバの違いの可能性もなくもないかとも思わなくもありませんが、無線LANでの動画ストリーミング再生遅延にはRAM増強が有効であるという実体験からしてCPUはさておき、PCは4GB、ラズパイは1GBというRAM容量の違いが、この遅延に起因しているのではないかと思っています。

 技適はまだな模様もRAM1GB版に加え、2GB、4GB(、8GBもありそうな雰囲気)もあるラズパイ4Bなら、この仮説を検証できるわけですが、今のところ、その予定はありません。

2019/12/28

 前述の通り、Raspberry Pi 3 Model B+とWeb|USBカメラLogicool C270の相性がよろしくないのでC270をメインのノートPCで、ラズパイスマートスピーカーではELECOM UCAM C0220FE(C0220FEWH)をつかっていました。

 が、ここにきてC270のカメラ機能が壊れたこともあり、昨日、ヨドバシ(yodobashi.com)でBUFFALO BSWHD06M/BSWHD06MWH [マイク内蔵120万画素Webカメラ HD720p対応モデル ホワイト]を買ってみたところ、より感度が良さそうなこともあり、これをラズパイスマートスピーカーのマイクとして使うことにし、UCAM-C0220FEをメインPC用としました。

2021/10/23

 自作スマートスピーカー製作以来、唯一、piアカウント使ってしまっていたのですが、ここでpiを削除、新規アカウントで運用することにしました。

 運用中の削除は、そこそこ面倒、セキュリティ面からは必須なので新規アカウント登録、piアカウントの削除は、初めに行っておきましょう。

 ただ、他にも次のようにやることはあるものの、後で気づけば、piユーザを使えないようにするは、良い手かも。

 運用アカウント変更に伴うパス変更。

  1. /etc/systemd/system/スマートスピーカー用自動起動サービスファイル
  2. 1から呼ぶファイル
  3. /etc/fstabのSambaサーバnoautoマウント行の.smb_credentialsファイルパス
  4. jsayスクリプト(出力ファイルパスなど)
  5. 応答スクリプト(jsay含め各種スクリプトファイルパスなど)
  6. 各種スクリプトの内のファイルパス
    例)$ cd targetPath;for file in $((ls .));do sed -i -e 's!/pi/!/new_user/! $file;done'
    単なるpiの場合、pingなどにも含まれるので一括置換時は要注意。
  7. ラズパイboot/reboot/shutdownボタン回路用systemdサービスファイル

 piアカウントが存在する状態で先に新規アカウントを作ってしまった場合。

 リンク先に倣ってユーザー名piを移行先運用アカウント名に変更。

  1. ログイン名を変更
    sudo usermod -l new_user pi
  2. ホームディレクトリを変更
    sudo usermod -d /home/new_user -m new_user
  3. グループ名を変更
    sudo groupmod -n new_user pi
  4. 移行先アカウントのパスワード設定
    sudo passwd new_user

 この時、piアカウントが存在する状態で先に新規アカウントを作ってしまった場合、1は既に存在すると言われる。

 piアカウントの権限削除。

  1. /etc/group内各グループのpiアカウント削除
  2. /etc/passwd内のpiアカウント削除
  3. sudoersファイル内のpi関連権限を削除

 改めて、ここでpiアカウントが存在する状態で先に新規アカウントを作ってしまってnew_userにpiとは別のgid/uidを割り振られている場合。

 気のせいか、bullseyeだからか、useradd -sでログインシェルを指定することは久しくなかった気がするのですが、省略したらシステムデフォルトとして純粋な/bin/shが選択されていることを上キーで文字化けし、historyコマンドがないことで気づくに至り、シェル変更。

 ssh経由で電源を落とすべく、ユーザーアカウントからパスワードなしでsudoを使いたいので/etc/sudoers.d/010_pi-nopasswdを次のようにした。

 あとは念の為、スマートスピーカーへのsshで楽するため、スマートスピーカーのホスト上でrm /home/new_user/.ssh/*後、他のホストから必要に応じて(ssh-keygen/)ssh-copy-id -i 公開鍵.pub スマートスピーカー.localやIP

 このくらいだったかな。

ホーム前へ次へ