GoogleHome に電車遅延情報を喋ってもらう(google-home-notifier編)


HOYA提供のVoideTextが無料ではなくなりました。以下は備忘で残しておきますがもう使用できません。2025/04/04

# はじめに

![image.png](https://qiita-image-store.s3.amazonaws.com/0/203949/88955020-fd74-c88d-7b51-78c39e716314.png)

朝の電車遅延は嫌ですよね。遅れてるいるものはどうにもならないので、せめて駅に着く前に心の準備をしておきたいものです。

朝、決まった時刻に列車運行情報をスクレイピングして遅延していたらGoogleHomeにしゃべってもらいます。

# 構成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/203949/8f2a8b03-faaa-9734-5370-260135218730.png)

RaspberryPiで定刻になったらYahoo路線情報ページをスクレイピングして正常運行以外ならば google-home-notifier を使ってGoogleHomeにしゃべってもらいます。google-home-notifier のしゃべりがイマイチなので、音声は VoiceText に変更します。

# ハード
* GoogleHome (私のはminiです)
* RaspberryPi (今回は3B)
* 母艦(Windowsマシン。何でも良い) 

# ソフト
* raspbian (今回は 2017-09-07-raspbian-stretch を使いました）
* nodejs (v9.8.0)
* npm (v6.4.1)
* google-home-notifier
* VoiceText

中心となるソフトウェアは上記です。
raspbian、nodejs、npm は設定済み前提です。

# google-home-notifier のインストール
まず始めに作業ディレクトリ(trainfo)を作成しておき、そこで作業します。

```bash
$ mkdir ~/trainfo
$ cd ~/trainfo
$ npm init
```

google-home-notifier インストールの前に必要なライブラリを入れてから。

```bash
$ sudo apt-get install libavahi-compat-libdnssd-dev
  :
$ npm install google-home-notifier
```

# google-tts-api
[2020/11/16 更新]
このままで google-tts-api が動きません。動かすにはゴニョゴニョする必要があるのですが、ここでは使用しないので先に進みます。

# VoiceText Web API
VoiceTextとはなんぞやというと、文字列を合成音に変えてくれるTTS(TextToSpeech)エンジンです。google-home-notifier では、google-tts-api というやつが TTSエンジンとして使われてます。で、google-tts-api を使わずに VoiceText に置き換えようと。

VoiceText は無料ですがAPIキーが必要です。取得しましょう。
[VoiceText Web API](https://cloud.voicetext.jp/webapi)
メールアドレスだけで取得できます。

# VoiceText のインストール

```bash
$ npm install voicetext
```

# google-home-notifier を修正

google-tts-api ではなく VoiceText を使うように、google-home-notifier を修正する。変更箇所には [nori-dev] が付けてある。

```js:~/trainfo/node_modules/google-home-notifier/google-home-notifier.js

// 6行目あたり
var language = 'ja';  // add ='ja' [nori-dev]

// add start [nori-dev]-->
var VoiceTextWriter = require('./VoiceTextWriter');
var voiceTextWriter = new VoiceTextWriter();
// <-- add end [nori-dev]

var device = function(name, lang = 'ja') {
:

// 71行目あたり
var getSpeechUrl = function(text, host, callback) {
  /* delete start [nori-dev]-->
  googletts(text, language, 1).then(function (url) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
  }).catch(function (err) {
    console.error(err.stack);
  });
  <-- delete end [nori-dev]*/
  // add start [nori-dev]-->
  voiceTextWriter.convertToText(text).then(function(result, reject){
    onDeviceUp(host, result, function(res){
     callback(res)
   });
  }).catch(function onRejected(error){
   console.error(error);
  });
  // <-- add end [nori-dev]
};
:

// 98行目あたり
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
      };
      // add start [nori-dev]-->
      if(url.endsWith('wav')){
        media.contentType = 'audio/wav';
      }else if(url.endsWith('ogg')){
        media.contentType = 'audio/ogg';
      }
      // <-- add end [nori-dev]
      player.load(media, { autoplay: true }, function(err, status) {
        client.close();
        callback('Device notified');
      });
    });
  });

```

# VoiceTextWriter.js の作成
VoiceTextWebAPI を呼び出すところを作成します。google-home-notifier.js と同じところに保存してください。
ソース中の \<VoiceTextのAPIキー> と \<RaspberryPIのIP>には適宜入力。
OUT_PATH は VoiceTextWebAPI で作成されたファイルを置く場所です。
次項の api.js の内容と合わせてください。

```bash:
$mkdir ~/trainfo/voice
```

```js:~/trainfo/node_modules/google-home-notifier/VoiceTextWriter.js
var fs = require('fs');
var VoiceText = require('voicetext');
var voice = new VoiceText('<VoiceTextのAPIキー>');
var OUT_PATH = '/home/pi/trainfo/voice/_temp.wav'
var OUTPUT_URL = 'http://<RaspberryPIのIP>:8080/googlehome/_temp.wav';

class VoiceTextWriter {

    convertToText(text) {
        return new Promise(function (resolve, reject) {
            voice
                .speaker(voice.SPEAKER.HIKARI)
                .emotion(voice.EMOTION.HAPPINESS)
                .emotion_level(voice.EMOTION_LEVEL.HIGH)
                .volume(150)
                .speak(text, function (e, buf) {
                    if (e) {
                        console.error(e);
                        reject(e);

                    } else {
                        fs.writeFileSync(OUT_PATH, buf, 'binary');
                        resolve(OUTPUT_URL);
                    }
                });
        });
    }
}
module.exports = VoiceTextWriter;
```

# api.js の作成
GoogleHomeからのHTTP要求に応答するHTTPサーバを express で作ってしまいます。
必要なライブラリをインストール。

```bash:
$ npm install express body-parser fs
```


```js:~/trainfo/api.js
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');

const app = express();
const serverPort = 8080;
const urlencodedParser = bodyParser.urlencoded({ extended: false });

// CORSを許可する
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

/**
 * 音声ファイルを取得する
 */
app.get('/googlehome/:voiceFileName', (req, res) => {
  const voiceFileName = req.params.voiceFileName;
  if (!voiceFileName) {
    res.status(400).send('Invalid Parameters.');
}

  const file = fs.readFileSync('/home/pi/trainfo/voice/' + voiceFileName, 'binary');
  res.setHeader('Content-Length', file.length);
  res.write(file, 'binary');
  res.end();
});

app.listen(serverPort, () => {
  console.log(`Start api-server. Port is ${serverPort}`);
})
```
api.js は常に実行させておく必要があるので forever します。
まだインストールしていない場合は

```bash:
$ sudo npm install -g forever
  :
$ forever start api.js
```

これでVoiceText準備はOK。

#VoiceTextのお試し
先程実行した notifier.js を再度。

```bash:
$ node notifier.js
```

いかがですか？いい声出ましたか？
次はいよいよ運行情報の取得です。

# Yahoo路線情報ページをスクレイピング

先程使用した notifier.js を拡張します。スクレイピングには cheerio-httpcli を使いますのでインストールします。

```bash:
$ npm install cheerio-httpcli
```

下記のソースでは、３路線の情報を取得しています。適宜変更して使ってください。
「平常運転」のときは何もしゃべりませんので、まずは遅延があるところでテストしてみてください。

Yahoo路線情報(https://transit.yahoo.co.jp/traininfo/top)


```js:~/trainfo/trainfo.js
const googlehome = require('google-home-notifier')
const client = require('cheerio-httpcli');

googlehome.ip("<GoogleHomeのIP>");
googlehome.device('ダイニング ルーム', 'ja');

const voice = [];

client.fetch('https://transit.yahoo.co.jp/traininfo/detail/22/0/') // 京浜東北線
    .then(function (result) {
        check(result);
        return client.fetch('https://transit.yahoo.co.jp/traininfo/detail/29/0/'); // 横須賀線
    })
    .then(function (result) {
        check(result);
        return client.fetch('https://transit.yahoo.co.jp/traininfo/detail/125/0/'); // 相鉄本線
    })
    .then(function (result) {
        check(result);
    })
    .catch(function (err) {
        console.log(err);
    })
    .finally(function () {
        var text = voice.join();
        if (text.length > 0) {
            googlehome.notify(text, function (res) {
                console.log(res);
            });
        }
    });

function check(result) {
    if (!result.error) {
        var $ = result.$;
        var ti = $('h1.title').text().trim();
        var sv = $('#mdServiceStatus > dl > dt').text().trim();
        var tr = $('#mdServiceStatus > dl > dd.trouble').text().trim();
        if (!sv.match('平常運転')) {
            voice.push(ti + tr);
        }
        console.log(ti);
        console.log(sv);
        console.log(tr);
    }
}
```

# 実行

```
pi@raspi01:~/train $ node trainfo.js
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
  :
白新線
[!]列車遅延
19:26頃、羽越本線内で線路内点検を行った影響で、現在も列車に遅れが出ています。（11月20日 21時45分掲載）
横須賀線
[○]平常運転

相鉄本線
[○]平常運転

Device notified
```

WARNING は見なかったことにしてちゃんと音声が出ましたでしょうか？
ここまでくればあともう少し！
※「平常運転」のときは、なにもしゃべりません。

# cron
朝にしゃべってもらわないといけないのでスケジュールします。

```bash:
$ crontab -l
#平日7:00から15分ごとに運行情報プログラムを実行する
0,15,30 7 * * 1-5 node /home/pi/trainfo/trainfo.js
```

それと前述した api.js を システム起動時に実行されるように /etc/rc.local にでも入れておいてください。

```bash:/etc/rc.local
  :
# google-home-notifier voicetext 運行情報で使う
forever start /home/pi/trainfo/api.js

exit 0
```

以上です。

# ソース
今回使用した作成・修正のファイルを以下においておきます。
github：https://github.com/nori-dev-akg/trainfo

#SpecialThanks
https://qiita.com/zono_0/items/95418d9ceaad3836a758
https://qiita.com/ktetsuo/items/8c9cd5714e231aa6ae09
http://gadgetneko.blog.jp/archives/5326857.html