GoogleHomeアプリをTypeScriptで書いてみた話

皆さん、『GoogleHome』はご存知でしょうか。
GoogleHomeとは
「スマートスピーカー」のひとつで、本体内蔵されたマイクに「OK Google. ~」と話しかけると応答や動作をしてくれる端末です。
私も先日 GoogleHomeを購入して、朝に家を出る前は天気を聞いたり、帰宅したらニュースを聞いたり、寝る前に音楽を聴いたりと意外と重宝しています。
さて今回は、このGoogleHomeのアプリケーションをサクッと作りたいと思い、模索したことについてまとめようと思います。
今回作ってみるもの
GoogleHomeに話を聞かせて、文章の内容から今どのような感情なのかを当てるアプリケーションを作る。
開発環境
●Actions on Google (https://developers.google.com/actions/)
●Dialogflow (https://dialogflow.com/)
●Cloud Functions for Firebase (https://firebase.google.com/docs/functions/)
●Cloud Natural Language API (https://cloud.google.com/natural-language/)
FirebaseをベースにNode.jsで作っていきます。
また、Cloud Functions for Firebaseの対象Nodeバージョンが若干低いので、TypeScript→JavaScriptのコンパイルを挟みます。
処理の流れ
1. dialogflowでGooleHomeから受け取った文章を言語解析させる。
<イメージ>
(ユーザーの発話)「今日は、○○○」、「今の気分は、○○○」 → (dialogflow){text: ○○○}
2. dialogflowからWebhookして、Cloud Functions for FirebaseのAPIを呼ぶ。
言語解析に成功したら、dialogflowが1の○○○をリクエストパラメータに付けて、3を呼ぶ。
3. Cloud Functions for Firebase で Cloud Natural Language API を使い文章を分析する。
パラメータに入っている文章を Cloud Natural Language API に渡して感情を分析。
分析した情報をもとにGoogleHomeが発話する相槌を生成してActions on Googleに渡す。
作成したソースコードは下に記載しておきます。
4. GoogleHomeが喋る
テスト
実際に、ちゃんと動くか確かめてみます。
まずは、Actions on Googleのシミュレーターを使って文章で会話してみます。

今の気分はあまりよろしくは無いです。 → 「あなたは今、ネガティブな気分ですね。」
今日は美味しいディナーに行きました。 → 「あなたは今、ポジティブな気分ですね。」
と良い感じに気分を言い当ててくれました。
最後に家にあるGoogleHomeで試してみます。
Actions on Googleでテストしたユーザーと同じユーザーでGoogleHomeにログインしている場合は、「テスト用アプリに繋いで」と発話するだけで作った機能が使えます。
最後に
思っていたよりも簡単にアプリケーションを作ることができました。
アプリを使ってみても、Googleアシスタントの音声認識の性能が良いので誤字が少ないのにも驚きです。
作ったアプリケーションは審査に通せば全ユーザーに公開することも可能なので、これからも新しいコンテンツがどんどん増えていくと思います。
今後のスマートスピーカーに期待です。
作成したソースコード
./index.ts
import {AnalyzeSentiment, ResultAnalyzedEmotion} from './interface';
import * as functions from 'firebase-functions';
import * as express from 'express';
import GoogleCloudNaturalLanguageAPI from './GoogleCloudNaturalLanguageAPI'
/**
* dialogflowからWebhookして呼ばれるAPI
*/
export const SentimentAPI = functions.https.onRequest((request : express.Request, response : express.Response) => {
// console.log(`body = ${JSON.stringify(request.body.result)}`);
// console.log(`query = ${JSON.stringify(request.query)}`);
// console.log(`header = ${JSON.stringify(request.headers)}`);
(async () => {
try {
// リクエストに入っているパラメータを取得
const parameters = request.body.result.parameters;
console.log(`parameters = ${JSON.stringify(parameters)}`);
// パラメータが空の場合は、アプリの説明をする
if( !parameters.text || parameters.text === '' ) {
response.json({
speech: `このアプリは、あなたの今の気分をお調べします。\n「今の気分は何々」や「今日は何々」のように今日の気分を教えてください。`,
displayText: `このアプリは、あなたの今の気分をお調べします。\n「今の気分は~」、「今日は~」と今日の気分を教えてください。`,
});
return;
}
// 文章を感情分析する
const resultAnalyzedEmotion : ResultAnalyzedEmotion = await GoogleCloudNaturalLanguageAPI.detectSentiment(parameters.text);
response.json({
result: `SUCCESS`,
json: resultAnalyzedEmotion,
speech: `あなたは今、${resultAnalyzedEmotion.feeling}な気分ですね。`,
displayText: `あなたは今、${resultAnalyzedEmotion.feeling}な気分ですね。`
});
}
catch (e) {
// どこかでエラーになった場合は GoogleHome の気分が悪いことにしておく
response.json({
result: `FAILURE`,
cause: e,
speech: `すみません。私の気分が優れないようです。`,
displayText: `すみません。私の気分が優れないようです。`
});
}
})();
response.status(200);
});
./GoogleCloudNaturalLanguageAPI.ts
import * as _ from 'lodash'
import {LanguageServiceClient} from '@google-cloud/language';
import {AnalyzeSentiment, Sentiment, ResultAnalyzedEmotion} from './interface';
/**
* Google Cloud Natural Language API でテキスト分析するクラス
*/
class GoogleCloudNaturalLanguageAPI {
/** ライブラリのインスタンス */
private client;
/**
* コンストラクタ
*/
constructor () {
// ライブラリの初期化
this.client = new LanguageServiceClient();
}
/**
* Google Cloud Natural Language API で 感情分析 します。
* @param text 文章
*/
async detectSentiment(text : string) : Promise<ResultAnalyzedEmotion> {
try {
// Google Cloud Natural Language API に接続
const results : AnalyzeSentiment[] = await this.client.analyzeSentiment({
document: {
content: text,
type: 'PLAIN_TEXT',
}
})
// Google Cloud Natural Language API から結果が返ってきたので、レスポンスから文章全体の気分を取得する
const sentiment : Sentiment = results[0].documentSentiment;
return Promise.resolve({
// 分析した文章
text: `${text}`,
// 感情の極性
score: sentiment.score,
// 感情の絶対的な大きさ
magnitude: sentiment.magnitude,
// sntiment.score は、0 より大きい値ならポジティブ、0 より小さい値ならネガティブ
feeling: `${ sentiment.score >= 0 ? 'ポジティブ' : 'ネガティブ'}`,
});
}
catch (e) {
// Google Cloud Natural Language API からエラーが返ってきた
console.error('ERROR:', e);
return Promise.reject(e);
}
}
}
export default new GoogleCloudNaturalLanguageAPI();
./interface.ts
export interface Sentiment {
magnitude: number;
score : number;
}
export interface Text {
content : string;
beginOffset: number;
}
export interface Sentences {
text : Text;
sentiment : Sentiment;
}
export interface AnalyzeSentiment {
language : string;
documentSentiment: Sentiment;
sentences : Sentences[];
}
export interface ResultAnalyzedEmotion extends Sentiment {
text : string;
feeling: string;
}
ホームページ http://www.ois-yokohama.co.jp
facebook https://www.facebook.com/orientalinformationservice/