2018年08月03日

Google Home を拠点間の双方向コミュニケーションに利用する

はてなブックマークに登録

ESP32 版 google-home-notifier のこと

google-home-notifier の ESP8266 移植版「esp8266-google-home-notifier」の作者である ほりひろ 様が、今年(2018年)6月に ESP32 への対応を実施されました。node.js プログラムであるオリジナルの google-home-notifier を実行可能なもっとも小振りなプラットフォームはラズベリーパイですが、さらにコンパクトで消費電力の少ない ESP32 ボードで自由に Google Home へのキャストを実現できることには大きな魅力があります。自宅ではこの素晴らしいライブラリを使って MQTT メッセージをトリガーにキャスティングを行う内容のエージェントを構成し様々な要件で利用させて頂いています。

「声のアバター」によるやりとり?

そんな中で先日ふと、google-home-notifier の「Google Home で任意の音を出せる機能」と、Google Home 側の「音声で所定の処理を実行できる機能」の組み合わせを、離れた拠点間での定型的なコミュニケーションに利用できるのではないか? と思い立ちました。図のようなイメージです。

双方のエージェントがそれぞれ MQTT ブローカー上の別のトピックを Subscribe しておき、必要に応じて相手のトピックへメッセージを Publish することで現地の Goole Home が所定のアナウンスを行います。その内容に対する所定のフレーズでの応答で相手へ向けての処理を発動する形にしておけば、いわば「声のアバター」を通じて両者間のやりとりが成立すると考えました。同じく音声による連絡方法であっても電話とは異なりこのやり方には直接的な拘束感やある種の重さがないため双方ともより手軽により淡々と利用できるでしょう。

試作のシナリオ

いろいろ使途がありそうですが、手元ではまず個人的に現在もっとも身近なテーマである「高齢者世帯の安否確認」を想定したシナリオを形にしてみました。以下の内容です。

A:見守り側
B:高齢者世帯側

  • A => Google Home A:「ねえ Google、安否を確認」
    • Google Home A => A:『はい、これから声をかけてみます』

  • Google Home B => B:『身のまわりは大丈夫ですか?問題がない場合は "ねえ Google、順調です" と話しかけて下さい。もし何かご相談があれば、"ねえ Google、コールして" と話しかけて下さい』

    • パターン 1: B => Google Home B:「ねえ Google、順調です」
      • Google Home B => B:『それはよかったです。さっそく大丈夫と伝えておきますね』
        • Google Home A => A:『先方から "元気です"と音声連絡がありました。メールを確認して下さい』
        • A あてにメールが届く


    • パターン 2: B => Google Home B:「ねえ Google、コールして」
      • Google Home B => B:『はい、すぐに電話をするようにと伝えます』
        • Google Home A => A:『先方から "電話してほしい"と音声連絡がありました。メールを確認して下さい』
        • A あてにメールが届き携帯電話が数回コールされる

相手が高齢者でもあるため Google Home からのアナウンスにはとりわけ「聞き取りやすさ」が求められます。定型の文言なので毎回音声合成を行う必然性もなく、ここでは、google-home-notifier の notify 関数から呼び出される Google 翻訳 TTS を使うのではなく、複数のオンライン TTS サービスを試した中で個人的にもっとも自然に感じられた音声を収録した MP3 データを play 関数で再生することにしました。

動作の様子

後出のプログラムリソース一式を使って二組の「Google Home Mini + エージェント」間でやりとりを行った様子の動画です。

(パターン 1: 「安否を確認」 〜 「順調です」 1分17秒

(パターン 2: 「安否を確認」 〜 「コールして」) 1分16秒

リソース

esp8266-google-home-notifier のカスタマイズ

前述のように手元では作成したエージェントをさまざまな要件で利用しており、この間にオリジナルの esp8266-google-home-notifier ライブラリのコードへ何点かプライベートに手を加えています。変更箇所は「#ifdef TANABE」部分で、fork した GitHub リポジトリへ反映しています。

$ git clone -b private https://github.com/mkttanabe/esp8266-google-home-notifier.git

変更内容

  1. 元の google-home-notifier に存在する ip 関数を追加
    • Google Home デバイスアドレスの mDNS 照会に比較的時間がかかるため IP アドレスの直指定もできると嬉しいため
  2. 元の google-home-notifier に存在する play 関数 (public)を追加
    • 手元では TTS 機能だけでなく MP3 再生機能も必要であるため
  3. play 関数による Google Home での MP3 データ再生終了を検知するためのコールバック機構を追加
    • 元の google-home-notifier にもないが欲しいと思っていた機能。理由は後日あらためて、、

エージェントプログラム

Arduino core for the ESP32 環境で作成したエージェントの Adruino スケッチです。MQTT ブローカーに Beebotte を使用しています。

  • GoogleHomeNotifierESP32Agent - github.com/mkttanabe
    GoogleHomeNotifierESP32Agent.ino の 以下の箇所を環境にあわせて書き替える
    //------- ユーザ定義 ------------------
    // Google Home デバイスの IP アドレス, デバイス名
    // 有効にすれば指定 IP アドレスを直接使用
    // 無効にすれば指定デバイス名で mDNS 照会
    #define USE_GH_IPADRESS

    IPAddress myGoogleHomeIPAddress(192,168,0,121);
    #define myGoogleHomeDeviceName "room01"

    // WiFi アクセスポイント
    #define ssid "ssid"
    #define password "pass"

    // Beebotte 情報
    #define mqtt_host "mqtt.beebotte.com"
    #define mqtt_port 8883
    #define mqtt_topic "test01/msg"
    #define mqtt_pass "token:" "token_************"

    // MP3 データを配置する Web サーバとデータパス
    #define dataServer "192.168.0.126"
    #define dataServerPort 80
    #define MP3DataFmt "http://" dataServer "/sound/%s.mp3"

    //-------------------------------------

    上の記述の場合、自分の Beebotte アカウントの Channel "test01", Resource "msg" のメッセージの data キーの値に応じて以下が行われる.
    • data 値の先頭文字がアスタリスクであれば音声合成用のテキストとみなして google-home-notifier の TTS 処理にかける
      • 例) {"data":"*こんにちは"} => 「こんにちは」と発話
    • data 値の先頭文字がアスタリスクでなければ MP3 ファイル名とみなし所定の Web サーバスペースの URL を編集して再生
      • 例) {"data":"file01"} => "http://192.168.0.126/sound/file01.mp3" をキャスト

用意した IFTTT アプレット

今回の試作では、トリガーに Google Assistant サービス、アクションに WebHooks サービスをアサインしたみっつの IFTTT アプレットを使っています。 ※ いずれもクリックで可読大表示

  • 問い合わせフレーズ「安否を確認」対向のアプレット
    アクションで Beebotte 上の相手側 "test02/msg" トピックへ "AnpiQuery.mp3" のキャスト再生を指示するメッセージを Publish
    トリガー
    アクション
  • 応答フレーズ「順調です」および「コールして」に対向のアプレット
    アクションで次項の Google Apps Script による Web アプリコードを所定のパラメータを添えて実行
    トリガー
    アクション
    トリガー
    アクション

Google Apps Script による Web アプリコード

上のふたつの応答用アプレットから呼び出している GAS コードです。

  • 「順調です」の場合
    アプレットから渡される res パラメータの値は「safe」。 Beebotte 上の相手側 "test01/msg" トピックへ "AnpiSafe.mp3" のキャスト再生を指示するメッセージを Publish して「元気です」と Gmail を送信
  • 「コールして」の場合
    アプレットから渡される res パラメータの値は「doCall」。まず Twillio 経由で相手の携帯電話を数回コールしてから Beebotte 上の相手側 "test01/msg" トピックへ "AnpiDoCall.mp3" のキャスト再生を指示するメッセージを Publish して「電話がほしい」と Gmail を送信


/*
  AnpiResponse
  2018-07
*/
var ADMIN = "***********@gmail.com";

function doGet(e) {
  return  ContentService.createTextOutput("??");
}

function doPost(e) {
  return doIt(e);
}

function doIt(e) {
  var msg, mp3Name;
  var res = e.parameter.res;
  var where = e.parameter.where;

  if (res == "safe") {
    msg = "元気です";
    mp3Name = "AnpiSafe";
  } else if (res == "doCall") {
    msg = "電話がほしい";
    mp3Name = "AnpiDoCall";
    doPhoneCall(); // twilio
  } else {
    return  ContentService.createTextOutput("res not found");
  }

  doPublish(mp3Name); // MQTT

  // gmail
  doSendMail(ADMIN, "Anpi", ADMIN, ADMIN,
           "安否連絡 [" + res + "]", 
           curDate() + " " + curTime() + "\n" +
           where + " より 「" + msg + "」と連絡がありました");

  return  ContentService.createTextOutput("OK");
}

function curDate() {
  var d = new Date();
  return d.getFullYear() + '-' +
    ('00' + (d.getMonth()+1)).slice(-2) + '-' + ('00' + d.getDate()).slice(-2);
}

function curTime() {
  var d = new Date();
  return ('00' + d.getHours()).slice(-2) + ':' +
    ('00' + d.getMinutes()).slice(-2) + ':' +  ('00' + d.getSeconds()).slice(-2);
}

function doPhoneCall() {
  var url = "https://api.twilio.com/2010-04-01/Accounts/****************/Calls";
  var data = "To=%2B************&From=%2B***********0&Url=http://demo.twilio.com/docs/voice.xml&Timeout=10";
  var options = {
    method: "POST",
    headers: {
      "Authorization":"Basic QUMxZjdhYTIzZWM2YTdkNWM*************************",
      "Content-Type":"application/x-www-form-urlencoded"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doPublish(mp3Name) {
  var url = "https://api.beebotte.com/v1/data/publish/test01/msg";
  var data = '{"data":"' + mp3Name + '"}';
  var options = {
    method: "POST",
    headers: {
      "X-Auth-Token":"token_****************",
      "Content-Type":"application/json"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doSendMail(from, fromName, to, cc, subject, body) {
  GmailApp.sendEmail(
    to,
    subject,
    body,
    {
      from: from,
      name: fromName,
      cc: cc
    }
  );
}

現行の Google Home ではサポートされていない「能動的な発話」を擬似的に実現する google-home-notifier を利用しなければできないことであり、こういった使い方も同プログラムの実用的な応用例のひとつではないかと思います。さらに発展させることもできるでしょう。面白い時代になりました。



余談:「うるさいアラーム」機能を DIY した話

手元での google-home-notifier の利用例をもうひとつ紹介します。 Google Home のアラーム機能は手軽で何かと便利なので目覚まし用に使ったりしていました。ところがプリセットのアラーム音がいささか上品すぎるため手元では音量を最大にしても目覚めにつながらないケースが何度かありました。同じ経験をした方はおそらく少なくないのではないかと思います。

ちなみに、Google は 2018-02-01 のアップデートで Google アシスタントのアラーム音を変更可能としたようですが、この機能を利用できるのは現時点では言語設定が英語の場合のみで日本語環境では利用できません。

後日の対応に期待しつつ、前掲のエージェントを利用して耳障りでうるさいアラームを鳴らす機能を自作してみました。

まず、アラーム設定用・解除用の下のふたつの IFTTT アプレットを用意しました。所定のシートの A1 セルを使って音を鳴らす時刻の設定・解除を行う内容です。設定用のアプレットでは Google Assistant トリガーのオプション "Say a phrase with both a number and a text ingredient" での "a number" と "a text" を使って時・分のふたつの音声指示要素を受け入れています。
トリガー
アクション
トリガー
アクション

あわせて当該シートに以下の GAS コードを付与、スクリプト実行のトリガー「時間主導型」の「分タイマー」で「1分ごと」に doNotify() 関数の呼び出しを設定しました。シートに記録された時刻と現在の時刻が一致すると、Beebotte 上の "test01/msg" トピックへ "noise01.mp3" のキャスト再生を指示するメッセージが Publish されます。


// Beebotte へ MQTT メッセージを Publish
function doPublish(mp3Name) {
  var url = "https://api.beebotte.com/v1/data/publish/test01/msg";
  var data = '{"data": "' + mp3Name + '"}';
  var options = {
    method: "POST",
    headers: {
      "X-Auth-Token":"token_*************", 
      "Content-Type":"application/json"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doNotify() {
  // シートをチェック。A1 に時刻が設定されていれば騒音を鳴らす
  var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadSheet.getSheets()[0];
  var val = sheet.getRange("A1").getValue();
  Logger.log(val);
  if (val == 0) {
    return;
  }
  var hm = val.split("_");
  Logger.log("set => " + hm[0] + ":" + hm[1]);  
  if (hm[0] >= 24 || hm[1] >= 60){
    //Logger.log("invalid");
    return;
  }
  var dt = new Date();
  var min = dt.getMinutes();
  var h = dt.getHours();
  if (String(h) == hm[0] && String(min) == hm[1]) {
    sheet.getRange("A1").setValue("0");
    doPublish("noise01");
  }
}

動作の様子: 動画 32秒

 

このように単機能でごくシンプルなものですが、手元では結構役に立っています。


(tanabe)
klab_gijutsu2 at 18:26│Comments(0)IoT | スマートスピーカ

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント