#AAJUG 関東支部 APLハンズオン コード解説 #alexa #alexadevs

せーのでございます。今日は、2019年3月1日に目黒Amazonオフィスにて行われた「Alexaスキル開発ハンズオン 〜APL対応スキルのつくりかた〜」 の講師をやってきましたので、当日時間の関係でお話しきれなかった所や、参加者の方から「ここが知りたい」とリクエストを受けた所を中心にお話したいと思います。参加者の方は復習にお使いください。

資料などなど

まずはハンズオン資料です。当日来られなかった方はこちらよりダウンロードください。

http://bit.ly/aajug-apl-src

次にスライドです。

事前に進行管理をしたところ、全て終わるのは140分、と出ていたのでSTEP4に関しては時間内に入らないだろうな、と予想していたのですが、参加者の皆さんの意欲が大変高く、集中して課題に取り組んで戴いたことで時間内にSTEP4まで完走することができました。

というわけで運営としては「STEP4はその場ではやらない」つもりで準備をしていたので、できたけどよくわからない、という方もおられたかと思います。このブログではそこらへんを中心にフォローしていきたいと思います。

STEP1に関してはAPLを使う前段階の、Voice onlyのスキルとなっていますので割愛します。

STEP2

APL Document

STEP2ではLaunchRequest時、つまりAlexaに呼びかけて、最初のメッセージが発話された時に画面にAPLで組み込んだページが表示されればゴール、となります。

ハンズオン時にもお話しましたが、APL DocumentはCSSに似ています。ですのでCSSでWebページをデザインするような感じで構成していくと、イメージしたものを組み上げられます。

自分の組みたいものがAPLではどのコンポーネントに当たるのか、まずは公式ドキュメントでチェックしてみましょう。左側のメニューよりコンポーネントを探してみましょう。

背景画像

ハンズオンで使用されたTOPページのAPLドキュメントはこちらになります。

http://bit.ly/aajug-apl-doc-top

この記事ではこのうち背景画像について少し掘り下げます。

こちらがTOPページです。後ろにうっすらAAJUGのロゴが見えますね。ここに画像を配置するにはどうしたら良いでしょう。

オーサリングツール Alexa Presentation Language(APL)オーサリングツール | Alexa Presentation Language にアップロードしてレイアウト構成を確認すると、背景画像は大元のContainerのすぐ下にあります。

プロパティを全てクリアすると画像の位置関係はこのようになります。

ここから

  • source: https://s3-ap-northeast-1.amazonaws.com/aajug-apl-handson/aajug.png
  • height: 100vh
  • scale: best-fill
  • positon: absolute
  • opacity:0.4
  • width: 100vw

と設定すると最初の背景画像のようになります。
CSSを触ったことがある方にはなんとなくイメージできるかと思いますが、Containerに並べた時点ではrelativeとなっているので順番に表示されますが、absoluteに変えることで絶対指定となり、重ねることができるようになります。

STEP3

STEP3はAPL DocumentとAPL Dataをバインディングさせて、ボイスによる選択によって画面を変化させる、という課題でした。

アンケートでも

データバインディング部分がよくわかっていません…もう少し詳しくやりたいです。
プログラムを書いたことがなかったので、step3の答えが理解できませんでした。。

という声がありましたので、少し基礎に戻って説明したいと思います。

データバインディングの基礎

データバインディングは砕いて言うと「APL Documentの一部を変数化して、APL Dataにその変数の内容を指定すること」です。

例えばAPL Documentのテキストに当たる部分を「value」という変数にしたとします。APL Dataに「value: “Alexa”」と入れると、APL Documentのテキストには「Alexa」という文字が表示される、ということです。

やってみましょう。

まずオーサリングツール を開いて「最初から作成」をクリックします。

左下のレイアウト図から「mainTemplate」をクリックしてその右上にある+ボタンをクリックしますとコンポーネントの追加画面がポップアップします。

「Text」を選択し、「{type}を追加」ボタンをクリックします。レイアウトにTextが追加されます。

「text」の部分に「Alexa」と打ち込み、「fontSize」を「100dp」とします。画面に少し大きめに「Alexa」という文字が表示されたかと思います。

ではこのテキストを変数化します。「text」の部分に「${payload.value}」と打ち込みます。上に表示されていた「Alexa」の文字が消えます。

この変数に文字を指定します。右下のレイアウト図上にある「JSONデータ」をクリックします。

表示されたJSONエディタに

{ “value”: “Alexa” }

と打ち込みます。先程と同じように画面に「Alexa」という文字が表示されます。値を変えると別な文字が表示されます。

payload

さて、ここで一つ注意点です。先程APL Documentを変数化した際に

${payload.value}

と書きました。${XXXX}と書くのはお作法として覚えてもらうとして、”payload”というのはどこからきたのでしょう。
payloadというのはAPL Documentの最初に定義されている、データバインディング用の値です。

ここで重要なのは「変数には接頭詞としてつける文字がある」ということです。 この接頭詞はAPL Documentの”parameters”という値の中に入っています。
APL DocumentをJSONで見た時に頭の方に入っています。

ここに入っている値が接頭詞となります。

Lambdaからデータバインディングする

さて、これらの「データを用意して、変数化したAPL Documentのコンポーネントにバインディングする」という処理をLambdaから行うとどうなるでしょうか。

STEP3の解答を元に解説致します。

responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                version: '1.0',
                 document: APLDocs.staff,
                datasources: {
                    cheerData: {
                        properties: {
                            staffImage: img,
                            staffString: str
                        }
                    }

                }
            });

まず、LambdaはAlexaのサーバーからJSON形式でRequestを受け、同じくAlexaのサーバーにJSON形式でResponseを返します

AlexaのサーバーからEchoデバイスへ「音声以外」の情報を送る時は「Directive」というセクションを使います。今回のAPL、つまり画面にまつわる情報もDirectriveセクションに情報を詰め込んで返します。

こちらがLambdaからのResponse Dataの一部です。

{
    "body": {
        "version": "1.0",
        "response": {
            "outputSpeech": { //ここに音声情報が入る
                "type": "SSML",
                "ssml": "<speak>応援メッセージスキルです。色々頑張りすぎてクタクタになっているスタッフに応援メッセージを送ってあげましょう。おんせんさん、しょうさん、せーのさんの、どなたを応援しますか?</speak>"
            },
            "directives": [
                {
                    "type": "Alexa.Presentation.APL.RenderDocument", // ここから画面情報が入る
                    "document": {
                        "type": "APL",
                        "version": "1.0",  
............
                    },
        },
        "sessionAttributes": {},
        "userAgent": "ask-node/2.4.0 Node/v8.10.0"
    }
}

AlexaからEchoに対しては基本はテキストデータが返り、その中に規定されているURLにEchoがリダイレクトして情報を取得する形になります(ですので画像や音声などのデータはEchoデバイスからアクセスできる形で保管されている必要があります)。ここまでが基本です。

LambdaからこのDirectiveセクションにデータを詰めるには responseBuilder.addDirective() メソッドを使います。type: 'Alexa.Presentation.APL.RenderDocument' と指定することでAPL DocumentやAPL Dataと連携できます。

もう一度STEP3の解答を見てみましょう。

responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                version: '1.0',
                 document: APLDocs.staff,
                datasources: {
                    cheerData: {
                        properties: {
                            staffImage: img,
                            staffString: str
                        }
                    }

                }
            });

document プロパティに表示するAPL Documentを指定します。今回は別ファイルに外出ししていますが、この場で動的に組んでも構いません。
そして datasource プロパティの中にバインドするデータをオブジェクト形式で書きます。ここに入っているデータがそのままAPL Documentのコンポーネントにバインドされます。

STEP3ではスタッフの画像と名前がバインドされていますが、例えばAPL Documentで画像を表示するimageコンポーネントのsourceプロパティは

${payload.cheerData.properties.staffImage}

と変数化されています。ここに上の datasource.cheerData.properties.staffImage に入っている画像パスが埋め込まれるわけです。
おわかりになりましたでしょうか。

Slackに投稿する

さて、APLとは関係ないところなのですが、ハンズオン中に「Slackに投稿する部分について教えて」という声がありましたので、ここで少し解説したいと思います。

Slackに投稿するにはApp機能でImcoming Webhooksを設定して、払い出されたURLにPostすればOKです。Imcoming Webhooksについてはこちらの記事を参考にしてください。

SNSでSlackにメッセージ送信する #slack | DevelopersIO

次にURLにPostする部分ですが、LambdaからURLにPostするにはNodeの「request」モジュールを使います。通常のLambdaではこういった外部のモジュールはローカルにてnpmコマンドなどでインストールし、node_modulesフォルダごとzipに固めてアップロードする形になります。
今回のハンズオンは「Alexa-hosted スキル」というAlexaがホストするAWSリソースを使っていますので、直接ローカルからアップロードすることはできません。その代わりに、hosted Lambda内の「package.json」に入れたいモジュールを書き込んでDeployすることで、Lambda内に外部モジュールを入れることができます。

今回はこのようにpackage.jsonにrequestモジュールを書き込んでDeployしました。これでhosted Lambda内にrequestモジュールが入ります。 あとは

var request = require('request');

var options_post_slack = {
    url: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXX',

    form: { payload: ''
    },
    json: true
};

このようにrequest先のURLとPostの形式を固めて

function sendSlack (staffName, yourName) {
  var notification_msg = yourName + 'さんより' + staffName + 'さんに応援のメッセージが入っております!';
  var random_msg = ['頑張ってね!', 'だからもっと働け、馬車馬のように!', 'あなたをみんな見守っていますよ!', '忙しくなんかない!忙しくなんかないんだ!', 'あと16時間は働けますね!']

  notification_msg += randomPhrase(random_msg);
    options_post_slack.form.payload = '{"text": "' + notification_msg + '"}';
    request.post(options_post_slack, function (error, response, body) {
              if (!error && response.statusCode === 200) {
                 console.log(body);
                 //context.succeed("post succeed.");
              }else{
                  console.log('error: '+ response.statusCode);
                  //context.fail("post failed.");
              }
          });

}

postメソッドでメッセージと共に飛ばすだけです。簡単ですね。

STEP4

最後のSTEP4ではAPL Commandの使い方についでのハンズオンでした。
大まかなAPL Commandはハンズオンにてお話したので割愛して、ここではLambdaからAPL Commandを流し込む部分について解説します。

まずはAPL Commandを定義している部分です。31行目付近です。

let aplCommands = [
                {
                    "type": "Sequential",
                    "commands": [
                        {
                            "type": "Parallel",
                            "commands": [
                                {
                                    "type": "SetPage", 
                                    "componentId": "staffinfo",
                                    "position": "absolute",
                                    "value": 1
                                },
                                {
                                    "type": "SpeakItem",
                                    "componentId": "SpeechOnsen" 
                                }
                            ]
                        },
..............

ここではAPL CommandをJSON形式でまとめています。SetPageでpagerのページを指定して、SpeakItemでcomponentIDに指定したコンポーネントで発話させます。これをスタッフの画像分繰り返して、最後はTopページに戻す、という動きです。
ちなみにここではページ数をposition: absoluteで絶対指定しています。こう指定すると、ラグなくパッと画面が切り替わります。一方position: relativeで相対指定すると動き的にはスッとスワイプされたような動きになります。

次にこれをAlexaサーバーに送信する部分です。100行目付近です。

const commandDirective = {
            type: 'Alexa.Presentation.APL.ExecuteCommands',
            version: '1.0',
            token: 'aplToken',
            commands: aplCommands, 
            };

Directiveのtypeを'Alexa.Presentation.APL.ExecuteCommands’と指定することで、APL CommandをAlexaに流すことが出来ます。ここは簡単ですね。

ssmlToSpeech

SpeakItemコマンドを使う時に少しややこしい部分を解説しておきます。
STEP4でAPL Commandを流す時のAPL Document Directiveの部分の一部を見てください。

const aplDirective = {
            type: 'Alexa.Presentation.APL.RenderDocument',
                version: '1.0',
                token: 'aplToken',
                document: APLDocs.launch,
                datasources: {
                    cheerData: {
                        properties: {
                            "SsmlOnsen": "<speak><prosody volume='x-loud'>おんせんさん</prosody></speak>",
............
                        },
                        transformers: [
                            {
                              inputPath: "SsmlOnsen",
                              outputName: "OnsenSpeech",
                              transformer: "ssmlToSpeech" 
                            },
............
                        ]
                    }
                }
            }

ここで使っているのが「transformer」というブロックです。transformerはAPL Documentにヒントや音声を変換して追加するためのブロックです。細かい情報はこちらを御覧ください。

APLデータソースとトランスフォーマー | Alexa Presentation Language

今回はこのtransformerから「ssmlToSpeech」を使用しています。これはdatasource内にあるSSMLデータを音声に変換する、という機能です。
このssmlToSpeechにはルールがありまして、元となるSSMLデータ(inputPath)はdatasource内では必ず「propertiesプロパティ内に定義する」と決まっています。ですので「properties.SsmlOnsen」と定義されているデータはinputPath内では「SsmlOnsen」と定義します。それをssmlToSpeechで変換してoutputName(OnsenSpeech)の名前で出力します。この「OnsenSpeech」という名前の変数をAPL Document内のTextコンポーネントに「Speech」プロパティとして規定しておくと音声としてバインディングされます。

APL Docment 175行目付近

                    {
                        "type": "Text",
                        "width": "0",
                        "height": "0",
                        "text": "\"\"",
                        "id": "SpeechOnsen", //ここがAPL CommandのComponentIDで指定されるところ
                        "speech": "${payload.cheerData.properties.OnsenSpeech}" //ここがtransformerのoutputNameで定義されるところ
                    },

ちなみに「SsmlOnsen」内に定義しているSSMLデータに<prosody volume='x-loud'> が入っているのは、SpeakItemコマンドで定義した音声は通常のOutputSpeechにて発話している音声に比べて音量が小さい、という特徴があるので、SSMLでボリュームをあげた上で出力しています。Tipsとして覚えておくといいかも知れません。

まとめ

以上、今回のハンズオンをアンケートや会場の声を元に解説してみました。
ハンズオンに参加された方は理解が深まったかと思います。
ハンズオンに参加されていない方はスライドとハンズオンテキストからいい感じに雰囲気を感じ取ってもらって、わからない部分は次回AAJUGで私を捕まえて聞いていただければと思います。

次回AAJUG関東支部は5月中旬を予定しています。お楽しみに!