LiBz Tech Blog

LiBの開発者ブログ

【コードつき】Alexa Echo Spot(画面付きAlexa)スキル開発

f:id:shimodach:20190117143915j:plain

はじめに

下田です。 Alexa Echo Spotを手に入れたものの時計としてしか使っていないので、ブログネタ用にスキル開発することにしました。

今回作るのは画面付きAlexa用スキルです。公式のガイドと公式サンプルコードのskill-demo-display-directiveを参考に作っていきます。

開発で利用するAlexa Skills Kit SDK for Node.jsのバージョンは2です。

作るもの

こういうものを作ります。

  • 「アレクサ、ハローワールド」に対して「Hello World! 貴方の名前を教えてください」を返す
  • 「私の名前はリブです」に対して「Hello リブ!」と返し、画面に「あなたの名前はリブです」と表示する。

Alexa開発者アカウント、AmazonDeveloperアカウント作成

色々詰まりポイントがあるので注意しながら作る必要があります。 自分が作成するときは失敗しないAlexa開発者アカウントの作り方の記事を参考にさせていただきました。

【Alexaコンソール】スキル設定

スキルを新規作成し、スキルビルダーのチェックリストに沿って進めていきます。

  • 呼び出し名
    • ハローワールド
  • インテント、サンプル、スロット
    • インテント名:HelloWorldIntent
    • サンプル発話:私の名前は {MyName} です
  • スロット
    • 名前:MyName
    • スロットのタイプ:Amazon.FirstName
  • インタフェース
    • 画面付きAlexaに対応できるように、DisplayインターフェースをONにします
  • 一旦モデルをビルド

エンドポイントの設定の前にLambda関数を作ります。

【Lambdaコンソール】Lambda設定

関数作成

新規作成ボタンから関数を作成します。

  • 名前:MyHelloWorld
  • ランタイム:Node.js 8.10
  • ロール:カスタムロールを適当に作って設定します

コードアップロード

コードをzipでアップロードします。 まずはskill-demo-display-directiveリポジトリを参考に、ローカルでindex.jsとpackage.jsonを作成します。

index.js(BodyTemplateHandler以外はほぼskill-demo-display-directiveのとおりです)

const Alexa = require('ask-sdk-core');

/* CONSTANTS */
const skillBuilder = Alexa.SkillBuilders.custom();

const welcomeMessage = 'Hello World! 貴方の名前を教えてください。';
const exitSkillMessage = 'Bye';
const repromptOutput = '貴方の名前を教えてください。';
const helpMessage = 'これはハローワールドスキルです。名前を教えると名前を画面に表示します。';

/* INTENT HANDLERS */
const BodyTemplateHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    console.log(request.type === 'IntentRequest' && request.intent.name === 'HelloWorldIntent');
    return (
      request.type === 'LaunchRequest' || (request.type === 'IntentRequest' && request.intent.name === 'HelloWorldIntent')
    );
  },
  handle(handlerInput) {
    const responseBuilder = handlerInput.responseBuilder;
    const request = handlerInput.requestEnvelope.request;
    var shouldEndSession = false;
    var speechOutput = '';

    if (request.type === 'LaunchRequest') {
      speechOutput = welcomeMessage + speechOutput;
    } else {
      let userName = request.intent.slots.MyName.value;
      speechOutput = 'hello ' + userName + '!';
            const primaryText = new Alexa.RichTextContentHelper()
        .withPrimaryText('あなたの名前は' + userName + 'です。')
        .getTextContent();
        shouldEndSession = true;

      if (supportsDisplay(handlerInput)) {
        responseBuilder.addRenderTemplateDirective({
          type: "BodyTemplate1",
          token: "string",
          backButton: 'visible',
          textContent: primaryText
        });
      }
    }

    return responseBuilder.speak(speechOutput)
      .reprompt(repromptOutput)
      .withShouldEndSession(shouldEndSession)
      .getResponse();
  },
};

const HelpHandler = {
  canHandle(handlerInput) {
    console.log('Inside HelpHandler');

    const request = handlerInput.requestEnvelope.request;
    return request.type === 'IntentRequest' &&
           request.intent.name === 'AMAZON.HelpHandler';
  },
  handle(handlerInput) {
    return handlerInput.responseBuilder
      .speak(helpMessage)
      .reprompt(helpMessage)
      .getResponse();
  },
};

const ExitHandler = {
  canHandle(handlerInput) {
    console.log('Inside ExitHandler');
    const request = handlerInput.requestEnvelope.request;

    return request.type === 'IntentRequest' && (
      request.intent.name === 'AMAZON.StopIntent' ||
      request.intent.name === 'AMAZON.PauseIntent' ||
      request.intent.name === 'AMAZON.CancelIntent'
    );
  },
  handle(handlerInput) {
    return handlerInput.responseBuilder
      .speak(exitSkillMessage)
      .getResponse();
  },
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`Session ended with reason: ${JSON.stringify(handlerInput.requestEnvelope)}`);
    return handlerInput.responseBuilder.getResponse();
  },
};

const ErrorHandler = {
  canHandle() {
    console.log('Inside ErrorHandler');
    return true;
  },
  handle(handlerInput, error) {
    console.log('Inside ErrorHandler - handle');
    console.log(`Error handled: ${JSON.stringify(error)}`);
    console.log(`Handler Input: ${JSON.stringify(handlerInput)}`);

    return handlerInput.responseBuilder
      .speak(`Something went wrong. ${helpMessage}`)
      .reprompt(helpMessage)
      .getResponse();
  },
};


/* HELPER FUNCTIONS */

// returns true if the skill is running on a device with a display (show|spot)
function supportsDisplay(handlerInput) {
  const hasDisplay =
    handlerInput.requestEnvelope.context &&
    handlerInput.requestEnvelope.context.System &&
    handlerInput.requestEnvelope.context.System.device &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display;
  return hasDisplay;
}

/* LAMBDA SETUP */
exports.handler = skillBuilder
  .addRequestHandlers(
    BodyTemplateHandler,
    HelpHandler,
    ExitHandler,
    SessionEndedRequestHandler,
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();

package.json

{
  "name": "template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts":  {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "Apache-2.0",
  "dependencies": {
    "ask-sdk-core": "^2.0.0",
    "ask-sdk-model": "^1.0.0"
  }
}

npm installをしたあと、コード群をzipにします。 Lambdaコンソールのコード エントリ タイプの.zipファイルをアップロードでアップロードした後保存すればアップロード完了です。

【Alexaコンソール&Lambdaコンソール】AlexaとLambdaの連携

Alexaからの入力にLambdaが応答できるように設定します。

  • Alexaのエンドポイント設定
    • Alexaのエンドポイントに、LambdaのARNを設定
  • Lambdaのトリガー設定
    • Designer欄のトリガーの設定からAlexa Skills Kitを選択
    • Alexaのエンドポイント設定画面にあるスキルIDを入力し保存

【Lambdaコンソール】テスト

ブログに貼ったコードそのままで動くはずですが、コードにミスがあった場合のテスト方法について書きます。

コードのテストはLambdaのテスト機能を使います。今回は下記2つの入力についてテストします。

  • 「アレクサ、ハローワールド」
  • 「私の名前はリブです」

テストに必要なパラメータを取得するために、一度Alexaコンソールに戻って、テストタブに移動します。 テキストボックスに「アレクサ、ハローワールド」を入力、Enterを押します。

f:id:shimodach:20190109140459p:plain

スキルI/OのJSON入力を丸ごとコピーします。

Lambdaコンソールに戻ります。 「テストイベントの設定」から新規テストイベントを作成、適当な名前をつけてさっきコピーしたJSONをペーストします。 (テンプレートは使わないのでなんでも良いです)

f:id:shimodach:20190109141214p:plain

「作成」ボタンで保存します。これで「アレクサ、ハローワールド」を入力したときのテストができるようになります。

「テスト」ボタンでテストを開始して、コードに問題があればその情報が出てくるので、成功するまでコードを直していきます。

f:id:shimodach:20190109143558p:plain

「アレクサ、ハローワールド」のテストが完了したら、同様の手順で「私の名前はリブです」の入力でも成功するまでテストします。

【Alexaコンソール】シミュレータでテスト

コードのテストが終わったら、シミュレータで動くか確認します。

再びAlexaコンソールのテストタブに移動し、テキストボックスを使って対話します。

f:id:shimodach:20190109144606p:plain

f:id:shimodach:20190109144618p:plain

無事期待通りの動作になりました🎉

実機テスト

認定タブに行くと色々警告が出てきます。

f:id:shimodach:20190109145714p:plain

案内が不親切ですが、大体公開タブの設定を埋めていけば解決します。 説明文入れたりアイコンを設定します。

警告をクリアしたら、公開タブから公開範囲画面に移動し、ベータテスト欄からメールアドレスでベータテスト招待します。

メールからこのリンクを踏んでスキルを有効化すれば、実機テストできます。

f:id:shimodach:20190109153400p:plain

実際に動かしてみましたが「あなたの名前はリブです」と表示された後一瞬で画面がホームに戻るので、要改善です…

おわりに

公式ガイドで紹介されているコードのSDKバージョンがv1だったりv2だったりしてだいぶ混乱しましたが、なんとか動かせるところまでいけました。

本当はスキル開発者向けキャンペーンでもらえるAlexaイラストTシャツを狙ってたのですが、ちんたら作っていたらキャンペーン期間が終わっていました。次こそ参加したいです。