make the smart speaker smarter 12

11/16

Index

Cooperate with Google

さてと、IFTTTと連携して
色んなことさせようと考えたけど
即時性を要求される機能はどうしても
タイムラグがあり、実用性に欠ける。

なので、今回はIFTTTネタは置いといて
Googleアシスタントの機能を使って
能動的にスマートスピーカーを使ってみたいと
思う。

speak to her, talk to me

今まで、自発的に情報を教えてくれる仕組みを
組んできたが、一旦それは置いといて
こっちから聞いたことに対して
応えてくれる仕組みを構築する。

例えば

私「電気つけて。」
スピーカー「、、くださいは?」
私「電気つけてください。」
スピーカー「電気をおつけください、Google Homeさまだろうが、この糞ゴミ虫がっ。」
私「電気をおつけください、Google Homeさま。」
スピーカー「貴様のようなゴキブリ風情は豆電球で十分だ。」
電気が付く
スピーカー「どうだ?明るいか?これが私の力だ。感謝するがいい。」

みたいなのはどうだろう。
部屋の電気つけるだけで、毎回このやり取り。

あとは

私「おやすみ」
スピーカー「今日も夜遅くまで自宅警備ご苦労さまニート。いい夢見てニート。明日は午後何時に起きるニート?快眠のために求人情報を調べニート?」

そこかしこに悪意を付けるとか。

Actions on Google V2

ま、そんな下らんこともできそうなのが
Actions on Googleというサービス。
要はGoogleが用意するSaaSで
オリジナルのGoogleアシスタントが作れる
サービス。

これに、Googleアシスタントのレシピを
構築できるDialogflowというサービスを
組み合わせて、家で構築したIoTの仕組みを
外部から操作できるようだ。

こりゃ、使ってみない手はない。

ザックリ手順を書くと

  1. Actions on Googleでプロジェクトを作成
  2. Invocation(アシスタントの呼び出し方)を決定
  3. Dialogflow(振る舞い方)によるActions(行動内容)を決定
  4. Entitiesを適宜定義
  5. Intents(会話の内容)を決定
  6. Webhooksに家のAPIを設定
  7. 家のAPI修正

こんな感じかな。
早速設定してみる。

Configuration

Actions on Google

まずはプロジェクトの作成。
Raspberry Piを使っていろいろ遊んでいるので
今回もラズパイにちなんでDoctor PieAreって
名前にしようと思う。縮めてDr.PieR。
ドクターパイアールってなんかちょっとエッチな
響きでよい。


こんな感じ?古いかな。最近だと、、

こちらかな。そんなことどうでもいいか。

てことで、ドクターパイアールで決定。

fig.1

つぎはInvocation。
Display Name(アシスタント名)を設定。
ユニークにした方が良いと思い
ドクターパイアールに設定。ユニークなのかは疑問。

fig.2

実際に呼び出すときは

OK Google、ドクターパイアールにつないで

となる。
声は女性。Female 2というのを選択。
恐らくロボットみたいな音声であろう。

次に、Actionsを設定。
Dialogflowにて詳細を設定するので
ConversationalなDialogflowを選ぶ。

fig.3

でアシスタントの詳細を設定するところも
ある。パイアールのInfomationと
Descriptionsとか、呼び出したときに
表示させる、192x192pixelの画像とか。
それらを全部入れたら、やっとビルドできる
ようになる。

fig.4

fig.5

使える国も選べるようだ。
今回はJapanのみにチェックを入れている。

fig.6

パイ先生を呼び出せるデバイスも限定できる。
ここでは敢えて制限はせずにいく。

で、V2から仕様が変わったのか個人利用のための
Draftとしてビルドするようなことができなくなった。
ステージングとしてAlpha、Beta、Production
の3環境があり、それぞれ使われるユーザ数の
スコープが違うようだ。

fig.7

ただ、Alpha利用をしてもらうユーザを
開発者が選択して、Inviteできるようなので
自分をInviteしたら試用できそうだ。
Productionなんてしたもんなら
誰でも私の部屋を操作できる恐ろしい事態に
なりかねない。恐ろしや。

さて、Actions on Googleの部分は
ここまでにして、続いてはDialogflowに
移ることにする。もう少し具体的な
構築になっていく。

Dialogflow

DialogflowではまずAgentを作る。
ここではNewAgentというダサい名前に
してしまった。

次に、Entitiesを設定する。
これはアシスタントとの会話で使われる
話題をシステムで効率よく使うために
予め設定しておくところ。
例えば、私の場合は、室温とか電気とか
人とか、NASとかがキーワードとして
でてくる。
このEntitiesの賢いところは
会話によっては、同じ意味でも違う言い回しが
あるので、それらを1つのEntityとして
認識させて、同じ結果を得られるように
頑張ることができる。

私の場合、「電気をつけて」っていい方もあれば
「でんきつけて」「部屋の明かりをつけて」
「あかりつけて」とかいろいろあると思う。
これらを予め同じことを言っているんだと設定しておく。
AIでもなんでもない。予定調和でしか物事は
進まないのである。なるべくしてなるそれが人生。

私の場合は以下の通りのEntityを作成した。

fig.8

fig.9
EntLight (light: 電気、明かり)

fig.10
EntRoomTemp (roomtemp: 部屋の温度、室温)

これ以外にも

  • EntIsSomebody (isSomebodyHere: 人がいる、wasSombodyHere: 人がいた)
  • EntNAS (nasPower: NASの電源)

こんなのも作っている。

次にIntentsを設定する。
それぞれにTraining Phrases(質問事項)と
Action and Parameters(APIに渡す情報)と
Response(回答内容)を設定する。
大事なのはAPIに渡す情報とAPIから返す情報。
ここではParametersが大事である。
今回設定したのは7つ。

fig.11

  1. RoomLightOff
  2. RoomLightOn
  3. RoomTemp
  4. IsSomebodyHere
  5. WasSomebodyHere
  6. nasPowerOn
  7. nasPowerOff

名前を見ればなんとなく分かると思うが
1は電気を消す、2は電気をつける、3は室温
だが、それ以降はちょっと説明が要りそう。

fig.14
参考までに電気を消すやつを。

Somebody There?

ラズパイに人感センサーを付けたので
DBに時系列で0,1を蓄積しているのだが
正に質問したときにそれをチェックして
1なら「居るよ」、0なら「いないです」を
回答するように。それが4のIsSomebodyHere。

fig.12
参考までに。

それとは少し変えて、過去に人がいたかを
チェックするのが、5のWasSomebodyHere。
過去10分以内をチェックするようにしてみる。

Need to Access Smartfully

次2つが家の某闘牛製NASの電源の入切。

電源ONはマジックパケットを送ることで
WOL対応のNICにモーニングコールする。
これは簡単なNode.jsのスクリプトで
実現。こういうライブラリが豊富にあるので
Node.jsは思考を止めずにサクサク進めるのが
好きだ。

const wol=require('wake_on_lan');
const hwaddr='FF:FF:FF:FF:FF';
var opts={
	port: 1304,
	address: '10.1.254.2',
};
wol.wake(hwaddr, opts, (err) => {
	if(err){
		console.log(err);
	}else{
		console.log('succeeded');
	}
});

こんな感じ。簡単すぎ。

電源OFFは、NASにてSSHが有効になっている
のでshutdownコマンドを投げるだけ。
簡単だ。これもexec関数であっさり。

const sshcmd='ssh';
const sshuser='root';
const sshserver='10.1.253.1';
const sshargs='/usr/local/bin/shutdown.sh';
exec(sshcmd+' '+sshuser+'@'+sshserver+' '+sshargs+'',(err, stdout, stderr) => {
	if(err){
		console.log(err);
	}else{
		console.log('succeeded');
	}
});

ラズパイユーザのssh公開鍵はNASに登録済み。

fig.13
参考までに。

これらが6のnasPowerOnと7のnasPowerOff。

Optimizing API service

我が家のラズパイのAPIサーバを改造する。
以前までの記事で紹介している
api.jsに手を入れる。

今回Dialogflowから渡されるURIの
振る舞いを構築するのだが
Node.jsだから、、というわけでも
ないのだが、最近流行りのREST API
というのを使っているので、
やりとりは全部JSON形式となる。

Node.jsはJSON.parse()で簡単に
アクセスできるので、ちゃちゃっと
作ってしまう。

/**
 *
 * for DialogFlow
 *
 */
app.post('/uuu/', (req, res) => {
    let resp=JSON.parse('{ "payload": { "google": { "expectUserResponse": true, "richResponse": { "items": [ { "simpleResponse": { "textToSpeech": "すみません。わかりません。" } } ] } } } }');

    if(!req.body.queryResult){
        res.header('Content-Type', 'application/json; charset=utf-8');
        res.status(400).send(resp);
    }   
    const getType = req.body.queryResult.parameters.getType;
    if (!getType) {
        res.header('Content-Type', 'application/json; charset=utf-8');
        res.status(400).send(resp);
    }   

    if (getType=="light-on"||getType=="light-off") {
        let textMessage=(getType=="light-on")?"電気をつけました":"電気を消しました";
        var filename=getType;
        exec('/usr/local/bin/kuro-rs-send /usr/local/share/kuro-rs/data/'+filename+' 2',(err, stdout, stderr) => {
            console.log(stderr);
            console.error(err);
        }); 
        resp.payload.google.richResponse.items[0].simpleResponse.textToSpeech=textMessage;
        res.header('Content-Type', 'application/json; charset=utf-8');
        res.status(200).send(resp);
        res.end();
    }else if(getType=="temp") {
        var getTemp=async () => {
            var temp=[];
            var j=await new jdg();
            temp=await j.getTemp();
            let textMessage="室温は"+Math.ceil(temp[0]-10)+"度です。";
            resp.payload.google.richResponse.items[0].simpleResponse.textToSpeech=textMessage;
            res.header('Content-Type', 'application/json; charset=utf-8');
            res.status(200).send(resp);
            res.end();
        }
        getTemp();
    }else if(getType=="isSomebodyHere"||getType=="wasSomebodyHere"){
        var getMotionData=async (stype) => {
            let textMessage;
            let ison=false;
            var j=await new jdg();
            if(stype=="isSomebodyHere"){
                ison=await j.isOn();
            }else{
                ison=await j.wasOn();
            }
            if(ison){
                textMessage=(stype=="isSomebodyHere")?"今、部屋に誰かがいます。":"部屋に人がいたかもしれません。";
            }else{
                textMessage=(stype=="isSomebodyHere")?"部屋には誰もいません。":"誰もいませんでした。";
            }
            resp.payload.google.richResponse.items[0].simpleResponse.textToSpeech=textMessage;
            res.header('Content-Type', 'application/json; charset=utf-8');
            res.status(200).send(resp);
            res.end();
        }
        getMotionData(getType);
    }else{
        res.header('Content-Type', 'application/json; charset=utf-8');
        res.status(400).send(resp);
    }
});

今回は抜粋。
こんな感じで簡単に書いている。
BASIC認証は通っている前提で、また、ソース内のjdg()というのは
以前構築したJudge.jsというモジュールで、センサーデバイスを使った
あらゆる評価をするモジュール。
そのうちこのJudgeモジュールで実装したisOn()とwasOn()は説明して
いこうかな。

これでapi.jsを再起動しておく。

Webhooks

最後に、今改修したapi.jsをDialogflowで設定する。

fig.15

こんな感じ。
さて、これで準備ができたと思う。

Usage

現在のところ、パイ先生ができることと
これから実装できそうなものは以下の通り。

  • 室温を答える
  • 部屋の電気をつけたり消したり
  • 部屋に人がいるか
  • 部屋に人がいたか
  • NASの電源の入切
  • お気に入りさんのつぶやきがあるか
  • お気に入りさんの動画がアップされてるか
  • 不正アクセスがあるか

上5つは今実装した。
それ以外もすぐにできそう。
これは私が思い描いていた
スマートホームに近づいているような
気がする。
ワクワクが押し寄せてくる。

さて、実際に使ってみる。
やり方は

私「OK Google、ドクターパイアールにつないで。」
スピーカー「すみません、わかりません。」

あれ?つながらない。
手元のPixelでも試してみる。

私「OK Google、ドクターパイアールにつないで。」
Pixel「すみません、わかりません。」

エー。なんじゃそれ。
よく見てみると、ドクターパイアールのつづりをみてみると

ドクターパイ アール

あれ?パイとアールにスペースがある。なんと。
で、前述の設定画面1をみていただくとわかるが
Dr.PieRのpronunciation(発音)を設定しており
「ドクターパイ アール」としている。
スクショを取ったときは事後だったのだが
このテストをしたときは「ドクターパイアール」と
していた。これが原因で、違う言葉として認識された
というのがオチ。

さて、設定し直してみた。

私「OK Google、ドクターパイアールにつないで。」
スピーカー「すみません、わかりません。」

なんでやねん。意味わからん。
手元のPixelでも試してみる。

私「OK Google、ドクターパイアールにつないで。」
Pixel「わかりました。ドクターパイ アールのテストバージョンです。」
パイ「こんにちは。」

あれ?できる。なんじゃこりゃ?
ま、でも出来そうだからテストしてみる。

私「電気をつけて」
電気が付く
パイ「電気をつけました。」
私「電気を消して」
電気が消える
パイ「電気を消しました。」
私「部屋の温度は」
パイ「室温は21度です。」
私「人がいる?」
パイ「今、部屋に人がいます。」
私「NASの電源を入れて」
パイ「NASの電源を入れます。」
NASが動き出す。
私「キャンセル」
パイが退出

ほぼタイムラグなし。すごいレスポンス。
声でも文字でもすぐに返ってくる。
IFTTTで同じことしようしてもここまで早くはない。
これは使えるってレベルではない。すごいと思う。
しかもGoogleアシスタントを使うので
もはやGoogle Homeを使わなくてもよいし
リモートからでもできてしまう。

前述2にもあるが、そこらへんは制限を
かけられるようだが、便利なことには変わりない。

ただ、難点が2つほど。
パイ先生と話をするためには
パイ先生と話をする意思表示が必要。
最初の
「ドクターパイアールにつないで」
をどうにかしたい。

Google Homeの設定で
別の言葉として認識させることは出来そう。

もう一つ、REGZAを操作するときに学んだ
「OK Google、ドクターパイアールに部屋の温度はと聞いて」
という聞き方は通じる。

だがしかしもう一つの難点があり
今回Dialogflowの設定で
一度パイ先生に聞くと次のコマンドも待機する
ようにしたことで、
毎回最後にキャンセルといわないと
いけない。

難点を解決するには一問一答形式にして
聞くときは全部「パイに〜と聞いて」という
聞き方を貫くことで回避はできそう。

ただ、日本語との言語としての
体系の違いが理由なんだろうけど
リファレンスを読んでいても英語の方が
やりやすいのは明白。

例えば、さっきの

OK Google、ドクターパイアールに部屋の温度はと聞いて

といういい方は文字に起こすと
カギカッコ付けたくなるような
読みづらいものになる。

これが英語だと

OK Google, Talk to Dr.PieR for room’s temperature.

こうなる。見た目にも非常にわかりやすい。
Talk toとforがあることと、
スペースがあるので区切りやすいし理解もしやすい。

今回、リファレンスは全て英語だったが
この点が非常にわかりやすかった。

Next to…

さて次はどうしようかな。
かなりボリュームがでかくなったので少し休もう。

もう少し賢くしたいので面白いことが
あれば考えてみる。


  1. 1.fig.1
  2. 2.fig.6

コメント: