Help us understand the problem. What is going on with this article?

Alexa Presentation Language(APL)スキル 開発チュートリアル

More than 1 year has passed since last update.

Alexa Presentation Language(APL)スキル 開発チュートリアル

by zono_0
1 / 47

1. はじめに


参考文献

今回のAlexa Presentation Language(APL)スキル 開発チュートリアルは、以下のサイトを参考に、2018年11月時点の開発環境にアレンジし、まとめたものとなります。


このスライドを見てできること

  • Alexa Presentation Language(APL)スキルの作り方が理解できるようになる。

今回作成するAlexaスキル

  • スキル名:ハローワールド

※ディスプレイ対応のAlexaスキルが作成できます。

トークシナリオ
ユーザー:「アレクサ、ハローワールドを開いて」
アレクサ:「XXXXXXXX。」(APLによるディスプレイ表示)


前提条件


2. 事前準備・開発環境


事前準備


必要なソフトウェア

  • ブラウザ:Google Chrome(推奨)
  • エディタ:Visual Studio Code(推奨)

必要な開発環境

  • マイク&スピーカが利用できるパソコン
  • インターネット(Wi-Fi)

3. スキルとモデルの作成


ask new

  • ask new コマンドを利用し、hello-world スキルを新規作成する。
Ubuntuコマンドライン
#作業ディレクトリ(任意)に移動(ホームディレクトリ ~/を想定)
$ cd

#スキルの作成
$ ask new
? Please type in your new skill name: hello-world

#作業ディレクトリに移動(hello-worldを想定)
$ cd hello-world

  • GitHubからも同様のZIPファイルをダウンロードすることは可能ですが、ZIPファイルには、必要なモジュール ask-sdk-core が含まれておりません。ZIP解凍後、コマンドラインから./lambda/custom/フォルダ内(index.jsとpackage.jsonファイルがあるフォルダ内)に移動し、npm install コマンドを実行することで、必要なモジュールを取り込むことが可能です。
Ubuntuコマンドライン
$ cd ./lambda/custom

$ npm install

skill.jsonファイルの日本語スキル化

  • ./skill.json ファイルをエディタで開き、"en-US" の文字列を "ja-JP" に書き替えて、日本語スキル化する。
skill.json
修正前:"en-US": {
修正後:"ja-JP": {

en-US.jsonファイルのリネーム

  • ./models/en-US.jsonファイル名を「ja-JP.json」にリネームし、日本語スキル化する。

ja-JP.jsonファイルの日本語スキル化

  • ./models/ja-JP.jsonファイルを開き、"invocationName"(スキル呼び出し名)の設定値を以下のとおり日本語化する。
ja-JP.json
修正前:"invocationName": "greeter",
修正後:"invocationName": "ハローワールド",

ja-JP.jsonファイルの日本語スキル化

  • HelloWorldIntentインテントのサンプル発話の日本語化
  • ./models/ja-JP.jsonファイルを開き、HelloWorldIntentのサンプル発話を以下のとおり日本語化する。
ja-JP.json
        {
          "name": "HelloWorldIntent",
          "slots": [

          ],
          "samples": [
            "ハロー",
            "セイハロー",
            "セイハローワールド"
          ]
        }


4. Lambdaの作成


node_modulesの最新化

  1. ./lambda/customフォルダに移動する。
  2. 以下のnpmコマンドを実行し、node_modulesを更新する。
Ubuntuコマンドライン
#作業ディレクトリ(任意)に移動(~/hello-world/lambda/customを想定)
$ cd ./lambda/custom

#node_modulesの最新化(index.jsファイルがあるフォルダで実行すること)
$ npm i -S ask-sdk-core@latest
$ npm i -S ask-sdk-model@latest


index.jsの修正

  • ./lambda/custom/index.jsをエディタで開き、ソースプログラムを修正する。
  • APL対応のポイントは、supportsAplを利用しデバイスがAPIに対応しているか判定することです。その他は、通常のスキルとほぼ同様となります。
index.js
/* template by ぽいもんラボ zono_0 */
/* eslint-disable  func-names */
/* eslint-disable  no-console */
"use strict";


/**
 * モジュール参照
 * @author zono_0
 */ 
const Alexa = require('ask-sdk-core');


/**
 * ディスプレイサポート(APL対応)判定値
 * @author zono_0
 */ 
const supportsApl = (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['Alexa.Presentation.APL'];

  return hasDisplay;
};


/**
 * ローンチリクエストハンドラ
 * @author zono_0
 */
const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const speechText = 'ハローワールドスキルへようこそ。このスキルでは、英語のあいさつの練習をおこないます。ハローとおっしゃってください。';

    // ディスプレイ有り(APL対応)の場合
    if (supportsApl(handlerInput)) {
      // APL対応(documentに設定したテンプレートレイアウトを利用し、datasourcesの内容をディスプレイに表示します。)
      handlerInput.responseBuilder
        .addDirective({
          type : 'Alexa.Presentation.APL.RenderDocument',
          version: '1.0',
          document: require('./homepage.json'),
          datasources: require('./data.json')
        });
    }

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withShouldEndSession(false)
      .withSimpleCard('ハローワールド', speechText)
      .getResponse();
  },
};


/**
 * ハローワールドインテントハンドラ
 * @author zono_0
 */
const HelloWorldIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent';
  },
  handle(handlerInput) {
    const speechText = 'お上手ですね、こんにちは。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withShouldEndSession(true)
      .withSimpleCard('ハローワールド', speechText)
      .getResponse();
  },
};


/**
 * ヘルプインテントハンドラ
 * @author zono_0
 */
const HelpIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
  },
  handle(handlerInput) {
    const speechText = 'このスキルでは、英語のあいさつの練習をおこないます。ハローとおっしゃってください。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withShouldEndSession(false)
      .withSimpleCard('ハローワールド', speechText)
      .getResponse();
  },
};


/**
 * キャンセル&ストップインテントハンドラ
 * @author zono_0
 */
const CancelAndStopIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
  },
  handle(handlerInput) {
    const speechText = '終了を受け付けました。ご利用ありがとうございます。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withShouldEndSession(true)
      .withSimpleCard('ハローワールド', speechText)
      .getResponse();
  },
};


/**
 * セッションエンドリクエストハンドラ
 * @author zono_0
 */
const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);

    return handlerInput.responseBuilder.getResponse();
  },
};


/**
 * エラーハンドラ
 * @author zono_0
 */
const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    console.log(`Error handled: ${error.message}`);

    return handlerInput.responseBuilder
      .speak('ちょっと分かりませんでした。ハローとおっしゃってください。')
      .reprompt('このスキルでは、英語のあいさつの練習をおこないます。ハローとおっしゃってください。')
      .withShouldEndSession(false)
      .getResponse();
  },
};


/**
 * メインンロジック
 * @author zono_0
 */
const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder
  .addRequestHandlers(
    LaunchRequestHandler,
    HelloWorldIntentHandler,
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();


5. スキルのデプロイ


ask deploy

  • ask deploy コマンドを利用し、hello-world スキルをAmazon Developer PortalおよびAWS Lambdaにデプロイします。
  • デプロイ先のAWS Lambdaリージョンは、カレントディレクトリ配下の ~/.aws/configファイルにて指定できます。
Ubuntuコマンドライン
#作業ディレクトリ(任意)に移動(~/hello-worldを想定)
$ cd hello-world

#スキルのデプロイ(skill.jsonフィルと同じディレクトリで実行すること。)
$ ask deploy

Ubuntuコマンドライン
$ cat ~/.aws/config
[default]
region=ap-northeast-1
output=json


デプロイ結果の確認(AWS Lambda)

  1. 当該AWSにログインし、デプロイ先リージョンのLambda関数一覧を確認する。
  2. 新規関数「ask-custom-hello-world-default」が作成されていることを確認する。

lambda.png


デプロイ結果の確認(Amazon Developer Portal)

  1. 当該Amazon Developer Portalにログインし、スキル一覧を確認する。
  2. 新規スキル「hello-world」が作成されていることを確認する。

adp.png


6. Alexa Presentation Language(APL)の作成


Amazon Developer Portal

  • Amazon Developer Portalのスキル一覧から当該スキルの「編集」を、クリックする。

apl1.png


Amazon Developer Portal

  • 「画面表示」をクリックし、APLオーサリングツール(デザインツール)を開く。

apl2.png


Amazon Developer Portal

  • 「画面表示サンプル」(任意)を、クリックする。

apl3.png


Amazon Developer Portal

  1. 「コードを書き出し」を、クリックする。
  2. 「apl_template_export.json」ファイルをダウンロードし、保存する。
  • ※ファイルダウンロードが完了したら、この画面の内容を保存せずに離れても結構です。

apl4.png


APL JSONファイル

  • 「apl_template_export.json」ファイルをダウンロード後、index.jsファイルがあるフォルダにファイルを移動する。(フォルダパス:/hello-world/lambda/custom)

apl5.png


2つのJSONファイル

  1. 「apl_template_export.json」をコピーし、2つのファイルにする。
  2. 1つのファイル名を「homepage.json」に、もう1つのふぁいる名を「data.json」にリネームする。

apl6.png


homepage.json

homepage.json
{
    "type": "APL",
    "version": "1.0",
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.0.0"
        }
    ],
    "resources": [
        {
            "description": "Stock color for the light theme",
            "colors": {
                "colorTextPrimary": "#151920"
            }
        },
        {
            "description": "Stock color for the dark theme",
            "when": "${viewport.theme == 'dark'}",
            "colors": {
                "colorTextPrimary": "#f0f1ef"
            }
        },
        {
            "description": "Standard font sizes",
            "dimensions": {
                "textSizeBody": 48,
                "textSizePrimary": 27,
                "textSizeSecondary": 23,
                "textSizeSecondaryHint": 25
            }
        },
        {
            "description": "Common spacing values",
            "dimensions": {
                "spacingThin": 6,
                "spacingSmall": 12,
                "spacingMedium": 24,
                "spacingLarge": 48,
                "spacingExtraLarge": 72
            }
        },
        {
            "description": "Common margins and padding",
            "dimensions": {
                "marginTop": 40,
                "marginLeft": 60,
                "marginRight": 60,
                "marginBottom": 40
            }
        }
    ],
    "styles": {
        "textStyleBase": {
            "description": "Base font description; set color and core font family",
            "values": [
                {
                    "color": "@colorTextPrimary",
                    "fontFamily": "Amazon Ember"
                }
            ]
        },
        "textStyleBase0": {
            "description": "Thin version of basic font",
            "extend": "textStyleBase",
            "values": {
                "fontWeight": "100"
            }
        },
        "textStyleBase1": {
            "description": "Light version of basic font",
            "extend": "textStyleBase",
            "values": {
                "fontWeight": "300"
            }
        },
        "mixinBody": {
            "values": {
                "fontSize": "@textSizeBody"
            }
        },
        "mixinPrimary": {
            "values": {
                "fontSize": "@textSizePrimary"
            }
        },
        "mixinSecondary": {
            "values": {
                "fontSize": "@textSizeSecondary"
            }
        },
        "textStylePrimary": {
            "extend": [
                "textStyleBase1",
                "mixinPrimary"
            ]
        },
        "textStyleSecondary": {
            "extend": [
                "textStyleBase0",
                "mixinSecondary"
            ]
        },
        "textStyleBody": {
            "extend": [
                "textStyleBase1",
                "mixinBody"
            ]
        },
        "textStyleSecondaryHint": {
            "values": {
                "fontFamily": "Bookerly",
                "fontStyle": "italic",
                "fontSize": "@textSizeSecondaryHint",
                "color": "@colorTextPrimary"
            }
        }
    },
    "layouts": {},
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "when": "${viewport.shape == 'round'}",
                "type": "Container",
                "direction": "column",
                "items": [
                    {
                        "type": "Image",
                        "source": "${payload.bodyTemplate7Data.backgroundImage.sources[0].url}",
                        "scale": "best-fill",
                        "position": "absolute",
                        "width": "100vw",
                        "height": "100vh",
                        "scrim": true
                    },
                    {
                        "type": "AlexaHeader",
                        "headerTitle": "${payload.bodyTemplate7Data.title}",
                        "headerAttributionImage": "${payload.bodyTemplate7Data.logoUrl}"
                    },
                    {
                        "type": "Container",
                        "grow": 1,
                        "alignItems": "center",
                        "justifyContent": "center",
                        "items": [
                            {
                                "type": "Image",
                                "source": "${payload.bodyTemplate7Data.image.sources[0].url}",
                                "scale": "best-fill",
                                "width": "100vh",
                                "height": "70vw",
                                "align": "center"
                            }
                        ]
                    }
                ]
            },
            {
                "type": "Container",
                "items": [
                    {
                        "type": "Image",
                        "source": "${payload.bodyTemplate7Data.backgroundImage.sources[0].url}",
                        "scale": "best-fill",
                        "position": "absolute",
                        "width": "100vw",
                        "height": "100vh"
                    },
                    {
                        "type": "AlexaHeader",
                        "headerTitle": "${payload.bodyTemplate7Data.title}",
                        "headerAttributionImage": "${payload.bodyTemplate7Data.logoUrl}"
                    },
                    {
                        "type": "Container",
                        "direction": "row",
                        "paddingLeft": "5vw",
                        "paddingRight": "5vw",
                        "paddingBottom": "5vh",
                        "alignItems": "center",
                        "justifyContent": "center",
                        "items": [
                            {
                                "type": "Image",
                                "height": "75vh",
                                "width": "90vw",
                                "source": "${payload.bodyTemplate7Data.image.sources[0].url}",
                                "scale": "best-fill",
                                "align": "center"
                            }
                        ]
                    }
                ]
            }
        ]
    }
}


data.json

data.json
{
    "bodyTemplate7Data": {
        "type": "object",
        "objectId": "bt7Sample",
        "title": "Today's Daily Photo of Cheese",
        "backgroundImage": {
            "contentDescription": null,
            "smallSourceUrl": null,
            "largeSourceUrl": null,
            "sources": [
                {
                    "url": "https://d2o906d8ln7ui1.cloudfront.net/images/BT7_Background.png",
                    "size": "small",
                    "widthPixels": 0,
                    "heightPixels": 0
                },
                {
                    "url": "https://d2o906d8ln7ui1.cloudfront.net/images/BT7_Background.png",
                    "size": "large",
                    "widthPixels": 0,
                    "heightPixels": 0
                }
            ]
        },
        "image": {
            "contentDescription": null,
            "smallSourceUrl": null,
            "largeSourceUrl": null,
            "sources": [
                {
                    "url": "https://d2o906d8ln7ui1.cloudfront.net/images/MollyforBT7.png",
                    "size": "small",
                    "widthPixels": 0,
                    "heightPixels": 0
                },
                {
                    "url": "https://d2o906d8ln7ui1.cloudfront.net/images/MollyforBT7.png",
                    "size": "large",
                    "widthPixels": 0,
                    "heightPixels": 0
                }
            ]
        },
        "logoUrl": "https://d2o906d8ln7ui1.cloudfront.net/images/cheeseskillicon.png",
        "hintText": "Try, \"Alexa, search for blue cheese\""
    }
}


Lambdaの再デプロイ

  • ask deploy -t コマンドを利用し、hello-world スキルのLambdaプログラムのみをAWS Lambdaに再デプロイします。
Ubuntuコマンドライン
#作業ディレクトリ(任意)に移動(~/hello-worldを想定)
$ cd hello-world

#スキルのデプロイ(skill.jsonフィルと同じディレクトリで実行すること。)
$ ask deploy -t lambda


Amazon Developer Portal

  • 「インターフェース」を、クリックする。

apl8.png


Amazon Developer Portal

  1. Alexa Presentation Languageを、チェックする。
  2. 「インターフェースを保存」を、クリックする。

apl9.png

追記:2019/02/04
- ディスプレイタップ等に正しく対応するために、「Displayインターフェース」もあわせてチェックしておくことをおすすめします。「Displayインターフェース」をチェックすることで、ブラウザシミュレータでの「タップ(クリック)」テストが可能になるかと思います。


7. スキルのテスト


ブラウザシミュレータ

  1. 「テスト」タブを、クリックする。
  2. 「テスト有効」を、ONにする。
  3. 「スキルI/O」のチェックを、はずす。

apl11.png


ブラウザシミュレータ

  1. 「ハローワールドを開いて」と発話し、スキルを起動する。
  • ※ APLのサンプル画像が表示されれば、OKです。

apl12.png


8. 補足


APL定義を修正したい場合

  • 表示する画像データを変更したい場合は、「data.json」内容の修正にチャレンジしましょう。
  • 表示する画像のレイアウトを変更したい場合は、「homepage.json」内容の修正にチャレンジしましょう。
  • 直接JSONファイルを編集することも可能ですし、APLオーサリングツール(デザインツール)を利用して編集することもできます。

APLオーサリングツール(デザインツール)の使い方


APLの向こう側

  • 動的に「data.json」内容を変えたい!!という場合は、ファイル定義での利用をやめて、Lambdaプログラムのdatasources指定箇所を、直接JSON記述することで、APLの向こう側にたどりつけるかもしれません。
index.js(抜粋:動作検証はしてないよ。)
    // いろいろ試してみてね!!
    let TITLE = 'タイトルを変数で動的に変えてみてね。';

    // ディスプレイ有り(APL対応)の場合
    if (supportsApl(handlerInput)) {
      // APL対応(documentに設定したテンプレートレイアウトを利用し、datasourcesの内容をディスプレイに表示します。)
      handlerInput.responseBuilder
        .addDirective({
          type : 'Alexa.Presentation.APL.RenderDocument',
          version: '1.0',
          document: require('./homepage.json'),
          datasources: {
              bodyTemplate7Data: {
                  "title": `${TITLE}`,
                  "logoUrl": "https://d2o906d8ln7ui1.cloudfront.net/images/cheeseskillicon.png",
                  "url": "https://d2o906d8ln7ui1.cloudfront.net/images/BT7_Background.png"
              }
          }
        });
    }


APLのベータ世界線

  • 動的に「data.json」内容を変えたい!!が、Lambdaプログラムで動的操作したいわけじゃない!data.jsonファイルを切り替えて扱いたいだけなんだ!というような利用ケースもあるかもしれません。この場合は、datasources指定箇所でrequireする「ファイル名」を、変数として扱うことで、もう一つのベータ世界線までもたどりつことができるかもしれません。
index.js(抜粋:動作検証はしてないよ。)
    // いろいろ試してみてね!!
    let DATA_JSON = './data2.json';

    // ディスプレイ有り(APL対応)の場合
    if (supportsApl(handlerInput)) {
      // APL対応(documentに設定したテンプレートレイアウトを利用し、datasourcesの内容をディスプレイに表示します。)
      handlerInput.responseBuilder
        .addDirective({
          type : 'Alexa.Presentation.APL.RenderDocument',
          version: '1.0',
          document: require('./homepage.json'),
          datasources: require(`${DATA_JSON}`)
        });
    }


APLの仕組みを深く理解する


9. おわりに


おわりに

みなさん、Alexa Presentation Language(APL)スキル 開発チュートリアルはいかがでしたか?みなさんの環境でも、うまく実行できていれば幸いです。うまくいけば、1時間もあれば完成するかと思います。
スマートスピーカのディスプレイ対応によって、スマートスピーカ界隈がより盛り上がっていくことを願っております。

2018/11/24 TAKAHIRO NISHIZONO


end

zono_0
( ɵ̷̥̥᷄ .̠ ɵ̷̥̥᷅ ) バイク:Vストローム250 パソコン:Ryzen 2700x カメラ:PENTAX K-r 派 動画編集:DaVinci Resolve 15 Qiita:http://qiita.com/zono_0 川崎市近郊バイクツーリングマップ:http://bit.ly/2GGMc9J
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした