WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

Amazon Echo (Alexa) 用に音声対話クイズのスキルを作成してみた

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
最近はAmazon Echo、Google Home、Clova WAVEと次々に新しいスマートスピーカーが出ていますね。
私も流行りに乗ってAmazon Echo用のAlexaスキルの作成を試してみたのでご紹介します。

作成するAlexaスキル

今回はクラッシュフィーバーのクイズを出題するスキルを試作します。
予め設定した問題を出題しユーザの回答(番号)によって正解か不正解かを返します。
流れとしては以下のような形です。

  1. Alexaから問題を出題
  2. ユーザが1番〜n番までの番号で回答
  3. Alexaから「正解」or「不正解」を返す
  4. 1に戻る(最後まで出題し終えたらAlexaから正解数を発表)

それでは早速始めます。


必要な事前登録

以下への登録が必要です。
developer.amazon.com aws.amazon.com


スキルの作成

開発者ポータルからAlexaのタブを開いて「Alexa Skills Kit」を始めます。
f:id:y-matsushita:20171128174147p:plain:w700

開いたら画面右上の「新しいスキルを追加する」から新規作成します。
f:id:y-matsushita:20171128125454p:plain:w200

スキル情報を入力して保存します。
f:id:y-matsushita:20171201125957j:plain:w700
今回は以下のように設定しました。
・スキルの種類:カスタム対話モデル
・言語:Japanese
・スキル名:クラフィクイズ
・呼び出し名:クラッシュフィーバー
・グローバルフィールド:全ていいえ

対話モデルの生成

次に対話モデルを生成します。
ユーザから受けた命令がどういった意図なのかAlexaが判断できるようにするための設定です。
今回はスキルビルダーから生成します。*1
以下のボタンからスキルビルダーを起動してください。
f:id:y-matsushita:20171128125823p:plain:w250

Intentの追加

Intentsタブの「ADD+」をクリックします。
f:id:y-matsushita:20171128130008j:plain:w700

追加できるIntentはユーザが独自で定義するCustom intentと予め用意されたBuilt-in libraryの2種類があります。
今回は以下のIntentを追加します。

  • Custom intent
     - QuizIntent
     (ユーザがクイズに回答する際に呼び出し)

  • Built-in library
     - AMAZON.RepeatIntent
     (ユーザがクイズの問題を聞き直したい場合に呼び出し)
     - AMAZON.StartOverIntent
     (クイズをスタートする場合に呼び出し)

まずはCustom intentのQuizIntentから追加します。
テキストボックスに「QuizIntent」と入力しCreate intentを押下します。 f:id:y-matsushita:20171128142412p:plain:w700

次にユーザが話す想定の文章を入力します。
ここでは以下のような文章を入力しました。

  • {Answer} 番です
  • {Answer} 番
  • 正解は {Answer} 番

f:id:y-matsushita:20171128144513p:plain:w700
ユーザには回答を番号で答えてもらう想定で、情報を取得したい箇所を{Answer}としています。
注意点として{Answer}の前後には半角スペースが入っていないと後のビルド時にエラーとなってしまうようです。
編集箇所が多くなった後でエラーが発生すると、どこが該当箇所か分からなくなるので注意しましょう。
また、Intentに{Answer}が入った文章を追加するとIntentSlotsに自動でAnswerが追加されます。
Slotには{Answer}で取得できる情報の例を追加します。今回は定義済みのAMAZON.NUMBER(数値情報)がありますので、それを設定してCustom intent の設定は完了です。

あとはBuilt-in libraryのAMAZON.RepeatIntentとAMAZON.StartOverIntentも同様に追加します。
Built-in library のIntentは何も例文を入れなくてもある程度認識してくれますが、ひとまず絶対に認識してほしい文章を入れておきます。今回はAMAZON.RepeatIntentに「もう一回教えて」などのようにクイズの問題を聞き直す文章を入れ、AMAZON.StartOverIntentに「ゲームスタート」とクイズを開始する言葉を入れました。
AMAZON.RepeatIntentとAMAZON.StartOverIntentからは取得したい情報が無いためSlotは設定しません。
まとめると今回は以下のような形で登録しました。

IntentSlot
QuizIntent ・{Answer} 番です
・{Answer} 番
・正解は {Answer} 番
{Answer}
・AMAZON.NUMBER
AMAZON.RepeatIntent もう一回教えて
もう一度教えて
無し
AMAZON.StartOverIntent ゲームスタート 無し

最後まで完了すると以下のようにIntentsとSlot Typesが並びます。
f:id:y-matsushita:20171128150846j:plain:w300

ビルド

Intentの登録が完了したらモデルをセーブして「Build Model」を押下します。
f:id:y-matsushita:20171128145837p:plain:w200
問題なく完了すれば以下のような表示がされます。
f:id:y-matsushita:20171128145928p:plain:w600

ビルドで失敗してしまった場合は以下が原因でエラーが出ていることが多かったので見直してみてください。

エラー内容確認する箇所
Error building interaction model
A sample utterance for a slot in the interaction model is invalid
設定した{Answer}の前後に半角スペースがあるかどうか
Error building interaction model Bad request {Answer}にAMAZON.NUMBERのSlotTypeを追加しているかどうか

ビルドが完了したら画面右上の「Configuration」から設定画面へ戻ります。
f:id:y-matsushita:20171128151132p:plain:w100


AWS Lambda

ここから先はAWS Lambdaで編集します。
AWS LambdaでAlexaから受け取ったIntentをもとにユーザに返すべき発言をAlexaを通して返します。

Alexa用のLambda関数を作成

Lambdaの関数の作成から「alexa-skill-kit-sdk-factskill」のテンプレートを選びます。
f:id:y-matsushita:20171128151842p:plain:w700

適当な関数名とロールを入力して作成します。
f:id:y-matsushita:20171128151944p:plain:w700

トリガーにAlexa Skils Kitを追加します。
f:id:y-matsushita:20171128152022p:plain:w700
追加すると以下のようになります。
f:id:y-matsushita:20171128152220j:plain:w400

コードを書き換え

デフォルトのコードを以下に書き換え保存します。

"use strict";
const Alexa = require('alexa-sdk');
// ステートの定義
const states = {
  QUIZ: '_QUIZMODE',
  START: "_STARTMODE"
};

// クイズ内容の定義
const questions = [
  { 'q' : 'クラッシュフィーバーの略称は? 1.クラフィ、2.クラッシュ',  'a' : '1' },
  { 'q' : 'ワンダープラネットの略称は? 1.ダープラ、2.ワンプラ、3.ワンダー',  'a' : '2'},
  { 'q' : 'クラッシュフィーバーの通貨単位は次のうちどれ? 1.ゼニー、2.ゴールド、3.ビット',  'a' : '3'},
  { 'q' : '存在しないタイプはどれ? 1.魔法、2.体力、3.バランス',  'a' : '1'},
  { 'q' : '果実を使うと上がるのはどれ? 1.レベル、2.バグ、3.ステータス',  'a' : '3'},
  { 'q' : 'クエストに参加するのに必要なものは次のうちどれ? 1.スタミナ、2.ビット、3.エナジー',  'a' : '3'},
];
var languageString = {
    "ja-JP": {
        "translation": {
            "WELCOME_MESSAGE": "クラフィクイズへようこそ。 ",
            "HELP_MESSAGE": "正解だと思う番号を回答してください。",
            "START_MESSAGE": "ゲームを始める場合は「ゲームスタート」と言ってください。 ",
            "ANSWER_CORRECT_MESSAGE": "正解。 ",
            "ANSWER_WRONG_MESSAGE": "残念、不正解。 ",
            "TELL_QUESTION_MESSAGE": "第%s問。 ",
            "GAME_OVER_MESSAGE": "全ての問題が終わりました。あなたの点数は%s点でした。遊んでくれてありがとう。 ",
            "UNHANDLED_MESSAGE": "すみません、よく聞きとれませんでした。"
        }
    }
};

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.resources = languageString;
  alexa.registerHandlers(handlers, startStateHandlers, quizHandlers);
  alexa.execute();
};

var handlers = {
  'LaunchRequest': function () {
    this.handler.state = states.START;
    this.emitWithState("StartGame");
  },
  "AMAZON.StartOverIntent": function() {
      this.handler.state = states.START;
      this.emitWithState("StartGame");
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', this.t("HELP_MESSAGE") + this.t("START_MESSAGE"));
  },
  'Unhandled': function () {
    var speechOutput = this.t("UNHANDLED_MESSAGE") + this.t("START_MESSAGE");
    this.emit(":ask", speechOutput, speechOutput);
  }
};


// ゲーム開始ステート
var startStateHandlers = Alexa.CreateStateHandler(states.START, {
    "StartGame": function () {
      this.handler.state = states.QUIZ; // クイズ回答ステートをセット
      this.attributes['advance'] = 1;   // 進行状況をセッションアトリビュートにセット
      this.attributes['correct'] = 0;   // 正解数を初期化
      var message = this.t("WELCOME_MESSAGE") + this.t("HELP_MESSAGE") + this.t("TELL_QUESTION_MESSAGE", "1") + questions[0].q;
      var reprompt = this.t("TELL_QUESTION_MESSAGE") + questions[0].q;
      this.emit(':ask', message, reprompt); // 相手の回答を待つ
      console.log(message);
    }
});


// クイズ回答ステート
var quizHandlers = Alexa.CreateStateHandler(states.QUIZ, {
  'QuizIntent': function() {

    // スロットから回答を参照
    var usersAnswer = this.event.request.intent.slots.Answer.value;
    if(!usersAnswer){
      this.emitWithState("Unhandled");
    }

    var resultMessage;
    if(questions[this.attributes['advance']-1].a == usersAnswer){
        resultMessage = this.t("ANSWER_CORRECT_MESSAGE")     //正解
        this.attributes['correct'] ++;
    }else{
        resultMessage = this.t("ANSWER_WRONG_MESSAGE")     //不正解
    }

    if(this.attributes['advance'] < questions.length){
        // まだ問題が残っている場合
        var nextMessage = this.t("TELL_QUESTION_MESSAGE", this.attributes['advance']+1) + questions[this.attributes['advance']].q;
        this.attributes['advance'] ++;
        this.emit(':ask', resultMessage+nextMessage, nextMessage);
    }else{
        // 全ての問題が終了した場合
        var endMessage = this.t("GAME_OVER_MESSAGE", this.attributes['correct'])
        // スキルを初期状態に戻すためステートをリセット
        this.handler.state = '';
        this.attributes['STATE'] = undefined;
        this.attributes['advance'] = 0;
        this.emit(':tell', resultMessage + endMessage, endMessage);
    }
  },
  "AMAZON.RepeatIntent": function() {
    var nextMessage = this.t("TELL_QUESTION_MESSAGE", this.attributes['advance']) + questions[this.attributes['advance']-1].q;
    this.emit(':ask', nextMessage, nextMessage);
  },
  'Unhandled': function() {
    var reprompt = this.t("UNHANDLED_MESSAGE") + this.t("HELP_MESSAGE");
    this.emit(':ask', reprompt, reprompt);
  }
});

このスキルでは主にstartStateHandlersとquizHandlersの2つのステートで状態を管理しています。
まずスキル起動直後はAlexaからAMAZON.StartOverIntentが来るのを待ちます。ユーザが「ゲームスタート」と言うとAMAZON.StartOverIntentが発生するのでstartStateHandlersで初期化を行い、ステートをquizHandlersへ移します。AMAZON.StartOverIntent以外(ユーザが「ゲームスタート」と言っていない)の場合は、誘導用のメッセージを返します。

quizHandlersではユーザの発言した回答の番号がthis.event.request.intent.slots.Answer.valueで取得できるので、questionsのaと比較し正否の判定を行います。またAMAZON.RepeatIntentが呼ばれている場合は再度問題文を読み上げます。もし何にも引っかからない場合はUnhandledが呼ばれ、ヘルプ用のメッセージを表示してユーザを誘導します。
正否判定の後には問題数と進行状況を比較し、クイズを続けるか、終えるかを判断しています。クイズが続く場合はthis.emitを:askにして再度Alexaから問いかけを行い、終了する場合はthis.emitを:tellにしてクイズの正解数を読み上げて対話を終了します。

ここまででコードの保存が完了したらLambdaの画面右上のARNをコピーします。
以上でLambda側の操作は完了です。
f:id:y-matsushita:20171128152558p:plain:w700

ARNの設定

Alexaの開発者コンソールに戻り、先ほどコピーしたARNを入力します。
他にも設定項目がいくつかありますが、今回はデフォルトのままでOKです。
f:id:y-matsushita:20171128173451p:plain:w700

入力後「次へ」を押して問題なければ作成したスキルでテストが可能になります。

シミュレータでのテスト

画面をスクロールするとサービスシミュレータという項目があります。
色々入力してどういう結果が返ってくるかWEB上でテストしてみましょう。
問題なければ「ゲームスタート」と送るとクイズが始まるはずです。
f:id:y-matsushita:20171128173712p:plain:w700
何かおかしな挙動がみつかれば対話モデルやLambdaのプログラムを修正していく流れになります。
サービスリクエストに使われたJSONはそのままLambdaに渡すとLambda側のみでテストすることも可能です。

実機でのテスト

シミュレータでのテストで問題が無ければ、いよいよ実機でテストを行います。
Alexaの開発者アカウントと実機テストをする端末のアカウントが同じであれば、すでに実機テストが可能です。
また「Skills Beta Testing」を使ってテストユーザとして招待する形で実機テストも可能です。
f:id:y-matsushita:20171201141213p:plain:w200
「Skills Beta Testing」を有効化するには、「公開情報」と「プライバシーとコンプライアンス」を記入していきます。
この作業をしても申請をしなければ一般公開されないのでご安心ください。

公開情報を入力

公開情報のタブを開き、必要事項を記入していきます。
全て入力する必要がありますが、申請しないのであれば適当に入力してもテストは可能です。 f:id:y-matsushita:20171201152450p:plain:w700
今回は以下のように設定しました。
・カテゴリー:Games, Trivia & Accessories
・サブカテゴリー:Games
・テストの手順:任意の文書
・国と地域:国と地域を選択する→Japan
・スキルの簡単な説明:任意の文書
・スキルの詳細な説明:任意の文書
・サンプルフレーズ:任意の文書(ウェイクワードなどを記載)
・キーワード:任意のキーワード (省略可)
・アイコン:108x108と512x512の画像を設定

プライバシーとコンプライアンスを入力

プライバシーとコンプライアンスのタブを開き、該当する項目にチェックを入れます。
f:id:y-matsushita:20171201153810p:plain:w700

「公開情報」と「プライバシーとコンプライアンス」を入力して問題なければ、
以下のボタンが押せるようになり、実機でのベータテストが可能になります。
f:id:y-matsushita:20171201154801p:plain:w200

テストユーザを登録

スキルを動かしたいユーザのAlexaアカウントのメールアドレスを入力します。
f:id:y-matsushita:20171201160501j:plain:w700

登録したメールアドレスの受信ボックスを確認すると以下のような文面のメールが確認できます。
f:id:y-matsushita:20171201160928j:plain:w500
リンクが2つあるのが確認できますが、下の「JP customers」のリンクをクリックしてください。
Alexaのスキル管理画面は英語版の「.com」と日本語版の「.co.jp」があります。
上のリンクをクリックすると英語版として認識されるため日本語のAlexaスキルが実行できない恐れがあります。

リンク先ではスキルの設定ができるので「有効にする」をクリックしましょう。
f:id:y-matsushita:20171201161639p:plain:w300

有効化すると遂にスキル一覧に作成したスキルが出てきます!
f:id:y-matsushita:20171201162043p:plain:w700

Alexaに呼びかけてみよう!

以上の手順を終えて呼びかけると、他の公開されているスキルと同様に反応してくれるはずです。 *2
手順が長くなりましたが、なんとかAlexaに独自のスキルを追加することができました。
実機のAlexaに話しかけて反応を楽しみましょう!
f:id:y-matsushita:20171201124024j:plain:w700

参考

*1:2017/11/27時点でβ版のため、今後のアップデートで設定内容が大きく変わる恐れがあります。

*2:Alexaに「すみません、なんだかうまくいかないみたいです。」と言われてしまう場合は、ウェイクワードを変えてみましょう。
ウェイクワードによっては起動しづらいことがあるようです。今回作成したスキルもウェイクワードを「クラフィクイズ」にしていた際、認識が上手くされないことがありました。