シェアする

google-home-notifierでGoogle Homeに任意の言葉を喋らせるようになるまで

シェアする

Raspberry Pi 3 Model B
Raspberry Pi 3 Model B

ABOX Raspberry Pi3 Model B ボード&専用ケースセット

この記事でできるようになること

  • Google Homeに「ばーかばーか」と言わせる

https://xxxxxxxx.ngrok.io/google-home-notifier?text=ばーかばーか

のようなURLにアクセスするだけでGoogle Homeが「ばーかばーか」と言います。

先日の記事Google Home MiniはIFTTTとRaspberry Piがあってこそ真価を発揮するなにができる?の部分で記載した「Google Homeに喋らせる」のやり方を自身のメモ書きも兼ねて記事にします。

今回やるのはあくまでも喋らせる機能のみなのでIFTTTでの連携については省略し、Raspberry PiにセットアップしたNode.js経由で喋らせる部分のみです。

IFTTTとの連携や実用例はまた後日。

用意するもの

今回使ったRaspberry Piはこちら

僕もRaspberry Piは初購入でがっつり初心者なので、一番迷わずスタートできそうな全部そろったやつを買いました。既にお持ちの方はそっちを使ってもいいと思います。

ちなみに試してないけどNode.jsが動くならレンタルサーバーでもいけるはず。ただし、Raspberry Piから赤外線モジュールでリモコン操作も使いたいなら今回紹介するやり方がスマートかな。

作業手順

Google Homeのセットアップは終わっている前提です。

  1. Raspberry PiにRaspbianをインストール
  2. npmインストール
  3. Node.jsインストール
  4. google-home-notifierのインストール
  5. 設定ファイルなどの書き換え
  6. google-home-notifier実行

なるべく詳細に説明していきたいと思います。

1. Raspberry PiにRaspbianをインストールしてGUI環境構築

Raspberry Piの初期セットアップを簡単・確実に完了するために必要な物・手順をまとめました。 #1 RaspberryPiの初期設定に必要な物 ・本体 RaspberryPi 3 Model B ・2.5A電源 ・Micro...

この記事が簡潔で分かりやすい。

ABOX Raspberry Pi3 Model B ボード&専用ケースセット の場合はnoobs入りのSDカードが既に用意されているので「3 起動・インストール」からになります。

僕が購入したやつは運が悪くSDカードのデータが破損しており、フォーマットしてからnoobsを入れ直しました(T_T)

SSHを使用可能に」とありますが、僕の場合はVNC ViewerからGUI操作でやったのでSSHできないとダメってわけではないです。Raspberry Pi用にモニタ・マウス・キーボードを用意するならリモートする必要も無いですし。

SSH接続でコマンド入力したい場合は

Raspberry PiにSSH接続したいけど、外出中のときや、ルーターやハブが目の前にないときに、Zeroconfという仕組みを利用してRaspberry PiのIPアドレスを調べたりせずにさくっとSSH接続できる方法を紹介します。...

こちらの記事が参考になります。

ただし、google-home-notifierのファイル書き換えなどを行う際にViエディタに慣れていない方は相当苦戦すると思うので、できればGUIでファイルの編集を終えたあとにした方が無難です。僕がそうでした。

ただ、パスワードは念のため変えておきましょう。

左端のアイコン→設定→Raspberry Piの設定

パスワードを変更を押してパスワードを任意のものに変更

2. npmインストール

青黒のアイコンをクリックしてLXTerminalを起動
青黒のアイコンをクリックしてLXTerminalを起動

青黒のアイコンをクリックしてLXTerminalを起動

npmをインストール

以下のコマンドを入力してnpmのバージョン5.5.1をインストール。

pi@raspberrypi:~ $ npm install -g npm@5.5.1

※今回利用するgoogle-home-notifierを実行するにあたってnpmとNode.jsのバージョンによっては動作しないことがあります。最初に試したときに最新版をインストールしたところ正常に動作できませんでした。

正常にnpm@5.5.1がインストールできたか確認

pi@raspberrypi:~ $ npm -v
正常にインストールできていれば「5.5.1」と表示されます 
正常にインストールできていれば「5.5.1」と表示されます 

正常にインストールできていれば「5.5.1」と表示されます

3. Node.jsインストール

こちらもバージョン8.9.3を指定してインストールしますが、万一動かなかった場合も考慮して、Node.jsのバージョン管理・変更を容易にするためにnvm(Node Version Manager)を利用します。

nvmのファイルをクローン(取得)

pi@raspberrypi:~ $ git clone https://github.com/creationix/nvm.git ~/.nvm
Cloning into '/home/pi/.nvm'...
remote: Counting objects: 6690, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6690 (delta 1), reused 3 (delta 1), pack-reused 6684
Receiving objects: 100% (6690/6690), 2.04 MiB | 853.00 KiB/s, done.
Resolving deltas: 100% (4153/4153), done.

続いて有効化

pi@raspberrypi:~ $ source ~/.nvm/nvm.sh<br>

バージョン確認

pi@raspberrypi:~ $ nvm -v

「Node Version Manager」に続いてヘルプ情報などが表示されれば準備OK。

pi@raspberrypi:~ $ git clone https://github.com/creationix/nvm.git ~/.nvm
Cloning into '/home/pi/.nvm'...
remote: Counting objects: 6690, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6690 (delta 1), reused 3 (delta 1), pack-reused 6684
Receiving objects: 100% (6690/6690), 2.04 MiB | 853.00 KiB/s, done.
Resolving deltas: 100% (4153/4153), done.
pi@raspberrypi:~ $ source ~/.nvm/nvm.sh
pi@raspberrypi:~ $ nvm -v

Node Version Manager

Note: <version> refers to any version-like string nvm understands. This includes:
  - full or partial version numbers, starting with an optional "v" (0.10, v0.1.2, v1)
  - default (built-in) aliases: node, stable, unstable, iojs, system
  - custom aliases you define with `nvm alias foo`

 Any options that produce colorized output should respect the `--no-colors` option.

Usage:
  nvm --help                                Show this message
  nvm --version                             Print out the installed version of nvm
  nvm install [-s] <version>                Download and install a <version>, [-s] from source. Uses .nvmrc if available
    --reinstall-packages-from=<version>     When installing, reinstall packages installed in <node|iojs|node version number>
    --lts                                   When installing, only select from LTS (long-term support) versions
    --lts=<LTS name>                        When installing, only select from versions for a specific LTS line
    --skip-default-packages                 When installing, skip the default-packages file if it exists
    --latest-npm                            After installing, attempt to upgrade to the latest working npm on the given node version
  nvm uninstall <version>                   Uninstall a version
  nvm uninstall --lts                       Uninstall using automatic LTS (long-term support) alias `lts/*`, if available.
  nvm uninstall --lts=<LTS name>            Uninstall using automatic alias for provided LTS line, if available.
  nvm use [--silent] <version>              Modify PATH to use <version>. Uses .nvmrc if available
    --lts                                   Uses automatic LTS (long-term support) alias `lts/*`, if available.
    --lts=<LTS name>                        Uses automatic alias for provided LTS line, if available.
  nvm exec [--silent] <version> [<command>] Run <command> on <version>. Uses .nvmrc if available
    --lts                                   Uses automatic LTS (long-term support) alias `lts/*`, if available.
    --lts=<LTS name>                        Uses automatic alias for provided LTS line, if available.
  nvm run [--silent] <version> [<args>]     Run `node` on <version> with <args> as arguments. Uses .nvmrc if available
    --lts                                   Uses automatic LTS (long-term support) alias `lts/*`, if available.
    --lts=<LTS name>                        Uses automatic alias for provided LTS line, if available.
  nvm current                               Display currently activated version
  nvm ls                                    List installed versions
  nvm ls <version>                          List versions matching a given <version>
  nvm ls-remote                             List remote versions available for install
    --lts                                   When listing, only show LTS (long-term support) versions
  nvm ls-remote <version>                   List remote versions available for install, matching a given <version>
    --lts                                   When listing, only show LTS (long-term support) versions
    --lts=<LTS name>                        When listing, only show versions for a specific LTS line
  nvm version <version>                     Resolve the given description to a single local version
  nvm version-remote <version>              Resolve the given description to a single remote version
    --lts                                   When listing, only select from LTS (long-term support) versions
    --lts=<LTS name>                        When listing, only select from versions for a specific LTS line
  nvm deactivate                            Undo effects of `nvm` on current shell
  nvm alias [<pattern>]                     Show all aliases beginning with <pattern>
  nvm alias <name> <version>                Set an alias named <name> pointing to <version>
  nvm unalias <name>                        Deletes the alias named <name>
  nvm install-latest-npm                    Attempt to upgrade to the latest working `npm` on the current node version
  nvm reinstall-packages <version>          Reinstall global `npm` packages contained in <version> to current version
  nvm unload                                Unload `nvm` from shell
  nvm which [<version>]                     Display path to installed node version. Uses .nvmrc if available
  nvm cache dir                             Display path to the cache directory for nvm
  nvm cache clear                           Empty cache directory for nvm

Example:
  nvm install 8.0.0                     Install a specific version number
  nvm use 8.0                           Use the latest available 8.0.x release
  nvm run 6.10.3 app.js                 Run app.js using node 6.10.3
  nvm exec 4.8.3 node app.js            Run `node app.js` with the PATH pointing to node 4.8.3
  nvm alias default 8.1.0               Set default node version on a shell
  nvm alias default node                Always default to the latest available node version on a shell

Note:
  to remove, delete, or uninstall nvm - just remove the `$NVM_DIR` folder (usually `~/.nvm`)

バージョン8.9.3をインストールしてデフォルトに設定

pi@raspberrypi:~ $ nvm install v8.9.3
pi@raspberrypi:~ $ nvm alias default v6.2.2

バージョン確認

pi@raspberrypi:~ $ node -v
v8.9.3

「v8.9.3」と表示されていればOK

他のバージョンをインストールすれば切り替え可能

pi@raspberrypi:~ $ nvm install v8.9.0
Downloading and installing node v8.9.0...<br>Downloading https://nodejs.org/dist/v8.9.0/node-v8.9.0-linux-armv7l.tar.xz...<br>######################################################################## 100.0%<br>Computing checksum with sha256sum<br>Checksums matched!<br>Now using node v8.9.0 (npm v5.5.1)

インストール済みのバージョンを切り替える

pi@raspberrypi:~ $ nvm use v8.9.3
Now using node v8.9.3 (npm v5.5.1)

不要なバージョンのアンインストール

pi@raspberrypi:~ $ nvm uninstall v8.9.0
Uninstalled node v8.9.0

4. google-home-notifierのインストール

インストール実行

pi@raspberrypi:~ $ npm install google-home-notifier

一応ここからさきの手順はGitHubにも記載されています。

google-home-notifier - Send notifications to Google Home

5. ファイルの書き換え

ここからが少々難しい。

ファイルマネージャを起動→google-home-notifierフォルダを開く

書き換えるファイルは以下のもの

  • example.js
  • google-home-notifier.js
  • node_modules/mdns/lib/browser.js
  • node_modules/google-tts-api/lib/api.js

各種ファイルはText Editorでexample.jsを開いて編集→保存

example.js

編集に必要なGoogle Homeの名前とIPアドレスはスマホアプリから確認可能

HOMEアプリのメニューから「デバイス」を選択
HOMEアプリのメニューから「デバイス」を選択

HOMEアプリのメニューから「デバイス」を選択

我が家の場合は「リビング」が名前です
我が家の場合は「リビング」が名前です

我が家の場合は「リビング」が名前です

IPアドレスは「設定」から確認可能
IPアドレスは「設定」から確認可能

IPアドレスは「設定」から確認可能

最下部までスクロールしていくと記載されています
最下部までスクロールしていくと記載されています

最下部までスクロールしていくと記載されています

編集箇所は以下のハイライト部分※「’(シングルクォーテーション)」がうっかり消えていたりするだけで動かなくなりますので十分注意しながら編集してください。

var express = require('express');
var googlehome = require('./google-home-notifier');
var ngrok = require('ngrok');
var bodyParser = require('body-parser');
var app = express();
const serverPort = 8080; // default port

var deviceName = 'リビング';// Google Homeの名前
var ip = '192.168.1.64'; // Google HomeのIPアドレス

var urlencodedParser = bodyParser.urlencoded({ extended: false });

app.post('/google-home-notifier', urlencodedParser, function (req, res) {
  
  if (!req.body) return res.sendStatus(400)
  console.log(req.body);
  
  var text = req.body.text;
  
  if (req.query.ip) {
     ip = req.query.ip;
  }

  var language = 'ja'; // jaに変更
  if (req.query.language) {
    language;
  }

  googlehome.ip(ip, language);

  if (text){
    try {
      if (text.startsWith('http')){
        var mp3_url = text;
        googlehome.play(mp3_url, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will play sound from url: ' + mp3_url + '\n');
        });
      } else {
        googlehome.notify(text, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will say: ' + text + '\n');
        });
      }
    } catch(err) {
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  }else{
    res.send('Please GET "text=Hello Google Home"');
  }
})

app.get('/google-home-notifier', function (req, res) {

  console.log(req.query);

  var text = req.query.text;

  if (req.query.ip) {
     ip = req.query.ip;
  }

  var language = 'ja'; // jaに変更
  if (req.query.language) {
    language;
  }

  googlehome.ip(ip, language);

  if (text) {
    try {
      if (text.startsWith('http')){
        var mp3_url = text;
        googlehome.play(mp3_url, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will play sound from url: ' + mp3_url + '\n');
        });
      } else {
        googlehome.notify(text, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will say: ' + text + '\n');
        });
      }
    } catch(err) {
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  }else{
    res.send('Please GET "text=Hello+Google+Home"');
  }
})

app.listen(serverPort, function () {
  ngrok.connect(serverPort, function (err, url) {
    console.log('Endpoints:');
    console.log('    http://' + ip + ':' + serverPort + '/google-home-notifier');
    console.log('    ' + url + '/google-home-notifier');
    console.log('GET example:');
    console.log('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
	console.log('POST example:');
	console.log('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
  });
})

google-home-notifier.js

var Client = require('castv2-client').Client;
var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
var mdns = require('mdns');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
var deviceAddress;
var language;

var device = function(name, lang = 'ja') { //jaに変更
    device = name;
    language = lang;
    return this;
};

var ip = function(ip) {
  deviceAddress = ip;
  return this;
}
deviceAddress = '192.168.1.64'; // Google HomeのIPアドレス

var googletts = require('google-tts-api');
var googlettsaccent = 'ja'; //jaに変更
var accent = function(accent) {
  googlettsaccent = accent;
  return this;
}

var notify = function(message, callback) {
  if (!deviceAddress){
    browser.start();
    browser.on('serviceUp', function(service) {
      console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getSpeechUrl(message, deviceAddress, function(res) {
          callback(res);
        });
      }
      browser.stop();
    });
  }else {
    getSpeechUrl(message, deviceAddress, function(res) {
      callback(res);
    });
  }
};

var play = function(mp3_url, callback) {
  if (!deviceAddress){
    browser.start();
    browser.on('serviceUp', function(service) {
      console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getPlayUrl(mp3_url, deviceAddress, function(res) {
          callback(res);
        });
      }
      browser.stop();
    });
  }else {
    getPlayUrl(mp3_url, deviceAddress, function(res) {
      callback(res);
    });
  }
};

var getSpeechUrl = function(text, host, callback) {
  googletts(text, language, 1, 1000, googlettsaccent).then(function (url) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
  }).catch(function (err) {
    console.error(err.stack);
  });
};

var getPlayUrl = function(url, host, callback) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
};

var onDeviceUp = function(host, url, callback) {
  var client = new Client();
  client.connect(host, function() {
    client.launch(DefaultMediaReceiver, function(err, player) {

      var media = {
        contentId: url,
        contentType: 'audio/mp3',
        streamType: 'BUFFERED' // or LIVE
      };
      player.load(media, { autoplay: true }, function(err, status) {
        client.close();
        callback('Device notified');
      });
    });
  });

  client.on('error', function(err) {
    console.log('Error: %s', err.message);
    client.close();
    callback('error');
  });
};

exports.ip = ip;
exports.device = device;
exports.accent = accent;
exports.notify = notify;
exports.play = play;

node_modules/mdns/lib/browser.js

getaddrinfo() → getaddrinfo({families:[4]})

var dns_sd = require('./dns_sd')
  , nif = require('./network_interface')
  , util = require('util')
  , rst = require('./resolver_sequence_tasks')
  , st = require('./service_type')
  , MDNSService = require('./mdns_service').MDNSService
  ;

var Browser = exports.Browser = function Browser(serviceType, options) {
  MDNSService.call(this);
  var self = this;

  options = options || {};
  var flags = options.flags             || 0
    , ifaceIdx  = nif.interfaceIndex(options)
    , domain = options.domain           || null
    , context = options.context         || null
    , requested_type = st.makeServiceType( serviceType );
    ;

  var interfaceNames = [];

  function on_service_changed(sdRef, flags, ifaceIdx, errorCode, serviceName,
      serviceType, replyDomain, context)
  {
    function on_resolver_done(error, service) {
      if (error) {
        self.emit('error', error, service);
      } else {
        self.emit('serviceChanged', service, context);
        self.emit('serviceUp', service, context);
      }
    }
    if (errorCode == dns_sd.kDNSServiceErr_NoError) {
      if (requested_type.isWildcard()) {
        serviceType = serviceName + '.' + serviceType.split('.').shift();
        serviceName = null;
      }

      var type;

      try {
        type = st.makeServiceType(serviceType);
      } catch(e) {
        self.emit('error', e);
        return;
      }

      var service = {
          interfaceIndex: ifaceIdx
        , type: type
        , replyDomain: replyDomain
        , flags: flags
      };
      if (serviceName) service.name    = serviceName;

      if (dns_sd.kDNSServiceInterfaceIndexLocalOnly === ifaceIdx) {
        service.networkInterface = nif.loopbackName();
      } else if (typeof dns_sd.if_indextoname !== 'undefined' && ifaceIdx > 0) {
        try {
          service.networkInterface = dns_sd.if_indextoname(ifaceIdx);

          interfaceNames[ifaceIdx] = service.networkInterface;
        } catch(e) {
          if(typeof interfaceNames[ifaceIdx] !== "undefined") {
            service.networkInterface = interfaceNames[ifaceIdx];
          } else {
            self.emit('error', e);
          }
        }
      }

      if (flags & dns_sd.kDNSServiceFlagsAdd) {
        resolve(service,
            options.resolverSequence || Browser.defaultResolverSequence,
            on_resolver_done);
      } else {
        self.emit('serviceChanged', service, context);
        self.emit('serviceDown', service, context);
      }
    } else {
      self.emit('error', dns_sd.buildException(errorCode));
    }
  }

  dns_sd.DNSServiceBrowse(self.serviceRef, flags, ifaceIdx, '' + requested_type,
      domain, on_service_changed, context);
}
util.inherits(Browser, MDNSService);

var resolve = exports.resolve = function resolve(service, sequence, callback) {
  var step = 0;
  if ( ! callback) {
    callback = sequence;
    sequence = Browser.defaultResolverSequence;
  }

  function next(error) {
    if (error) {
      callback(error, service);
      return;
    }
    if (sequence.length === step) {
      callback(undefined, service);
      return;
    }
    sequence[step++](service, next);
  }

  next();
}

Browser.create = function create(serviceType, options) {
  return new Browser(serviceType, options);
}

Browser.defaultResolverSequence = [
  rst.DNSServiceResolve()
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})
, rst.makeAddressesUnique()
];

exports.browseThemAll = function browseThemAll(options) {
  options = options || {}
  options.resolverSequence = options.resolverSequence || [];
  return Browser.create(st.ServiceType.wildcard, options);
}

node_modules/google-tts-api/lib/api.js

var url = require('url');
var token = require('./token');
var ushost = 'https://www.google.co.jp'; //日本用のURLに変更
var ukhost = 'https://translate.google.co.jp'; //日本用のURLに変更

/**
 * Generate "Google TTS" audio download link
 *
 * @param   {String}  text
 * @param   {String}  key
 * @param   {String!} lang   default is 'en'
 * @param   {Number!} speed  show = 0.24, default is 1
 * @param   {String}  accent   default is 'us', otherwise 'uk'
 * @return  {String}  url
 */
module.exports = function (text, key, lang, speed, accent) {
  if (typeof text !== 'string' || text.length === 0) {
    throw new TypeError('text should be a string');
  }

  if (typeof key !== 'string' || key.length === 0) {
    throw new TypeError('key should be a string');
  }

  if (typeof lang !== 'undefined' && (typeof lang !== 'string' || lang.length === 0)) {
    throw new TypeError('lang should be a string');
  }

  if (typeof speed !== 'undefined' && typeof speed !== 'number') {
    throw new TypeError('speed should be a number');
  }
// アクセントの設定をコメントアウト
//  if (typeof accent !== 'undefined' && accent !== 'us' && accent !== 'uk') {
//    throw new TypeError('accent must be "us" or "uk"');
//  }

  var host = accent === 'ja' ? ukhost : ushost; // jaに変更

  return host + '/translate_tts' + url.format({
    query: {
      ie: 'UTF-8',
      q: text,
      tl: lang || 'ja',// jaに変更
      total: 1,
      idx: 0,
      textlen: text.length,
      tk: token(text, key),
      client: 't',
      prev: 'input',
      ttsspeed: speed || 1
    }
  });
};

ここまでで下準備は完了です!

google-home-notifier実行

再びターミナルに戻って以下のコマンドを入力

pi@raspberrypi:~/google-home-notifier $ node example.js

もし以下のようなエラーが出た場合はカレントディレクトリをgoogle-home-notifierに戻しましょう。

module.js:538
 throw err;
 ^
Error: Cannot find module '/home/pi/example.js'
 at Function.Module._resolveFilename (module.js:536:15)
 at Function.Module._load (module.js:466:25)
 at Function.Module.runMain (module.js:676:10)
 at startup (bootstrap_node.js:187:16)
 at bootstrap_node.js:608:3

これで戻ります。

pi@raspberrypi:~ $ cd google-home-notifier
pi@raspberrypi:~/google-home-notifier $

実行できたら喋らせるコマンドを入力!

curl -X POST -d "text=ばーかばーか" http://<<Google HomeのIPアドレス>>:8080/google-home-notifier

うまくいけばこれで「ばーかばーか」と言ってもらえるはずです。

そしてURLにアクセスするだけで というのを外部から、例えば出先からスマホでこのコマンドを実行するにはhttps://xxxxxxxx.ngrok.io/google-home-notifier?text=ばーかばーかのようなHTTPSプロトコルのURLにアクセスすればOK。

pi@raspberrypi:~/google-home-notifier $ node example.js

を実行した際にサンプルとしてURLが表示されているはずです。

ここで注意すべき点が一つ。

このHTTPSのドメインは再起動するたびに変動します

なので、今回は紹介していませんがIFTTTとの連携設定など行ったあとで再起動すると全て設定したURLを再度変更しなければならなくなるため、必ず十分テストしてからIFTTTの設定を行いましょう。

以上、Google Homeに「ばーかばーか」と喋らせる方法でした!

イントネーションがなんかおかしいですが、今後改善されるんでしょうかね。されるといいな。

スポンサーリンク