マルチモーダルなAlexaスキルの作り方

視覚要素を簡単に作成する

Table of Contents

コンテキストなしの起動リクエストのドキュメントがうまく機能するようになったので、今度は別の画面を作成しましょう。このセクションでは、再利用可能なレイアウトとAPLパッケージの作成方法を学習しながら、カウントダウン画面と誕生日画面の両方の視覚要素を完成させます。さらに、Videoコンポーネントを使用して、誕生日に特別なビデオが流れるようにします。

1.レイアウト

ここまでで、以下の画面が完成しました。

final no context screen image

今度は以下の2つの画面を作成します。

final countdown screen image
final birthday screen image

似ているところがおわかりでしょうか? カウントダウン画面は、現在のドキュメントで既に作成できています。 誕生日画面もほとんどできています。誕生日画面と現在の起動画面の違いは2つだけです。誕生日画面では画像の代わりにビデオが表示され、背景アセットが異なっています。レイアウトを使用して現在のドキュメントを最適化することから始めましょう。

レイアウトは、ほかのコンポーネントとレイアウトからなる複合コンポーネントです。既にレイアウトを使用していますが(レスポンシブ対応コンポーネントはAPLパッケージでAmazonが定義しているレイアウト「alexa-layouts」です)、ここではスキルの画面に表示されるパターンに基づいて独自のレイアウトを定義しましょう。

レイアウトもJSONで定義されます。レイアウトには、description、parameters、ほかのコンポーネントで構成されるitemsセクションが含まれます。parametersでは、パラメーター値がない場合に使用されるデフォルト値と特定の型(または「any」)を指定できます。ドキュメントからテキスト部分を抽出して、それを基にレイアウトを作成できます。さっそくやってみましょう。

A. APLオーサリングツールを開きます。

B. 繰り返しパターンのレイアウトをドキュメントのlayoutsセクションに追加します。再利用可能なテキストレイアウトは、以下のようになります。

"cakeTimeText": {
    "description": "start、middle、endからなる基本的なテキストレイアウトです。",
    "parameters":[
        {
            "name": "startText",
            "type": "string"
        },
        {
            "name": "middleText",
            "type": "string"
        },
        {
            "name": "endText",
            "type": "string"
        }
    ],
    "items": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${startText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${middleText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${endText}"
                }
            ]
        }
    ]
}

これにより既存のドキュメントを単純化でき、重複するJSONを削除できます。Echo Spotとそれ以外のデバイスのTextコンポーネントでは、1つの違いがあることに注意してください。これについては、ContainerではなくpaddingTopプロパティにある条件文で解決することで、JSONドキュメント全体を大幅に簡素化できます。

C. 冗長なコード(各セクションでレイアウトに移動された重複コードすべて)を削除し、新しい「cakeTimeText」レイアウトに置き換えます。このレイアウトは以下のように使用します。

{
    "type": "cakeTimeText",
    "startText":"${text.start}",
    "middleText":"${text.middle}",
    "endText":"${text.end}"
}

冗長なコードを削除し、独自のレイアウトへの参照に置き換えると、以下のようになります。

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {
        "bigText": {
            "values": [
                {
                    "fontSize": "72dp",
                    "color": "black",
                    "textAlign": "center"
                }
            ]
        }
    },
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {
        "cakeTimeText": {
              "description": "start、middle、endからなる基本的なテキストレイアウトです。",
            "parameters":[
                {
                    "name": "startText",
                    "type": "string"
                },
                {
                    "name": "middleText",
                    "type": "string"
                },
                {
                    "name": "endText",
                    "type": "string"
                }
            ],
            "items": [
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "Text",
                            "style": "bigText",
                            "paddingTop":"${@viewportProfile == @hubRoundSmall ? 75dp : 0dp}",
                            "text": "${startText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${middleText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${endText}"
                        }
                    ]
                }
            ]
        }
    },
    "mainTemplate": {
        "parameters": [
            "text",

            "assets"
        ],
        "items": [
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${assets.backgroundURL}"
                    },
                    {
                        "type": "cakeTimeText",
                        "startText":"${text.start}",
                        "middleText":"${text.middle}",
                        "endText":"${text.end}"
                    },
                    {
                        "type": "AlexaImage",
                        "alignSelf": "center",
                        "imageSource": "${assets.cake}",
                        "imageRoundedCorner": false,
                        "imageScale": "best-fill",
                        "imageHeight":"40vh",
                        "imageAspectRatio": "square",
                        "imageBlurredBackground": false
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile != @hubRoundSmall}"
            },
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${assets.backgroundURL}"
                    },
                    {
                        "type": "cakeTimeText",
                        "startText":"${text.start}",
                        "middleText":"${text.middle}",
                        "endText":"${text.end}"
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile == @hubRoundSmall}"
            }
        ]
    }
}

ちょっと待ってください。 簡単になるどころか、むしろ長くなってしまいました。 レイアウトとスタイルを独自のAPLパッケージに入れて、もっと簡素化しましょう。

2.独自のAPLパッケージをホストする

コンポーネントのレンダリングが完了し、起動画面も同じように表示されるようになったら、今度は独自のレイアウトをホストして複数のドキュメントで使用できるようにしましょう。レイアウト、スタイル、リソースは、すべてAPLパッケージでホストできます。実は、APLパッケージの形式は、mainTemplateがないこと以外はAPLドキュメントと同じです。これは、リソース、スタイル、または独自のカスタムレスポンシブ対応コンポーネントやUIパターンを複数のAPLドキュメント間で共有する方法として優れています。ドキュメントを作成して、Alexa開発者コミュニティで共有することもできます。

独自のスタイルとレイアウトの両方をホストしたいので、バックエンドでS3バケットを使用します。残念ながら、Alexa-hosted環境を使用しているので、与えられているS3プロビジョニングの権限は変更できません。Alexaデバイスとシミュレーターでは、Access-Control-Allow-Originヘッダーが設定され、*.amazon.comが許可されている必要があります。Cross-Origin Resource Sharingについては、技術資料を参照してください。また、リンクが公開されている必要もあります。これはAlexa-hostedでは対応できません。ただし、この演習では、このGitHubリンクを使用してJSON APLパッケージをホストします。注: Githubは、すべてのドメインでのCORSをサポートしています。

別のサービスを使用してアセットをホストする場合は、そのサービスも適切なヘッダーを送信する必要があります。

ここで作成するパッケージは、このコースのAPLドキュメントの再利用可能なプロパティセットです。これにはレイアウトとスタイルが含まれています。レイアウトのレンダリングにはalexa-layoutsを使用するので、importセクションも必要です。

パッケージは次のとおりです。

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {
        "bigText": {
            "values": [
                {
                    "fontSize": "72dp",
                    "color": "black",
                    "textAlign": "center"
                }
            ]
        }
    },
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {
        "cakeTimeText": {
            "description": "start、middle、endからなる基本的なテキストレイアウトです。",
            "parameters":[
                {
                    "name": "startText",
                    "type": "string"
                },
                {
                    "name": "middleText",
                    "type": "string"
                },
                {
                    "name": "endText",
                    "type": "string"
                }
            ],
            "items": [
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${startText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${middleText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${endText}"
                        }
                    ]
                }
            ]
        }
    }
}

メインドキュメントで、mainTemplateレイアウト以外をすべて削除し、パッケージの新しいimportを追加できます。オーサリングツールでテストする場合は、この公開リンクを使用できます。

A. このimportをドキュメントに追加します。

{
    "name": "my-caketime-apl-package",
    "version": "1.0",
    "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-caketime-apl-package.json"
}

このimportにより、自分のカスタムスタイル(bigText)とレイアウト(cakeTimeText)の値を参照できるようになります。レイアウトとスタイルを削除できたので、ドキュメントがかなり短くなり、読みやすくなりました。

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "my-caketime-apl-package",
            "version": "1.0",
            "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-caketime-apl-package.json"
        },
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {},
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {},
    "mainTemplate": {
        "parameters": [
            "text",

            "assets"
        ],
        "items": [
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${assets.backgroundURL}"
                    },
                    {
                        "type": "cakeTimeText",
                        "startText":"${text.start}",
                        "middleText":"${text.middle}",
                        "endText":"${text.end}"
                    },
                    {
                        "type": "AlexaImage",
                        "alignSelf": "center",
                        "imageSource": "${assets.cake}",
                        "imageRoundedCorner": false,
                        "imageScale": "best-fill",
                        "imageHeight":"40vh",
                        "imageAspectRatio": "square",
                        "imageBlurredBackground": false
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile != @hubRoundSmall}"
            },
            {
                "type": "Container",
                "paddingTop": "75dp",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${assets.backgroundURL}"
                    },
                    {
                        "type": "cakeTimeText",
                        "startText":"${text.start}",
                        "middleText":"${text.middle}",
                        "endText":"${text.end}"
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile == @hubRoundSmall}"
            }
        ]
    }
}

インポートするレイアウトの中で alexa-layoutsをインポートしているのに、なぜ元のドキュメントでもalexa-layoutsをインポートしているのでしょうか?一般に、直接使用している依存関係について、明示的に宣言することが推奨されています。これは例えば、my-caketime-apl-packageがalexa-layoutsを使用しなくなったら、ドキュメントは機能しなくなります。ドキュメントにはalexa-layoutsとの依存関係があるので、ドキュメント側にもalexa-layoutsをインポートしておくことが推奨されるのです。

B. 開発者ポータルで、Cake Timeスキルを開きます。

C. スキルのコードエディタタブで、現在のlaunchDocument.jsonをこの新しいドキュメントで上書き保存します。

では、ドキュメントを仕上げていきましょう。

3.特別な誕生日ビデオを追加する

birthdayDocumentでは、Textを削除し、Imageコンポーネントの代わりに全画面ビデオを使用します。このときに、同じレイアウトで配置されるようにします。この場合のVideoコンポーネントの構造はシンプルです。

A. コードエディタタブで、birthdayDocument.json という名前の新しいドキュメントを作成し、以前のドキュメントをコピーします。

B. birthdayDocument.jsonで、ContainerのImageコンポーネントを以下のVideoコンポーネントと置き換えます。

{
    "type": "Container",
    "paddingTop":"3vh",
    "alignItems": "center",
    "items": [{
        "type": "Video",
        "height": "85vh",
        "width":"90vw",
        "source": "${assets.video}",
        "autoplay": true
    }]
}

このContainerを追加して、APLドキュメントでコンポーネントを中央揃えにします。viewportの上部に背景が少し見えるようにパディングを設定します。この1つ目のContainerからTextオブジェクトも削除している点に注意してください。 ビデオを前面の中央に配置します。使用するビデオでは、誕生日ケーキのアニメーションでAlexaの歌が流れます。以下のようなビデオです。

このビデオを全画面表示で再生できるように、高さと幅をそれぞれviewportの85%と90%に設定しています。ビデオを表示するので、テキストは不要です。

C. 1つ目のContainerからCakeTimeTextコンポーネントを削除します ${@viewportProfile != @hubRoundSmall}の場合)。

D. これを、birthdayDocument.jsonという新しいファイルとしてスキルコードに保存します。

4.バックエンドを結合する

index.jsファイルに戻って、ほかのAPL画面と結合しましょう。

現在の起動ドキュメントとこれまで見てきた誕生日ドキュメントの違いは、コンテンツのみです。 HasBirthdayLaunchRequestHandlerを変更して、条件に合ったlaunchDocument.jsonファイルまたはbirthdayDocument.jsonファイルを状況に応じて使用するようにしましょう。次のような表示にしていきます。

final countdown screen image

A. コードの重複は避けたいので、ヘルパー関数を使用して、キーに基づいた背景画像を取得します。これは、代替ドキュメント用の新しい背景画像の取得にも使用されます。また、デバイスの画面サイズをアセットのサイズロジックに含めるためにも使用します。次のヘルパー関数を、index.jsのほかの関数またはオブジェクトの外の任意の場所に追加します。

function getBackgroundURL(handlerInput, fileNamePrefix) {
    const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
    const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/"+fileNamePrefix+"_1920x1080.png" : "Media/"+fileNamePrefix+"_1280x800.png";
    return util.getS3PreSignedUrl(backgroundKey);
}

こうすると、想定されるファイル名を1か所でまとめて管理できるので便利です。viewportプロファイル検出を追加する場合、またはホスティングをS3バケットから変更する場合は、この1か所で簡単に行えます。

B. ここで、新しい関数を使用するために、LaunchRequestHandler.handle()コードをリファクタリングする必要があります。新しいデータソースでは、backgroundURLキーに新しい値を持つようになります。

backgroundURL: getBackgroundURL(handlerInput, "lights")

以下の行は削除できます。

const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/lights_1920x1080.png" : "Media/lights_1280x800.png";

C. 使用する起動ドキュメントは同じなので、ドキュメントを表すJSONのimportは既にあります。LaunchRequestHandlerと同じように、HasBirthdayLaunchRequestHandlerのreturn文の直前にブロックを追加します。

// // APLディレクティブを応答に追加します
if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']) {
    // Renderディレクティブを作成します
}

D. データソースnumberDaysStringで使用する変数を定義します。これは「1 day」または「234 days」のような可変文字列で、次の式で表すことができます。

const numberDaysString = diffDays === 1 ? "1 day": diffDays + " days";

この変数を// APLディレクティブを応答に追加します comment.

E. // Renderディレクティブを作成しますの下に、ディレクティブを追加します。

handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: launchDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Your Birthday",
            middle: "is in",
            end: numberDaysString
        },
        assets: {
            cake: util.getS3PreSignedUrl('Media/alexaCake_960x960.png'),
            backgroundURL: getBackgroundURL(handlerInput, "lights")
        }
    }
});

numberDaysStringをデータソースで使用するようになりました。これは、ユーザーからの入力と、スキルを使用している年月日に基づいて変更されます。さらに、ヘルパー関数を使用して、適切な署名付きURLを取得するためにlights用のURLを作成しています。

F. テストしてみましょう。 この画面にたどり着くまでに、誕生日の年、日、月を入力するフローをすべて通過する必要があります。

5.誕生日ビデオを結合する

A. ここまでの動作を確認できたら、誕生日がきた場合の挙動を作成しましょう。新しいドキュメントのbirthdayDocument.jsonを使用するので、これをbirthdayDocumentとして最初にインポートすることから始めましょう。

const birthdayDocument = require('./documents/birthdayDocument.json');

B. 新しいコードへの条件付きロジックを追加して、その日が誕生日かどうかによってAPLドキュメントを切り替える必要があります。HasBirthdayLaunchRequestHandler// Renderディレクティブを作成します というコメントの下に、以下を追加します。

if (currentDate.getTime() !== nextBirthday) {
    //TODO ここに前のディレクティブを移動します。
} else {
    //TODO ここに誕生日固有のディレクティブを入力します。
}

C. 前のセクションで作成したhandlerInput.responseBuilder.addDirectiveReplace({…​}) を切り取り、//TODO ここに前のディレクティブを移動します。というコメントと置き換えます。

D. else文の中に、上でインポートしたbirthdayDocument を使用する新しいディレクティブを追加できます。"confetti"画像を使用します。このディレクティブ全体をelse文の中に追加します。

// Renderディレクティブを作成します
handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: birthdayDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Happy Birthday!",
            middle: "From,",
            end: "Alexa <3"
        },
        assets: {
            video: "https://public-pics-muoio.s3.amazonaws.com/video/Amazon_Cake.mp4",
            backgroundURL: getBackgroundURL(handlerInput, "confetti")
        }
    }
});

この新しいディレクティブでは、Textオブジェクト用のデータが異なり、画像がビデオに置き換えられ、背景にconfettiアセットが使用されます。円形の小型デバイス用のバリアントで使用されるので、start、middle、endの各テキストの入力は必要です。

E. では、スキルをテストしましょう。S3の自分のユーザーデータを削除して、今日が誕生日ということにしてください。 もし今日が本当に誕生日なら、 お誕生日おめでとうございます!

うまくいったら、最後のモジュールに進んでコマンドについて学んでいきましょう。

Githubのコード(完全版)