GoogleHome向けのActionを作ってみた

はじめに

先日、Google Home Mini を購入したので、せっかくだし自作のアプリでも作ってみるかと思い、季節を答える簡単なActionを作成してリリースしてみた。
一応リリースできたけど、何回かリジェクトされたりしたので、どんな感じに作ったかや、自分の理解を深めるために雑にまとめてみる。

世界観

色々なサイトを見れば詳しい解説が出ているが、一応ザックリとまとめてみる。

スマートスピーカー

GoogleHomeやHome mini などのような賢いスピーカー。我々人間がやりとりを行う時の相手になるもの。

Google Assistant

Googleが開発したAIアシスタントスマートスピーカーに住んでいる妖精で、内容に応じたAgentを呼び出してくれる。

Actions on Google

Actionを開発するためのプラットフォーム。

Action

Google Assistantで使えるサードパーティ製のアプリ的なもの。

Dialogflow

自然言語理解(NLU)エンジンを積んでいて、GUIから簡単に会話型のインターフェイス(独自Agent)を構築できる機能。

例えば、天気を尋ねる時などに、言葉の微妙なニュアンスの違いがあったとしても、Dialogflow はそれを吸収して構造化された形式に変換してくれる。 ユーザからの「今日」「明日」「1月1日」などという発話は 「年/月/日/時間」の構造を持っているので、Dialogflowが曖昧な発話を「年/月/日/時間」の構造に変換して渡してくれるような理解。 他の例としては、「今日から明日」「7月」などは、「期間(from - to)」の構造を持っているので「年/月/日/時間 - 年/月/日/時間」に変換するなどなど。

プロジェクトを作成する

Googleで「Actions Console」で検索すると、一番上に Actions Consoleが出てくると思うので、そこから Actions on google のコンソールに入る。

f:id:kyonta1022:20190629194551p:plain

「New Project」 から新規プロジェクトを作成する。

f:id:kyonta1022:20190629195016p:plain

どんなものを開発しますか?的なことを聞かれるので、今回は会話型(Conversational)を選択する。

f:id:kyonta1022:20190629195340p:plain

プロジェクトの概要ページに移る。

f:id:kyonta1022:20190629200549p:plain

UI はかなり親切な設計になっており、上から

  • Quick Setup
    • Actionの起動方法決める
  • Build your Action
    • Actionの作成とテスト
  • Get ready for deployment
    • リリースするに当たってActionの説明(Directory)などを設定する
  • Release
    • Release申請する

という流れになっており、リリースするまでにやるべき事がパッとわかるようになっているので、迷う事なく進められる。

Actionの名前を決める

「Quick setup」を選択して、 声とActionの名前を設定する(後からも変更可能)
Actionの名前は、よくある単語や一般的に使われそうな名前ではダメというポリシーがあるらしいので、命名時は少し気にするといいかも。自分は「にじゅうしせっき」って付けたらリジェクトされた。

ちなみに、今回は女性の名前で作成したのだが、Female 2 の方が喋り方が自然だったので、Female 1ではなくFemale 2をオススメする。 Female 1 は、よくある機械が喋るような発声なので、聞くとガッカリする。

入力が完了したら、右上に出ている「Save」を忘れないようにする。

f:id:kyonta1022:20190629201323p:plain

Actionの作成

サイドメニューに表示されている Actions を選択するか、概要ページの「Add Action(s)」を選択する。

f:id:kyonta1022:20190629202322p:plain

組み込みやテンプレートみたいなのものもあるが、今回はカスタムインテントを選択する。選択すると Dialogflow のページに遷移する。

f:id:kyonta1022:20190629202501p:plain

言語を選択してAgentを作成する。
AgentとはDialogflow のプロジェクト。管理単位。

f:id:kyonta1022:20190629202844p:plain

会話を構築する

用語の説明

会話を構築するにあたり、いくつか用語があるのでまとめとく。

Intent

Intent とは、ユーザからの発話に対する処理の定義的なもので、インテント毎にどのような発話に反応(Training phrases)するかを決めて、それに対してどのような応答(Response)をするかなどを定義できる。いくつか項目があるので、ざっとまとめる。

Context

コンテキストにはinoutがある。使い分けとしては、このインテントが実行されるときに、別のインテントで発生したパラメーターなどを受け継ぐ時は、inにそのコンテキスト名を指定すると使えるようになる。outは、このインテントで発生したパラメータを別のインテントに引き継ぎたい場合に利用する。

会話の文脈的な意味合いから Cotext という名前がついたのかな?と覚えた。

Events

イベントはユーザからの発話ではなく、何か出来事が起こった時にインテントを起動するためのもの。トリガー。

Training phrases

レーニングフレーズは、ユーザからどんな「発話」を受け取った時にインテントが起動するかを決める。

また、フレーズの内容は構造化されたパラメーターとして受け取る事ができる。
例えば「6月6日は」と定義した場合、「年/月/日」の部分を「sys.date」パラメータに設定することができるため、そのパラメータを Responseや Fulfillment で利用する事ができる。

Action and parameters

上のフレーズから抽出できるパラメータが自動的に設定される。
※ 手動で設定しなければいけない場合もある

Responses

このインテントの応答。
複数の応答を定義すると、その中からランダムに返してくれる。

Fulfillment

これを有効にすると、このインテントが呼ばれた場合に webhook する事ができる。 ちなみに、webhook は Actionにつき一つしか設定できないので、インテントでは使用有無しか設定できない。

構築

今回はこんな感じの会話を想定して構築した。

ちなみに、会話の構築で注意することは、ユーザがどのような発話の候補を持っているか を明確に教えてあげないと Release 時にリジェクトされるので、インテント毎に次にどんな発話を望んでいるか(想定)を丁寧に示すのがいいと思う。

[Action Start]

> こんにちわ、季節をお答えします。

XX月YY日は?

> XX月YY日の季節は・・・

もう一回

> XX月YY日の季節は・・・

おわり

> ご利用ありがとうございました。

[Action End]

作成したインテントとしては、

Welcomeインテント

Action起動時に機能の説明をして欲しいので、Eventの設定とResponseのみを設定している。

f:id:kyonta1022:20190629214653p:plain

Mainインテント

ユーザからの発話から日付を取得して、該当する季節を答えるため、Training phrases と Parameter と Filfullment を設定している。

f:id:kyonta1022:20190629222846p:plain

Filfullmentでは、Inline Editorを使ってサーバー側の処理を書いた。

'use strict';

function solar_term(now) {
  const SOLAR_TERM = [
    {month: 1, date: 5, name:"小寒", name_kana: "しょうかん", description: "池や川の氷も厚みを増し、寒さが厳しくなる頃です。この日を「寒の入り」といい、寒さの始まりを意味します。そして、小寒と大寒を合わせたおよそ1か月を「寒中」「寒の内」といい、寒中見舞いを出す時期とされています"},
    {month: 1, date:20, name:"大寒", name_kana: "だいかん", description: "冷え込みもはげしく、寒さが最も厳しい頃。二十四節気の最後の節気で、ここを乗り切れば春近しということです。寒気を利用した食物(凍り豆腐、寒天、酒、味噌など)を仕込む時期にもあたります"},
    {month: 2, date: 4, name:"立春", name_kana: "りっしゅん", description: "二十四節気の最初の節気で、この日から暦の上では春となり、さまざまな決まりごとや節目の基準になっています。旧暦では立春近くに正月がめぐってきたので、立春は春の始まりであり、1年の始まりでもありました。まだまだ寒さは厳しいですが、立春を過ぎてから初めて吹く強い南風を「春一番」といいます"},
    {month: 2, date:19, name:"雨水", name_kana: "うすい", description: "雪から雨へと変わり、降り積もった雪も溶けだす頃という意味です。実際にはまだ雪深いところも多く、これから雪が降り出す地域もありますが、ちろちろと流れ出す雪溶け水に、春の足音を感じます"},
    {month: 3, date: 6, name:"啓蟄", name_kana: "けいちつ", description: "大地が温まって、冬ごもりから目覚めた虫が、穴をひらいて顔を出す頃。「啓」はひらく、「蟄」は土の中にとじこもっていた虫(蛙や蛇)という意味です。ひと雨ごとに暖かくなり、日差しも春めいて、生き物が再び活動し始めます"},
    {month: 3, date:21, name:"春分", name_kana: "しゅんぶん", description: "昼夜の長さがほぼ同じになる日で、この日を境に陽が延びていきます。春分の日は彼岸の中日で前後3日間を春彼岸といい、先祖のお墓参りをする習慣があります。「自然をたたえ、生物をいつくしむ」として国民の祝日になっています"},
    {month: 4, date: 5, name:"清明", name_kana: "せいめい", description: "清明は「清浄明潔」の略で、万物がけがれなく清らかで生き生きしているという意味です。花が咲き、鳥は歌い、空は青く澄み、爽やかな風が吹き、すべてのものが春の息吹を謳歌する頃。各地でお花見シーズンを迎えます"},
    {month: 4, date:20, name:"穀雨", name_kana: "こくう", description: "春の柔らかな雨に農作物がうるおうという意味です。この時期に農作物の種をまくと、雨に恵まれ、よく成長するといわれています"},
    {month: 5, date: 6, name:"立夏", name_kana: "りっか", description: "この日から立秋の前日までが暦の上では夏となります。新緑に彩られ、さわやかな晴天が続く頃です。ちょうどゴールデンウィークの時期にあたり、レジャーに出かけるにもよい気候です"},
    {month: 5, date:21, name:"小満", name_kana: "しょうまん", description: "陽気がよくなり草木が成長して茂るという意味です。農家では田植えの準備を始める頃。動物や植物にも活気があふれます。また、秋にまいた麦の穂が付くころで安心する(少し満足する)という意味もあります"},
    {month: 6, date: 6, name:"芒種", name_kana: "ぼうしゅ", description: "「芒」とはイネ科植物の穂先にある毛のような部分のことで、稲などの穀物の種をまく時期という意味です。田植えの目安とされ、農家が忙しくなる時期。梅雨入りも間近で少し蒸し暑くなってくる頃です"},
    {month: 6, date:21, name:"夏至", name_kana: "げし", description: "北半球では、太陽が最も高く昇り、1年で最も昼が長い日です。ただ、日本では梅雨のシーズンでもあるので、日照時間が短く、あまりひの長さを実感できないかもしれません。暦の上では夏の折り返し地点にあたり、夏至を過ぎると暑さが増して本格的な夏がやってきます"},
    {month: 7, date: 7, name:"小暑", name_kana: "しょうしょ", description: "だんだん暑さが増していくという意味で、梅雨明けも近くなり、湿っぽさの中にも夏の熱気が感じられるようになります。海や山に出かけるのにもいい時期です。また、小暑と大暑を合わせたおよそ1か月を「暑中」といい、「暑中見舞い」を出す期間とされています"},
    {month: 7, date:23, name:"大暑", name_kana: "たいしょ", description: "夏の暑さが本格的になるという意味ですが、子どもたちは夏休みに入ってわくわく。農家にとっては田の草取り、害虫駆除など暑い中での農作業が続く大変な時期です。また、土用の丑の日が近く、夏バテ防止にうなぎを食べたりする頃です"},
    {month: 8, date: 7, name:"立秋", name_kana: "りっしゅう", description: "厳しい残暑は続きますが、この日から暦の上では秋となります。これからは少しずつ涼しくなり、秋の気配が漂いだす頃です。また、立秋を過ぎたら「暑中見舞い」は「残暑見舞い」に変わります"},
    {month: 8, date:23, name:"処暑", name_kana: "しょしょ", description: "さがおさまるという意味で、日中は暑いものの、朝晩の涼しさに初秋の息遣いを感じる頃です。夏休みもそろそろ終わり。秋の台風シーズンに入っていきます"},
    {month: 9, date: 8, name:"白露", name_kana: "はくろ", description: "秋が深まり、草花に朝露がつきはじめる頃という意味です。空は高くなり、秋雲がたなびくようになり、本格的な秋の到来です。また、実りの秋を前に台風が心配な時期でもあります"},
    {month: 9, date:23, name:"秋分", name_kana: "しゅうぶん", description: "昼夜の長さがほぼ同じになる日で、この日を境に日が短くなり、秋の夜長に向かいます。秋分の日は彼岸の中日で前後3日間を秋彼岸といい、先祖のお墓参りをする習慣があります。「祖先を敬い、亡くなった人をしのぶ日」として国民の祝日になっています"},
    {month:10, date: 8, name:"寒露", name_kana: "かんろ", description: "草木に冷たい露が降りる頃という意味です。秋の長雨が終わり、ぐっと秋が深まります。稲刈りが終わるころで、その他の農作物の収穫もたけなわとなります。また、北の方から紅葉の便りが届きはじめます"},
    {month:10, date:23, name:"霜降", name_kana: "そうこう", description: "早朝に霜が降りはじめる頃という意味です。晩秋を迎え、北の方では朝霜が降り、山々は紅葉に染まります"},
    {month:11, date: 7, name:"立冬", name_kana: "りっとう", description: "この日から立春の前日までが暦の上では冬となります。木枯らしが吹き、冬の訪れを感じる頃。太陽の光が弱まって日も短くなり、木立ちの冬枯れが目立つようになります。木枯らしが吹くのは、冬型の気圧配置になった証拠です"},
    {month:11, date:22, name:"小雪", name_kana: "しょうせつ", description: "木々の葉が落ち、山には初雪が舞い始める頃です。「小雪」とは、冬とは言えまだ雪はさほど多くないという意味で、冬の入口にあたります"},
    {month:12, date: 7, name:"大雪", name_kana: "たいせつ", description: "山の峰々は雪をかぶり、平地にも雪が降る頃です。本格的な冬の到来で、動物たちも冬ごもりを始めます。年末に向け、お正月の準備も始まって、何かとあわただしい時期でもあります"},
    {month:12, date:22, name:"冬至", name_kana: "とうじ", description: "太陽が最も低い位置にあり、1年で最も夜が長く、昼が短い日です。太陽の力が一番弱まる日ですが、翌日からは再び強まるということから、運が向いてくるとされています。また、冬至かぼちゃ、冬至がゆ、柚子湯などで、厄払いや無病息災を願う風習があります"},
  ];
  
  var index = SOLAR_TERM.findIndex(season => {
    if (new Date(now.getFullYear(), season.month -1, season.date) > now) {
      return season;
    }
  });
  return (index <= 0) ? SOLAR_TERM[SOLAR_TERM.length -1] : SOLAR_TERM[index - 1];
}

function say(date, season) {
  var say_season = `${date.getMonth() + 1}月${date.getDate()}日は、${season.name_kana}の季節です。${season.description}。。`;
  var announcement = "別の日について知りたい場合は、希望する日付を指定してください。終わりにする場合は、「終わり」と言って下さい。";
  return say_season + announcement;
}

const functions = require('firebase-functions');
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

app.intent('One More', conv => {
    const season = conv.contexts.get("season");
    if (typeof season === 'undefined') {
      conv.ask("希望する日付を指定してください。");
    } else {
      conv.ask(season.parameters.say);
    }
});

app.intent('Another Date', (conv, params) => {
    const date = new Date(params.date);
    const answer = say(date, solar_term(date));
    conv.contexts.set("season", 10, {say: answer});
    conv.ask(answer);
});

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

One Moreインテント

Mainインテントで設定されたseasonコンテキスト(conv.contexts.set("season", 10, {say: answer});)をもらい、同じ内容で応答するために、 Training phrases と Parameter と Response を設定している。

f:id:kyonta1022:20190629224139p:plain f:id:kyonta1022:20190629224154p:plain

Exitインテント

Actionを終了するために、Training phrases と Response と「set this intent as end of conversation」 を設定している。

f:id:kyonta1022:20190629224557p:plain f:id:kyonta1022:20190629224615p:plain

リリースするにあたってディレクトリを設定する

概要の「Enter information required for listing your Action in the Actions directory」を選択する

f:id:kyonta1022:20190629230130p:plain

内容はこのくらいあるが、注意すべきところだけここでは取り上げる

f:id:kyonta1022:20190629230324p:plain

Description

Actionが何をできるかを簡潔に書く。曖昧だとリジェクトされる。

Sample Invocations

Actionを起動するときのフレーズを設定する。最初に設定したAction名からかけ離れているとリジェクトされる。
ちなみに、同じフレーズでも「ひらがな」と「漢字」版など、想定されそうなフレーズは複数設定しといたほうがいい。自分は最初、ひらがなだけのフレーズを設定していたが全然Actionが起動されず、スマホGoogle Assistant で試してみたら、発話の一部分が漢字に変換されていてフレーズに一致しない事象が起きていた。

Images

好きなアイコンと背景画像をどうぞ。

Contact details

連絡先情報と開発者名(任意)をどうぞ。

Privacy and concent

プライバシーポリシーを明記したリンクを設定する。最初はよく分からなかったので、他のActionのプライバシーポリシーを参考にした。
ちなみに、プライバシーポリシーはGoogleサイトで作成した。

リリース

リリースボタンを押して、アプローブされるのを待とう。
レスポンスが結構早いのと、あちら側でも簡単なテストをやってくれるのでありがたい。
曖昧な説明や動作がおかしかったりするとリジェクトされる。

リリース後

ちなみにリリースから数日経つと、リリース記念に「Tシャツ」と「毎月200ドルのGoogle Cloudクレジット」がプレゼントされる。これは嬉しい。