Edited at

Google Homeで伝言を届けるスマホアプリを作ってみた

More than 1 year has passed since last update.


やりたいこと

わが家の子供が小学生になったのですが、あまりに忘れ物が多すぎて心配しています。子供が家をでるときに「給食袋持った?」とか「今日は天気予報が雨だから傘を持ってね!」とか言ってフォローしてあげたいのですが、私が会社に家をでる時間の方が早いため、実現できません。

そこで、最近買ったGoogle Homeを使って解決したいと思います。

やりたいことはこんなかんじです。


  • スマホで伝言を録音し、Google Homeで伝言を再生させる

  • 伝言を再生する時刻は指定できる

  • 伝言を聞き逃した時のために、繰り返し再生できる

  • 「伝言ある?」とGoogle Homeに聞いたときに再生する「オンデマンド再生」もできる

やりたいこと

メッセージをGoogle Homeの人工音声で読み上げるだけならばQiita記事「LINEに送信したメッセージを、Google Homeで読み上げ、家族に通知」など既に実現できているようですが、Google Homeの人工音声ではなく、パパの声で伝言を残したかったので、アプリを自作することにしました。


構成

システム構成は下のような感じにしました。

スクリーンショット 2018-02-04 1.16.14.png

スマホに音声を録音すると、以下の流れでGoogleHomeにて再生されます。


  1. 音声ファイルをAWS S3にアップロード

  2. 音声ファイルに関するメタデータ(後述)をFirebase Realtime Databaseに書き込み

  3. ラズパイ上のNodeJSプログラムがFirebaseへの書き込みを検知

  4. 再生すべき時刻がきたら、Google Homeに音声ファイルのURLを指定して再生を依頼

  5. Google Homeが指定されたURLの音声を取得し、再生

メタデータとしては以下の情報を格納しています。


  • S3にアップロードした音声ファイルのURL

  • 再生する時刻

  • 音声の長さ

  • Push通知かオンデマンドか


スマホアプリ開発


React Native

我が家は私がiPhone、嫁がAndroidを使っているため両OSで動作するアプリをつくる必要がありました。そこで、JavaScriptでAndroid/iOSアプリを作れるReactNativeを利用しました。React Native自体の説明としてはQiita記事「React Nativeとは何なのか」などがあります。

ReactNativeではAndroid/iOSの知識がほとんどなくても、既存のReactNativeのライブラリを使用して開発できました。ライブラリの検索にはNative Directoryというサイトがオススメです。

UIライブラリとしてはNativeBaseが人気なようですが、今回はreact-native-material-kitというのを使ってみました。コンポーネント数はNativeBaseなどと比べると少ないかもしれませんが、癖がなく使いやすかったです。このライブラリでつくった画面は下のようなかんじです。

Simulator Screen Shot - iPhone 7 - 2018-02-06 at 00.36.32.png

中央のマイクボタンをタップすると録音、再タップまたは一定時間経過で録音終了です。SELECT DATE & TIMEをタップするとDate Pickerで再生日時を決められます。よく使う時刻はプリセットとして一番下に表示され、ワンタップで時刻を指定できます。

react-native-material-kitには日時選択(Date Picker)が含まれていないのでreact-native-modal-datetime-pickerを利用しました。


音声録音

音声の録音にはreact-native-audio-player-recorderというライブラリを使いました。下のようなコードで簡単にマイクから録音することができます。


import { AudioPlayer, AudioRecorder, AudioUtils,
} from 'react-native-audio-player-recorder'

// 録音準備
AudioRecorder.prepareRecordingAtPath(
  `${AudioUtils.DocumentDirectoryPath}/voice.aac`, {
SampleRate: 22050,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac',
AudioEncodingBitRate: 32000
});

// 録音開始
AudioRecorder.startRecording()

// 録音終了
AudioRecorder.stopRecording()

READMEに従って、react-native linkを実行することと、Info.plistとAndroidManifest.xmlにマイク仕様許可を設定することを忘れないようにしましょう。


AWS S3への音声ファイルアップロード

録音した音声をAWSのS3にアップロードするためのライブラリは大きく2種類に分かれます。


  • JavaScript上で動くもの

  • ネイティブアプリのSDKで動くもの

今回は性能を求めなかったため、JavaScript上で動くreact-native-aws3というライブラリを利用しました。録音したファイルをS3にアップロードするには下のようなコードになります。

import {Platform} from 'react-native'

import {RNS3} from 'react-native-aws3';
import {AudioUtils} from 'react-native-audio-player-recorder'

const S3_OPTIONS = {
keyPrefix: "uploads/",
bucket: "voice",
region: "us-east-1",
accessKey: "XXX",
secretKey: "XXX",
successActionStatus: 201,
};

const fileUri = `${AudioUtils.DocumentDirectoryPath}/voice.aac`;

// Android/iOSで場合分け
const uri = Platform.select({
ios: fileUri,
android: 'file://' + fileUri,
});

const file = {
uri,
name: `${new Date().toISOString()}.aac`,
type: "audio/aac"
};

// AWS-S3へのアップロード
RNS3.put(file, S3_OPTIONS)

S3_OPTIONSにはシークレットキーを設定するので、別ファイルに移すなどし、うっかりGitなどで公開しないように注意しましょう。

react-native-audio-player-recorderで録音したファイルのパスですが、Androidの場合は先頭に'file://'を付与しないとエラーになりました。react-nativeが提供するPlatform.select()をつかってAndroid/iOSの場合分けをしています。

また、iOSのバージョンによるのかもしれませんが、アップロード通信時に証明書エラーが表示され、ファイルアップロードができませんでした。ネイティブまわりがよくわかっていないのでStackOverflowの事例をいろいろ試した結果、node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm#pragma mark - NSURLSession delegateという行の下に、下記のコードを追加すれば動くようになりました。


- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}

動くようにはなりましたが、正直何をやっているのかはわかってません。だれか教えてください。


Firebase Realtime Databaseへのメタデータ格納

Firebaseを利用するためのライブラリもAWS-S3と同様に、ネイティブのSDKを利用するものと、JavaScriptのSDKを利用するものに分かれます。例えば、ネイティブのSDKを利用するライブラリとしてはreact-native-firebaseなどがあり、Qiita記事「React Native Firebaseについて」などで紹介されています。

今回は性能を求めないので、JavaScriptのSDKを利用することにしました。JavaScriptのライブラリをつかう場合はWebと同様なので、公式のドキュメントに従えば特に困ることはありませんでした。


RaspberryPi側アプリ (Node.js)

Node.jsでもWebと同様にFirebaseのJavaScript SDKを利用することができます。Node.jsからFirebase Realtime Databaseの変更を監視し、新しいメタデータの登録を検知すると、メタデータの指定時刻に、Google-Home-Notifierで音声ファイルのURLを指定してGoogle Homeに再生させています。

指定時刻に再生するのにNode-Scheduleというライブラリを利用しました。このライブラリを使うと、下のように指定時刻に任意の関数を実行できます。

Schedule.scheduleJob(new Date('2018-02-06T09:00:00'), 

() => notifyWithGoogleHome());

また、Google-Homeに「もう一度再生して」と話すとIFTTT連携でFirebase Realtime Databaseにトリガーとなるデータを書き込みむようにし、Node.jsで同様に検知して伝言をもういちど再生するようにしました。

オンデマンド再生も似たような仕組みです。


Google-Home-Notifierは非同期実行

Node.js側の実装では、Google-Home-Notifierによる音声再生が非同期実行であることに悩みました。非同期実行のため、同じ時刻に複数の伝言があると、うまく動作しないのです(1つしか再生されない)。

そこで、音声録音時に伝言の長さ(再生時間)を計測してメタデータとし、Node.jsでは再生後にこの再生時間分のウェイト(sleep)を入れることで解決しました。

const sleep = interval => new Promise(resolve => 

setTimeout(() => resolve(), interval));

const notify = async (url, duration) => {
googlehome.play(url);
await sleep(duration);
}


完成!

以上は要点だけですが、こんな感じで無事にやりたかったことを実現できました:blush:

子供へのフォローだけでなく、嫁への「今日は遅くなるから晩ゴハンいらないよ」も、LINEではなく自分の声で届けられて、より家庭が円満になった気がします。

最近作ったこちらの工作は家族の理解を得られず悲しんでいましたが、このアプリは家族から好評でとても嬉しいです♪