エムティーアイ エンジニアブログ

株式会社エムティーアイのエンジニア達による技術ブログ

Alexa のオリジナルスキルを開発したいと思ったときに必要な手順まとめ(アイディアの誕生~お手元の Amazon Echo で遊べるようになるまで)

こんにちは。music.jp というプロダクトの開発を担当しているエンジニアの藤田です。

2017年の下半期ぐらいから Google Home、Amazon Echo、Clova WAVE、Xperia Hello...等々、日本でもスマートスピーカーが盛り上がりを見せはじめ、それにともなって、スマートスピーカーを使ったライフハックの記事や「スマートスピーカーを使って XXXX を作ってみた」といった記事がどんどん増えてきましたね!
私自身も Amazon 印のスマートスピーカー Amazon Echo dot が家に届いてから、Alexa スキル開発に挑戦してみました。今回は簡単なスキルを作る過程をまとめようと思います。たくさんのアイディアと少しのエンジニアリングスキルで簡単に作れるので、是非みなさんも Alexa スキル開発に挑戦してみてください!

事前準備

Alexa のスキル開発をやろうと思うと Amazon の開発者アカウントと AWS のアカウントが必要になります。

AWS をバリバリ使っている開発者の方なら「Amazon 関係で何かを開発するってことは AWS だな!」と最初は思うのではないでしょうか。
ですが、Alexa 自体が Amazon.com 管轄のサービスであるため、Alexa のスキルの設定は AWS アカウントではなく、Amazon.com ドメインの Amazon 開発者アカウントで行うことになります。

既に似たような言葉が複数出てきて混乱しそうですね。
下記のクラスメソッドさんの記事がここらへんの話を詳しく説明してくださっています。一度アカウントを作る前に下記の記事を読んでみたほうがいいかもしれません。

Amazon.comアカウントが優先してAlexaアプリに入れない問題の解決法

各アカウントとその用途のイメージとしては

  • Amazon 開発者アカウント : Alexa スキルの基本事項、対話モデル、使い方等を定義
  • AWS アカウント : Alexa スキルの実際の処理内容を AWS Lambda Function で定義

という感じでしょうか。

Amazon 開発者アカウント取得(アカウント持ってない人は)

Amazon 開発者コンソール から [サインイン] > [Amazon Developer アカウントを作成] で必要情報を入力してアカウントを作成します。

※ Amazon.co.jp のアカウント(いわゆる日本の Amazon でお買い物するアカウント)を登録済みの場合は、Amazon.co.jp アカウントの登録メールアドレスと Amazon 開発者コンソールで登録するメールアドレスを分けたほうが後で余計な混乱を防げます(私は結構混乱して詰まりました)

AWS アカウント取得(アカウント持ってない人は)

AWS コンソール から AWS アカウントを取得します。

※アカウント取得方法は公式の AWS アカウント作成の流れ を読みながら進めるのがいいですね。

自作スキルの基本情報・対話モデル設定

ここからは自作スキルの基本情報・対話モデルを Amazon 開発者コンソールで設定していきます。

今回は麻雀の符数と翻を伝えれば、対応する点数を教えてくれるスキルを作りたいと思います。

Alexa スキルの基本

スキルの設定内容を具体的に見ていく前に、Alexa スキルにまつわる基本用語や概念を整理したいと思います。

  • ウェイクワード(Wake Word)
    • Amazon Echo 等のスマートスピーカーに何か指示を与えるときの合言葉のようなものです。Google Home なら「OK Google」、Amazon Echo なら「Alexa」という言葉にあたります。ウェイクワードはスマートスピーカー毎にいくつか用意されていて、それらの中からユーザーが好きなものを選択することができます。
  • 呼び出し名(Invocation Name)
    • 「Alexa、XXXX を開いて」の XXXX にあたるワード。Alexa ができること(スキル)はいくつも登録できることができ、その中でどのスキルを呼び出すかを識別するための名前です。
  • インテント(Intent)
    • ユーザーがどのような意図や要望を持って Alexa のスキルを呼び出したかを表す概念です。抽象的で分かりづらいのですが、次に説明する サンプル発話 と併せて理解するのがいいと思います。実際の処理を行う Lambda Function ではこのインテントに対応するハンドラを定義することになります。
  • サンプル発話(Sample Utterances)
    • ユーザーがこのスキルを使うときに、どのような発話をするのかを予めサンプルとして定義するものです。このサンプル発話に対して上記の インテント を紐付けることで、ユーザーの発話内容を最終的に Lambda Function のハンドラと結びつけるわけです。
  • スロット(Slots)
    • サンプル発話の中で扱える変数のようなものです。数字を扱うようなサンプル発話が必要になったときに「Alexa サンプルスキルで1回話して」「Alexa サンプルスキルで2回話して」・・「Alexa サンプルスキルで100回話して」というのを全て登録していたら気が狂いますもんね(そもそも数字が有限じゃないと破綻する)。この数字の部分をスロットという変数のような概念で扱えるようにしたものです。

ここまでの内容を簡単な図でまとめてみます。

f:id:tasmaniadecoco:20180114224349p:plain

あるユーザーが対局中にある役であがりました。そして点数を知りたいのですが覚えていません。
そこで「子の○符△符の点数を知りたいなー」というインテントが生まれるわけです。
このとき、次のような流れで処理が行われます。

  1. ユーザーが近くの Amazon Echo に向かって、「ウェイクワード + 呼び出し名 + インテントと対応するサンプル発話」の形式に従って、発声します。
  2. Amazon Echo はユーザーの発声内容を Alexa にデータとして渡します。
  3. Alexa は予め登録されているスキルの中から呼び出し名と対応するスキルを探し出し、そのスキルで定義されているサンプル発話から対応するインテントを見つけます。
  4. Alexa は予めエンドポイントを登録していた Lambda Function にそのインテントを伝えます。
  5. Lambda Function はインテントと対応する処理を行い(ここでは 30 符 2 翻の点数が何かを計算して、ユーザーに返す発声内容を決める)、Alexa にレスポンスを返します。
  6. Alexa はレスポンス内容を Amazon Echo に伝え、Amazon Echo がユーザーに向けて発声します。

概要の話が長くなってしまいました。早速具体的にスキルを作る手順を説明します。
Amazon 開発者コンソール > [Developer Console] を辿って、先程作成したアカウントでサインインした状態で、Alexa スキルの新規作成 に遷移しましょう。
この時点で Amazon 開発者コンソールで設定する項目は下記の通りです。

  • スキル情報
  • 対話モデル
  • 設定

スキル情報

f:id:tasmaniadecoco:20180114232101p:plain

  • スキルの種類
    • カスタム対話モデル を選択
  • 言語
    • Japanese を選択
  • スキル名
    • Alexa のアプリのスキル一覧で表示されるスキル名です。わかりやすくかつキャッチーに、目を引く名前にしましょう。
  • 呼び出し名
    • 「Alexa、XXXX を開いて」の XXXX にあたるワード。何を呼び出し名にするかによって使いやすさは変わります。覚えやすく、かつ曖昧さのない名前にするのがいいと思います。

※スキル情報を保存するとアプリケーションIDが決まります。このアプリケーションIDは後に使うことになるのでメモしておきましょう。

対話モデル

  • インテントスキーマ
    • インテントとそのインテントで使用するスロットの定義を JSON 形式で指定します。ちなみに今回の麻雀の点数を教えてくれるスキルでは下記のようなインテントスキーマになりました。
    • 下記の例では AMAZON.HelpIntentAMAZON.CancelIntentAMAZON.StopIntent のように、Alexa が標準で搭載しているビルトインインテントtellChildPoint のように今回のスキル用に新たに定義したインテントを定義しており、いくつかのインテントには次に説明するカスタムスロットも定義されています。
  {
    "intents": [
    {
      "intent": "AMAZON.HelpIntent"
      //スキルのヘルプを知りたいというインテント
      //(サンプル発話では標準で「ヘルプ」のような言葉に対応する) 
    },
    {
      "intent": "AMAZON.CancelIntent"
      //スキルのキャンセルしたいというインテント
      //(サンプル発話では標準で「キャンセル」のような言葉に対応する)
    },
    {
      "intent": "AMAZON.StopIntent"
      //スキルを停止したいというインテント
      //(サンプル発話では標準で「ストップ」のような言葉に対応する)
    },
    {
      "slots": [
        {
          "name": "fu",
          "type": "CALL_FU"
        },
        {
          "name": "han",
          "type": "CALL_HAN"
        }
      ],
      "intent": "tellChildPoint"
      //子のロンのときの点数が知りたいというインテント
    },
    {
      "slots": [
        {
          "name": "fu",
          "type": "CALL_FU"
        },
        {
          "name": "han",
          "type": "CALL_HAN"
        }
      ],
      "intent": "tellParentPoint"
      //親のロンのときの点数が知りたいというインテント
    },
    {
      "slots": [
        {
          "name": "fu",
          "type": "CALL_FU"
        },
        {
          "name": "han",
          "type": "CALL_HAN"
        }
      ],
      "intent": "tellChildTsumoPoint"
      //子のツモのときの点数が知りたいというインテント
    },
    {
      "slots": [
        {
          "name": "fu",
          "type": "CALL_FU"
        },
        {
          "name": "han",
          "type": "CALL_HAN"
        }
      ],
      "intent": "tellParentTsumoPoint"
       //親のツモのときの点数が知りたいというインテント
    }]
  }
  • カスタムスロットタイプ
    • サンプル発話で扱えるスロットですが、AMAZON.DATEAMAZON.NUMBERAMAZON.TIME のように、Alexa が標準で搭載しているビルトインスロットと、CALL_FUCALL_HAN のように今回のスキル用に新たに定義したカスタムスロットがあり、この項目ではカスタムスロットタイプのタイプの名前と値を定義します。
  • サンプル発話
    • インテントスキーマで定義したインテントと紐付けるユーザーの発話のサンプルを登録します。ちなみに今回の麻雀の点数を教えてくれるスキルでは下記のようなサンプル発話を登録しました。1つのインテントに対するサンプル発話は1つとは限らず、様々な言い回しがあったりします。それらの違いを上手く吸収してユーザーが不便にならないようにサンプル発話、ひいては対話モデルを設計する必要があります。VUI 設計という新しい技能になってくるところですね。

      tellChildPoint {fu} {han}
      tellChildPoint こども {fu} {han}
      tellChildPoint {fu} {han} こども
      tellChildPoint こどもの {fu} {han}
      tellChildPoint この {fu} {han}
      tellParentPoint おや {fu} {han}
      tellParentPoint おやの {fu} {han}
      tellParentPoint {fu} {han} おや
      tellChildTsumoPoint こども {fu} {han} つも
      tellChildTsumoPoint こどもの {fu} {han} つも
      tellChildTsumoPoint こどものつも {fu} {han}
      tellChildTsumoPoint つも {fu} {han} こども
      tellChildTsumoPoint つも {fu} {han}
      tellParentTsumoPoint おや {fu} {han} つも
      tellParentTsumoPoint おやのつも {fu} {han}
      tellParentTsumoPoint おやの {fu} {han} つも
      tellParentTsumoPoint つも {fu} {han} おや

設定

  • エンドポイント
    • このスキルが呼び出され、サンプル発話からインテントが明らかになった後に実際の処理を行うエンドポイントを指定します。今回は Lambda Function を使用したので、後に作成する Lambda Function の ARN を入力します。
  • アカウントリンク
    • Alexa を介して既存の別のサービスを呼び出す場合に別サービスのアカウントと Alexa ユーザーを紐付けることができます。今回は使用しません。
  • アクセス権限
    • Alexa を呼び出しているデバイスの位置情報(住所、郵便番号など)、Alexa アプリに登録しているやることリストへのアクセスを許可するかを設定します。今回は使用しません。

ここまででスキルの基本事項は登録できたので、一度スキルの内容を保存しましょう。

自作スキルの処理を実装

次に実際にスキルが実行する処理を Lambda Function で実装しましょう。
※この記事では Lambda Function 自体の詳しい説明、及び Lambda Function で扱う Node.js に関する説明は行いません

今回の麻雀の点数を教えてくれるスキルでは下記のような実装になりました(一部抜粋)

'use strict';
const Alexa = require('alexa-sdk');
const Table = require('./PointTable'); // 得点表を const で全部定義しておく

// Alexa スキル実装時のおまじない部分
exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = //Alexa のスキル登録のときに決まったアプリケーション ID を入れる;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

// ハンドラの定義
const handlers = {
    'LaunchRequest': function(){
      this.emit(':ask', '麻雀やろうぜ!なんじっぷなんはんの点数が知りたいんだい?','なんじっぷなんはんの点数が知りたいんだい?');        
    },
    'tellChildPoint': function(){
      // 対話モデルで定義したスロットは this.event.request.slots.{スロット名}.value で取り出せます
      const fu = this.event.request.intent.slots.fu.value;
      const han = this.event.request.intent.slots.han.value;
      // ロンで、符が fu、翻が han のときの得点を const の table から取得
      // 本当は fu や han が undefined の場合などの例外処理が必要だけど、省略
      const point = Table.ChildPointTable["ロン"][fu][han];      
      this.emit(':tell', `子の、 ${fu}${han} は、${point}です`);
    },
    'tellParentPoint': function(){
        // tellChildPoint と同様に親のロンの点数を取得して this.emit(':tell',`点数の内容`) を実行する
    },
    'tellChildTsumoPoint': function(){
        // tellChildPoint と同様に子のツモの点数を取得して this.emit(':tell',`点数の内容`) を実行する
    },
    'tellParentTsumoPoint': function(){
        // tellChildPoint と同様に親のツモの点数を取得して this.emit(':tell',`点数の内容`) を実行する
    },            
    'AMAZON.HelpIntent': function() {
        this.emit(':tell', 'スキルの使い方を説明するよ。面倒だから省略するよ');
    },
    'AMAZON.CancelIntent': function() {
        this.emit(':tell', '麻雀おーわり');
    },
    'AMAZON.StopIntent': function() {
        this.emit(':tell', '麻雀おーわり');
    },
    'Unhandled': function(){
        this.emit(':tell', '正しく応答できないっす!');        
    }
};

alexa-skills-kit-sdk-for-nodejs が公開されているので、それを参考にしながら各インテントと対応するハンドラの処理を定義していく作業になります。
上記のプログラムの構造を詳しく説明していきます。

Alexa スキル実装時のおまじない部分

Alexa スキル用の sdk 使った実装では必ずと言っていいほど、登場する部分です。
ここは state を使って状態ごとに複数のハンドラを使い分けたり、resource を使って返答内容をローカライズしたりする時以外は、上記の内容を真似するだけでいいと思います。
Alexa スキルのアプリケーションIDの登録を忘れずに。

ハンドラの定義

基本的には

  'インテント名1' : function(){
     // インテント1に対応する処理内容
  },
  'インテント名2' : function(){
     // インテント2に対応する処理内容
  },
  :

のように、インテントに対応する function を定義すればいいだけです。
ただ、これまでの説明に出てきていない LaunchRequestUnhandled というインテントが定義されています。
どちらもビルトインインテントで、LaunchRequest は「Alexa、{呼び出し名}を開いて」という発声に反応するインテントで、 Unhandled はユーザーのサンプル発話からインテントがわからなかったときなどに呼び出されるインテントです。

また、各 function 内の最後の this.emit() でユーザーに対する応答を返すわけですが、その返し方にはいろいろな種類があります。
今回使った二種類に関して補足で説明します。

  • this.emit(':tell', speechOutput)
    • speechOutput に入れた文字列を読み上げてセッションを終了する
  • this.emit(':ask', speechOutput, repromptSpeech)
    • speechOutput に入れた文字列を読み上げたあと、ユーザーの返答を数秒間待つ。数秒間ユーザーの返答がない場合は repromptSpeech を読み上げて、再度ユーザーの返答を数秒待つ。再度数秒間ユーザーの返答がない場合はセッションを終了する。

その他やらなければいけないこと

基本的には Lambda Function の実装を Alexa に登録したスキルのインテントスキーマ、サンプル発話に合わせて作ればスキルは簡単に作れます。
ここではその他 Lambda 側の設定で忘れてはいけないことをいくつか書きます。

  • Lambda Function のトリガーの設定
    • 作成した Lambda Function のトリガーには Alexa Skill Kit を設定しておきましょう。ここの設定を忘れていると、Alexa のスキル設定で Lambda Function のエンドポイントを登録するときにはじかれてしまいます。
  • Lambda Function のエンドポイント情報をメモ
    • 作成した Lambda Function の ARN をメモしておきましょう。Alexa のスキルに Lambda Function を登録するときに使います。

ここまでで Lambda Function 側の設定は終わりです。

自作スキルのβテスト

ここからは Amazon の開発者コンソールに戻り、βテスト用にスキルを限定公開するために下記の項目も設定していきましょう。

  • テスト
  • 公開情報
  • プライバシーとコンプライアンス

テスト

スキルの対話モデルが正常に登録できていて、Alexa のスキルの [設定] でサービスのエンドポイントに先ほど作成した Lambda Function の ARN を登録できていれば、コンソール上で作成済みのスキルをテストできるはずです。

下の図のようにテキスト入力でサンプル発話を与えると、対応する結果を返してくれることがわかります。

f:id:tasmaniadecoco:20180115020851p:plain

公開情報

一般公開する際には Alexa アプリのスキル一覧に表示される情報になるので、大変重要な設定項目です。
ただ、取り急ぎ自分の Amazon Echo で試したいだけならβテストとして公開するはずなので、カテゴリーから画像まで適当なものを設定すれば良いです。

プライバシーとコンプライアンス

こちらの設定も一般公開する際には重要になってきます。また、作るスキルの特性にもよりますね。
ただ、βテストで試すだけならば適当な設定で構いません。

Skill Beta Testing

Alexa スキルの設定項目全てに緑のチェックマークがつくと、画面左下にチラチラ見えている Skill Beta Testing が行えるようになります。
複数のメールアドレス宛にβテスト用の招待メールを送ることができます。招待メールにはβテストの対象のスキルを enable するリンクがあるので、そちらをクリックしてスキルを有効にしましょう。

ここまでいけば、お手元の Amazon Echo と連携している Alexa アプリ上に自作の Alexa スキルが表示されるはずです。 こちらのスキルを有効にすれば、自分の Amazon Echo で自作スキルを試すことができます!

f:id:tasmaniadecoco:20180115111914j:plain

最後に

今回 Alexa スキルを自作してみて大きく思ったことが2点ありました。

  1. スキルの各設定や対話モデルをコードで管理したい
  2. スロットに数字を使ったスキルでは日本語ならではの罠がある

1 はスキルの各設定項目やインテントスキーマなどをコンソール上で設定していく作業が面倒臭い上、バージョン管理できないのが難儀だなと思いました。
しかしながら、やはりそういったニーズを満たす Serverless Framework のプラグインを作っている方がいました!

Alexa Skillの開発をServerless Frameworkだけで完結するための「Serverless Alexa Skills Plugin」の紹介

これは早速取り入れなければと思い、実は今回のスキルは上記のプラグインを使ってスキルの対話モデルと Lambda Function のどちらも Serverless Framework を使って管理していました。とても楽でした笑
その話もまた別のエントリで書けたらいいなと思います。


2 の方ですが、鋭い方は今回のスロットの定義に疑問を感じたのではないでしょうか?
今回の麻雀の得点を伝えるスキルで肝になる「○符△翻」の表現で、○と△の部分をスロットにする場合に何故 AMAZON.NUMBER を使わずにわざわざカスタムスロットを定義したのかと。
答えは Alexa が認識してくれないから です。
例えば、日本人の比較的麻雀ができる人の自然な感覚で「30符2翻」を音に直すと、「さんじ(ゅ)っぷりゃんはん」ではないでしょうか。
スロットとサンプル発話の関係を考慮して上記を分解すると下記のようになると思います([] がスロット部分に相当)

[さんじっ] 符 [りゃん] 翻

まず間違いなく「りゃん」が数字の2を表すとは Alexa は思わないですよね笑
だから少なくとも翻の方のスロットはカスタムスロットとして、麻雀で使われる可能性がある翻数全てを数字としてではなくて文字列として定義することにしました。
また、「さんじっ」の方ですがこちらも中々30として認識してくれません。今回のケースでいくと前半部分を「30分」と認識することが多かったです。
とても辛かったですが、符の方も20符から110符まで文字列として「にじっぷ」、「ひゃくじっぷ」のように文字列として定義することとしました。

ちなみにですが、これは日本語ならではの問題かなと思います。英語では「30符2翻」は下記のように表すと思うからです。

Thirty fu and two han

正確な英語表現は分かりませんが、少なくとも数字部分を表現する発音はその後に続く単位の部分には依存しないからです。  


大変長くなりましたが、今回自分で作ってみたからこそ、日本語の難しさを再発見することができました。
スキル自体もエラーハンドリングが不完全だったり、ユーザビリティを考えられていなかったりするので、一般公開して使ってもらえるようなスキルにブラッシュアップしていきたいです。みなさんもアイディアを形にしてみて、いいスキルを世の中に増やしていきましょう!