帰宅したらGoogleHomeから好きな音声で「おかえり」って言ってもらいますを読んで自分もやってみようと思ったのが発端です
生き物がしゃべってくれてる風なら、多少寂しさがまぎれるのではという目論見
使ったもの
- Google Home
- MacBook Air (Sierra)
- Parrot Flower Power
- 明るさ、気温、土の水分量などがとれる植物用センサーデバイス
- docomo 音声合成API (Powered by AI)
システム構成
実装
テキスト=>音声ファイル変換サーバー
Google Homeで再生してほしい音声ファイルを作るためのものです
サーバーはテキストを受け付けたら音声合成APIを叩いてその結果を返します
動作準備
# バイナリ=>wavの変換コマンドを入れる
$ brew install lame
$ brew install sox
# サーバーとして動作させるため
$ npm install express
また、事前に音声合成APIの利用申請は完了させておきます
コード
基本的な部分はほぼ参考記事のものを使わせてもらっています
const express = require("express");
const fs = require("fs");
const exec = require("child_process").exec;
const app = express();
const serverPort = 8080;
// 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("/audio/:text", (req, res) => {
let text = req.params.text;
if (!text) {
res.status(400).send("Invalid Parameters.");
}
console.log("Received text:" + text);
const MAX_BUFFER_SIZE = 2000*1024;
exec("./get_speech.sh " + text, {maxBuffer: MAX_BUFFER_SIZE}, (err, stdout, stderr) => {
if (err) { console.log(err); return; }
const file = fs.readFileSync(__dirname + "/audio/voice.wav", "binary");
res.setHeader("Content-Length", file.length);
res.write(file, "binary");
res.end();
});
});
app.listen(serverPort, () => {
console.log(`Start text to speech server. Port is ${serverPort}`);
});
get_speech.shというシェルスクリプトの中で音声合成APIを叩いた後、レスポンス(バイナリ)をwavファイルに変換しています
変換方法はこちらの情報を元にしています
#!/bin/bash
if [ $# -ne 1 ];then
echo "Usage: $0 <text>"
exit 1
fi
TEXT=$1
. ./env.sh
APIKEY=${DOCOMO_API_KEY}
SPEAKER=nozomi
SSML='<?xml version="1.0" encoding="utf-8" ?><speak version="1.1"><voice name="'${SPEAKER}'">'${TEXT}'</voice></speak>'
SSML_LENGTH=`echo ${SSML} | wc -c | tr -d " " | sed -e 's/\n//g'`
SSML_LENGTH=`expr ${SSML_LENGTH} - 1`
echo ${SSML_LENGTH}
URL="https://api.apigw.smt.docomo.ne.jp/aiTalk/v1/textToSpeech?APIKEY=${APIKEY}"
curl -H "Accept: audio/L16" -H "Content-Type: application/ssml+xml" -H "Content-Length: ${SSML_LENGTH}" -d "${SSML}" "${URL}" > tmp.raw
sox -t raw -r 16k -e signed -b 16 -B -c 1 tmp.raw audio/voice.wav
rm tmp.raw
SPEAKERを変更すると話者を変えることができます
API Keyは別ファイルにして管理します(githubに上げたりしたくないため)
#!/bin/bash
export DOCOMO_API_KEY=XXXXXXXXXXX
これで好きな言葉をいろんな声で聞けるようになりました
アプリケーション本体
動作の流れ
- 一定時間毎にポーリングして指定された動作時間帯かどうかチェック(今回は20:00〜23:30を対象)
- 動作時間帯だったら、Flower Powerに接続し、明るさセンサーの値を監視し始める(ライブモード)
- 明るさが閾値を超えたら帰宅したと判断し、しゃべりはじめる
- センサーの外気温と土の水分量を元にメッセージを構築
- テキスト=>音声ファイル変換サーバーにメッセージを送信、音声ファイルを作ってもらう
- テキスト=>音声ファイル変換サーバーのレスポンスをGoogle Homeに渡す
動作準備
各種デバイスを操作するためのライブラリを入れておきます
# GoogleHomeを操作
$ npm install google-home-notifier
# Flower Powerに接続しセンサー値を読み取る
$ npm install flower-power
# 時間を扱うときに便利
$ npm install moment
# 動作が込み入ってくるので、ログにタイムスタンプをつけるため利用
$ npm install log4js
コード
JS力が足りずネストが大変なことになってたり、多重起動防止の処理が幾つか入ったりしてますが、やっていることは動作の流れのとおりでさほど多くはありません
Flower Powerの使い方はJavaScriptから部屋の植物を監視。 Parrot Flower PowerをNode.jsから制御するを参考にさせてもらいました
//Google Home Notifierの準備
const googlehome = require("google-home-notifier");
let deviceName = "Google Home";
googlehome.device(deviceName);
googlehome.accent("ja");
//Flower Powerモジュールの準備
const flower = require("flower-power");
//ロガー準備
let log4js = require('log4js');
let logger = log4js.getLogger();
logger.level = 'info';
//このマシンのIPアドレスを取得
let myip;
const os = require("os");
os.networkInterfaces()["en0"].forEach(function(nic){
if(nic["family"] == "IPv4"){
myip = nic["address"];
}
});
//時間取扱ライブラリ準備
let moment = require("moment");
//多重起動させないためのフラグ
//動作が完了した日付
let greatedDate = {};
//すでに動作中かどうか
let alreadyRunning = false;
//起動する明るさの閾値
const LIGHT_THRESHOLD = 0.3;
//夜の時間帯か判定
const isNight = function(currentMoment){
let nightStart = moment({hours: 20, minutes: 00});
let nightEnd = moment({hours: 23, minutes: 30});
if(currentMoment.diff(nightStart) > 0 && currentMoment.diff(nightEnd) < 0){
return true;
}
return false;
}
//気温に応じたメッセージ
const getTemperatureMessage = function(temperature){
let message = "今日は";
if(temperature < 15){
message += "とても寒い";
}else if(temperature < 20){
message += "はだ寒い";
}else if(temperature < 25){
message += "過ごしやすい";
}else{
message += "あつい";
}
return message + "一日だったね。";
}
//土の水分に応じたメッセージ
const getMoistureMessage = function(moisture){
let message = "";
if(moisture < 20){
message += "そろそろお水が欲しいかも。";
}else if(moisture < 50){
message += "お水はちょうどよいかんじ。";
}else{
message += "ちょっと水が多すぎるかも。";
}
return message;
}
//GoogleHomeにしゃべってもらう
const sayMessage = function(message){
googlehome.play("http://" + myip + ":8080/audio/" + message, (notifyRes) => {
logger.info(notifyRes);
});
}
//一定間隔毎に起動チェックして、条件を満たしたらセンサーを動かし始める
const interval = 300000;
setInterval(function() {
logger.info("起動チェック中...");
let current = moment();
//夜でなければすぐ終了
if(!isNight(current) ){
logger.info("まだ夜ではありません zzz");
return;
}
//すでに今日の動作は終わっているか
if(current.format("YYYYMMDD") in greatedDate){
logger.info("今日はもう挨拶しました");
return;
}
//先行したプロセスが動いているか確認
if(alreadyRunning){
logger.info("すでに起動しています");
return;
}
logger.info("夜になりました。起動します");
alreadyRunning = true;
logger.info("FlowerPowerを探しています...");
flower.discover((fp) => {
logger.info("FlowerPowerを見つけました。接続中です...");
fp.connectAndSetup((err) => {
if(err) return;
logger.info("接続しました");
//LED光らせる
fp.ledPulse((err) => { if(err)return; });
fp.enableLiveMode((err) => {
if(err)return;
logger.info("ライブモードを開始します");
isLiveMode = true;
//照度センサーを監視して、明るくなったら動作する
fp.on("sunlightChange", (sunlight) => {
logger.info("照度; " + sunlight);
//ライブモード中に夜時間帯を越えていたら終了する
if(!isNight(moment())){
logger.info("指定の時間帯を越えたので終了します");
alreadyRunning = false;
fp.disconnect((err) => { return; });
}
if(current.format("YYYYMMDD") in greatedDate){
//音声再生中にもイベントが来てしまうので、多重動作対策
return;
}
if(sunlight > LIGHT_THRESHOLD){
logger.info("帰宅を検知しました");
fp.ledPulse((err) => { if(err)return; });
//本日の音声再生シーケンスに入ったのでフラグを立てる
greatedDate[moment().format("YYYYMMDD")] = 1;
let text = "おかえりなさい。";
//温度チェック
fp.readAirTemperature((err, temperature) => {
if(err)return;
text += getTemperatureMessage(temperature);
//水分チェック
fp.readSoilMoisture((err, moisture) => {
text += getMoistureMessage(moisture);
//植木鉢の状態をもとにメッセージを構築してGoogleHomeからしゃべる
sayMessage(text);
});
});
//終了
logger.info("ライブモードを終了します");
fp.disableLiveMode((err) => {
fp.disconnect((err) => { return; });
return;
});
alreadyRunning = false;
}
});
});
});
});
}, interval);
動かす
text2speech.js, app.jsをそれぞれ起動しておくと以下のようなログが確認できます
$ node app.js
[2017-11-22T19:34:34.802] [INFO] default - 起動チェック中...
[2017-11-22T19:34:34.803] [INFO] default - まだ夜ではありません zzz
[2017-11-22T20:04:42.351] [INFO] default - 起動チェック中...
[2017-11-22T20:04:42.352] [INFO] default - 夜になりました。起動します
[2017-11-22T20:04:42.352] [INFO] default - FlowerPowerを探しています...
[2017-11-22T20:04:51.251] [INFO] default - FlowerPowerを見つけました。接続中です...
[2017-11-22T20:04:54.935] [INFO] default - 接続しました
[2017-11-22T20:05:00.151] [INFO] default - ライブモードを開始します
[2017-11-22T20:05:01.145] [INFO] default - 照度; 0.12969122901677757
[2017-11-22T20:05:02.139] [INFO] default - 照度; 0.12969122901677757
(中略)
[2017-11-22T21:20:54.780] [INFO] default - 照度; 0.12969122901677757
[2017-11-22T21:20:55.773] [INFO] default - 照度; 0.3016792274350308
[2017-11-22T21:20:55.774] [INFO] default - 帰宅を検知しました
[2017-11-22T21:20:55.775] [INFO] default - ライブモードを終了します
[2017-11-22T21:21:00.810] [INFO] default - Device notified
$ node text2speech.js
Start text to speech server. Port is 8080
Received text:おかえりなさい。今日ははだざむい一日だったね。そろそろお水が欲しいかも。
Device notifiedのところでメッセージが送信され、Google Homeから音声が流れています
やってみて
Parrot Flower Power
発売が数年前のデバイスだったので、なかなかいいお値段で入手することになったんですが。。。
- センサー数種類搭載 & Bluetoothで通信可能
- 単4電池一本で動作
- 公式アプリで時系列のセンサー値の可視化ができる (こんな感じ)
といった点はとても良いです
特に時系列のセンサー値がすぐに見れるので、閾値を決めるのに役立ちました
帰宅したときの楽しみがちょっと増えた
家で待っててくれてるものがあるって楽しいです
あと、わりと植物に愛着がわいてきます
一言だけしかしゃべらないとちょっとわびしいので、そこは工夫の余地がありそう
改善ポイントもまだまだ
- アプリケーション本体を動かす場所
- 今回はMacを使っていたが、Raspberry Pi上で動かすなどしたい
- ライブモードの使い所
- Flower Powerは接続して動き出すまでに1分ほどかかるため、ライブモードで繋ぎっぱなしにしていたのですが、常に通信状態なので電池消費が激しく長時間動かすのには向いていなかった
- しゃべってくれる内容
- メッセージのバリエーションに変化をつけたり、ランダムなタイミングで適当なことをしゃべるとか
- 天気予報と連携させて、今日は外に出しておいてとか言ってくれるのも面白いかも