Alexaにフロントエンドマークアップ言語がやってきた – Alexa Presentation Language (APL)ことはじめ

Alexaスキルを使っていると、たまに「ん?このディスプレイ表示どうやってるんだ?」となるものや、「デフォルトの天気予報みたいな表示やりたいんだけどなぁ」ということはありませんでしたか? いままでは決められたフォーマット […]

広告ここから
広告ここまで

目次

    Alexaスキルを使っていると、たまに「ん?このディスプレイ表示どうやってるんだ?」となるものや、「デフォルトの天気予報みたいな表示やりたいんだけどなぁ」ということはありませんでしたか?

    いままでは決められたフォーマットでのみディスプレイ表示を定義することができましたが、今日からはついに自由にフロントエンドをマークアップすることができるようになります。

    Now Available: Alexa Presentation Language (Public Beta) for Multimodal Experiences

    「Alexaの見た目を定義する言語」ということで「Alexa Presentational Language(略称APL)」とよばれているこの機能を、さっそく触ってみましょう。

    作ったSkillにAPLを導入する

    APLをスキルに組み込むには、スキルの設定を更新する必要があります。

    スキル開発画面の[インターフェース]から、[APL]をオンにしましょう。

    ASK CLIで管理されている場合は、skill.jsonに以下を追加します。

    "apis": {
          "custom": {
            "endpoint": {
              "uri": "arn:aws:lambda:us-east-1:999~"
            },
            "interfaces": [
              {  
                  "type": "ALEXA_PRESENTATION_APL"
              } 
            ],

    ALEXA_PRESENTATION_APLを含めた状態でask deployまたはask deploy -t skillすることでAPL Readyとなります。

    APLでフロントエンドを構築する

    ではこのAPL、どこで実装すればよいでしょうか?プレビューしながら実装するためのコンソールがスキル開発画面に用意されています。

    [画面表示]をクリックすることで、以下のようにベースとなるテンプレートを選択できます。

    よく見ると、サンプルはこれまであったテンプレートが表示されている様子ですね。

    サンプルをクリックすると、以下のようにプレビュー付きの開発画面が表示されます。

    デフォルトではブロック単位で編集するビジュアルモードが表示されます。真ん中にある青いトグルをクリックすると、APLの実態である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": {
            "description": "********* Full-screen background image **********",
            "parameters": [
                "payload"
            ],
            "items": [
                {
                    "when": "${viewport.shape == 'round'}",
                    "type": "Container",
                    "direction": "column",
                    "items": [
                        {
                            "type": "Image",
                            "source": "${payload.bodyTemplate1Data.backgroundImage.sources[0].url}",
                            "position": "absolute",
                            "width": "100vw",
                            "height": "100vh",
                            "scale": "best-fill"
                        },
                        {
                            "type": "AlexaHeader",
                            "headerTitle": "${payload.bodyTemplate1Data.title}",
                            "headerAttributionImage": "${payload.bodyTemplate1Data.logoUrl}"
                        },
                        {
                            "type": "Container",
                            "grow": 1,
                            "paddingLeft": "@marginLeft",
                            "paddingRight": "@marginRight",
                            "paddingBottom": "@marginBottom",
                            "items": [
                                {
                                    "type": "Text",
                                    "text": "${payload.bodyTemplate1Data.textContent.primaryText.text}",
                                    "size": "@textSizeBody",
                                    "spacing": "@spacingSmall",
                                    "style": "textStyleBody"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "Container",
                    "height": "100vh",
                    "items": [
                        {
                            "type": "Image",
                            "source": "${payload.bodyTemplate1Data.backgroundImage.sources[0].url}",
                            "position": "absolute",
                            "width": "100vw",
                            "height": "100vh",
                            "scale": "best-fill"
                        },
                        {
                            "type": "AlexaHeader",
                            "headerTitle": "${payload.bodyTemplate1Data.title}",
                            "headerAttributionImage": "${payload.bodyTemplate1Data.logoUrl}"
                        },
                        {
                            "type": "Container",
                            "paddingLeft": "@marginLeft",
                            "paddingRight": "@marginRight",
                            "paddingBottom": "@marginBottom",
                            "items": [
                                {
                                    "type": "Text",
                                    "text": "${payload.bodyTemplate1Data.textContent.primaryText.text}",
                                    "size": "@textSizeBody",
                                    "spacing": "@spacingSmall",
                                    "style": "textStyleBody"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    }

    たのしいですね。

    APLを触ってみる

    あえてJSONをダイレクトにさわるマゾい道を選んでみます。

    themedarklightに変更すると、表示がこのように変わりました。

    ここの文字色は以下のように定義されています。

        "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"
                }
            },

    themedarkの場合は#f0f1ef、そうでない場合は#151920があたるということですね。

    文字が読めなくなっているので、色を変えてみましょう。

        "resources": [
            {
                "description": "Stock color for the light theme",
                "colors": {
                    "colorTextPrimary": "lightblue"
                }
            },
            {
                "description": "Stock color for the dark theme",
                "when": "${viewport.theme == 'dark'}",
                "colors": {
                    "colorTextPrimary": "#f0f1ef"
                }
            },

    ちなみに色コードにはRGBやRGBAも指定可能です。

    ASK SDKのコードで読み込ませる

    作成したViewはJSON形式でASK SDKから読み込ませます。エディタ右上の書き出しボタンでJSONを出力しましょう。

    またASK SDK(Node.js)は2.2以上にする必要があります。

    $ npm i -S ask-sdk-core@latest
    or 
    $ npm i - S ask-sdk@latest

    そして読み込みのコードは以下のようになります。

          .addDirective({
              type: 'Alexa.Presentation.APL.RenderDocument',
              version: '1.0',
              datasources: {
                        // テンプレートに流し込むデータ
              },
              document: require('PATH_TO_TEMPLATE.JSON')
          })

    先ほどのサンプルですと、こうなります。

    handlerInput.responseBuilder
            .speak(messages)
            .addDirective({
              type: 'Alexa.Presentation.APL.RenderDocument',
              version: '1.0',
              datasources: {
                'bodyTemplate1Data': {
                  'type': 'object',
                  'objectId': 'bt1Sample',
                  'backgroundImage': {
                    'contentDescription': null,
                    'smallSourceUrl': null,
                    'largeSourceUrl': null,
                    'sources': [
                      {
                        'url': 'https://d2o906d8ln7ui1.cloudfront.net/images/BT1_Background.png',
                        'size': 'small',
                        'widthPixels': 0,
                        'heightPixels': 0
                      },
                      {
                        'url': 'https://d2o906d8ln7ui1.cloudfront.net/images/BT1_Background.png',
                        'size': 'large',
                        'widthPixels': 0,
                        'heightPixels': 0
                      }
                    ]
                  },
                  'title': 'Did You Know?',
                  'textContent': {
                    'primaryText': {
                      'type': 'PlainText',
                      'text': 'But in reality, mice prefer grains, fruits, and manmade foods that are high in sugar, and tend to turn up their noses at very smelly foods, like cheese. In fact, a 2006 study found that mice actively avoid cheese and dairy in general.'
                    }
                  },
                  'logoUrl': 'https://d2o906d8ln7ui1.cloudfront.net/images/cheeseskillicon.png'
                }
              },
              document: require('../apmls/result.json')
            })

    datasourcesの値は、先ほどのエディタ画面から[JSONデータ]タブを開くことでみることができます。

    ここでJSONの値を入れ替えてプレビューすることもできますので、いろいろデバッグしてみましょう。

    APLを使うときの注意点

    Displayインターフェースと同じですが、Echo Dotsやスマートフォンアプリに対するレスポンスに含めることはNGです。リクエストの値を見てaddDirectiveをレスポンスに含めるかを確認するように実装しましょう。

    ask-utilsというライブラリを公開・メンテナンスしていますが、こちらを使えば以下のような形で分岐させることが可能です。

    const {
      supportsDisplay
    } = require('ask-utils')
    
    // 中略
    handle(handlerInput) {
      const response = handlerInput.responseBuilder
                            .speak('こんにちは元気?')
                            .reprompt('元気?')
      if (supportsDisplay(handlerInput)) {
        response.addDirective({
              type: 'Alexa.Presentation.APL.RenderDocument',
              version: '1.0',
              datasources: {
                        // テンプレートに流し込むデータ
              },
              document: require('PATH_TO_TEMPLATE.JSON')
          })
      }
      return response.getResponse()
    }

    まとめ

    今回は触りの部分を中心にAPLを紹介しました。デバイスサイズやモードによって動的にコンテンツを変更することなどもできますので、まとめ次第こちらも随時紹介していきます。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark