Alexaスキルアワード2019ハッカソン大阪で作った「ゼロカロリー」の技術を惜しみなく晒してみた

2019年7月6日〜2019年7月7日にかけて開催されたAlexaスキルアワード2019ハッカソン大阪で作った「ゼロカロリー」というスキルで使った技術を惜しみなく晒していきます。

イベントレポートはこちら
https://note.mu/gaomar/n/nc27f57c89201

# はじめに
２日間のハッカソンなので、とりあえず動くプロトタイプを作っています。セキュリティ対策などほとんどしていないので、参考程度にお楽しみください。
作ったものはサンドウィッチマンの伊達さんが考案した`ゼロカロリー理論`を誰でも登録できるシステムです。登録したものをAlexaが答えてくれます。

# 完成動画
[![ゼロカロリー](http://img.youtube.com/vi/iXaamrXA02M/0.jpg)](http://www.youtube.com/watch?v=iXaamrXA02M)

# アーキテクチャ
使った技術は以下の通り。Alexaのハッカソンなのに関係ない技術ばかりｗ

* Alexa Skills Kit
* Dialogflow
* MessagingAPI
* LINE Bot
* DynamoDB
* API Gateway
* AWS Lambda
* Azure Functions

<img width="1160" alt="スクリーンショット 2019-07-10 12.17.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/253a0397-9294-a38d-9720-b6a2e614d44f.png">

# 1. 登録・取得用のAPIを作成
まずは食べ物の名前とゼロカロリーの理由を登録する処理を行います。
API Gateway → AWS Lambda → DynamoDBという流れです。S3もありますが、今回のハンズオンでは直接画像をアップロードしています。

↓ アーキテクチャのこの部分 ↓
<img width="482" alt="スクリーンショット 2019-07-10 12.22.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/2834a947-0e6d-8fdd-7fd1-ebf5b9bd6dec.png">

## 1-1. AWS Lambda関数を作成する
サクッと関数を作成します。

|項目|値|
|:--|:--|
|①関数名|Zerocalorie-API|
|②ランタイム|Node.js 10.x|
|③実行ロール|AWS ポリシーテンプレートから新しいロールを作成|
|④ロール名|Basic-Lambda-DynamoDB|
|⑤ポリシーテンプレート|基本的なLambda@Edgeのアクセス権限|

![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2019-07-10-10_38_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/d4a27a4a-eb74-2e10-90ab-a5094a75febc.png)

## 1-2. プログラムを記述する
では実際にプログラムを書いていきます。

```javascript:index.js
'use strict';
const Util = require('./util.js');

exports.handler = async (event) => {
    var ret = {
        "fulfillmentText": ""
    };
    
    if (event.httpMethod === 'POST') {
        // 投稿
        const body = JSON.parse(event.body);
        const food = body.queryResult.parameters.food;
        const reason = body.queryResult.parameters.reason;

        // DynamoDBに保存する        
        await Util.putCalorie(food, reason);
        
        ret.fulfillmentText = `${food}を登録しました。`;
        
    } else if (event.httpMethod === 'GET') {
        // 取得
        const food = event.queryStringParameters.food;
        ret = await Util.searchCalorie(food);
        
    }

    const response = {
        statusCode: 200,
        body: JSON.stringify(ret)
    };

    return response;
};
```

```javascript:util.js
'use strict';
const AWS = require('aws-sdk');
const DynamoDB = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1"
});
const documentClient = new AWS.DynamoDB.DocumentClient();
const uuid = require("uuid/v4")

// ゼロカロリー理論登録
module.exports.putCalorie = async function putCalorie(food, reason) {
    // テーブル名設定
    const tableName = process.env.DYNAMO_TABLE_NAME;

    // DynamoDBに書き込む
    await documentClient.put( {
        "TableName": tableName,
        "Item": {
          "ID": uuid(),
          "food": food,
          "reason": reason
        }
    }, function( err, data ) {
        console.log(err);
        
    }).promise();
};

// ゼロカロリー理論検索
module.exports.searchCalorie = async function searchCalorie(food) {
  try {
    // queryの実行
    const queryItems = await DynamoDB.query({
      TableName: process.env.DYNAMO_TABLE_NAME,
      IndexName: 'food-index',
      KeyConditionExpression: "#FOOD = :FOOD",
      ExpressionAttributeNames: {"#FOOD": "food"},
      ExpressionAttributeValues: {":FOOD": food}
    }).promise();
    
    // 結果をreturnする
    return queryItems;
    
  } catch (err) {
    // エラー発生時はエラー文をreturnする
    console.error(`[Error]: ${JSON.stringify(err)}`);
    return err;
  }  
};
```

## 1-3. API Gatewayを設定する
トリガーの追加をクリック

<img width="943" alt="スクリーンショット_2019-07-10_10_48_49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/30f07371-fcd1-e36e-6750-7495e6a1ebec.png">

API Gatewayの設定を行う。

<img width="819" alt="スクリーンショット_2019-07-10_10_49_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/2bb06a33-5e56-9bf4-fa34-f180eb642528.png">

発行されたURLをメモしておく
<img width="950" alt="スクリーンショット_2019-07-10_10_49_24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/653d9100-33af-fc55-a4d0-ecf03d28ef28.png">

## 1-4. Lambdaレイヤーを設定する
UUIDを発行するためのモジュールをレイヤーに追加しておきます。左側メニューのLayersからレイヤーの作成をクリックします。

<img width="1005" alt="スクリーンショット_2019-07-10_10_50_06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/95b18aa7-bda2-c0d0-0738-914ab0793d02.png">

|項目|値|
|:--|:--|
|①名前|UUID-SDK|
|②説明|UUID-SDK|
|③アップロード|作成したzipを選択する|
|④ランタイム|Node.js 10.x|

<img width="832" alt="スクリーンショット_2019-07-10_10_50_47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/e530e5ec-9baa-4efb-1226-514bd235a175.png">

再びLambda関数に戻って、Layersをクリックします。その下にあるレイヤーの追加をクリックします。

<img width="958" alt="スクリーンショット_2019-07-10_10_51_47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/d7422371-2824-a20f-82b3-1d122ba0b4b7.png">

作成したUUID-SDKをプルダウンメニューから選択、バージョンも選択して追加ボタンをクリックします。

<img width="830" alt="スクリーンショット_2019-07-10_10_52_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/df287fbf-8347-c759-c5b4-a936dad8042d.png">

Layers（１）になっていることを確認します。右上の保存ボタンをクリックします。

<img width="1061" alt="スクリーンショット_2019-07-10_10_52_28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/0225d3de-3e9e-efe5-c914-fe4e5af00740.png">

## 1-5. 実行ロール権限編集する
適用されているロールの権限を編集します。

<img width="525" alt="スクリーンショット_2019-07-10_10_52_56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/ba321b6f-90ae-b08a-dbbf-53548aaf9b4e.png">

インラインポリシーを追加します。

<img width="879" alt="スクリーンショット_2019-07-10_10_53_14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/918fa2e9-967d-dd95-41de-5b797d343258.png">

DynamoDBを検索してクリックします。

<img width="985" alt="スクリーンショット_2019-07-10_10_55_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/14a093f2-9df8-e457-3156-291c17066371.png">

QueryとPutItemの権限を付与します。

<img width="864" alt="スクリーンショット_2019-07-10_10_56_49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/c9a0e562-61ef-5d7a-ba31-37dba5b6f26a.png">

リソースは全てのリソースを選択して、ポリシーの確認をクリックします。

<img width="1054" alt="スクリーンショット_2019-07-10_10_57_22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/001d9126-5c29-0d91-14bf-ca5ad5ad9cc6.png">

ポリシー名を入力して作成をクリックします。

<img width="851" alt="スクリーンショット_2019-07-10_10_58_15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/37434f5b-cba8-dd6b-8ce8-d759eac323ee.png">

## 1-6. DynamoDBテーブルを作成する
ゼロカロリー理論を格納するためのデータベースを作成します。テーブル名とプライマリキーを入力します。

<img width="685" alt="スクリーンショット_2019-07-10_11_00_29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/ca06a0aa-0899-d0fa-a9d2-e0cb256f4ae9.png">
キーを入力します。

インデックスタブのインデックスの作成ボタンをクリックします。

<img width="720" alt="スクリーンショット_2019-07-10_11_02_46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/13195549-433a-88b6-0d2e-2e7df75b1665.png">

プライマリキー以外の食べ物で検索をしたいので、foodのインデックスを作成します。

<img width="627" alt="スクリーンショット_2019-07-10_11_03_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/135b8c34-f238-de39-ea3e-256d9366abcb.png">

`food-index`が生成されました。生成には5分ほどかかります。

<img width="569" alt="スクリーンショット_2019-07-10_11_09_56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/35b1d341-e11b-d8ac-8004-46eed0fd7dbe.png">

## 1-7. POSTMANで確認してみる（登録編）
出来たプログラムの動作確認をしてみましょう。POSTMANを起動して、登録できるか確認してみます。
`POST`を選択して、API GatewayのURLを指定します。Bodyの値は以下の通りです。

foodに食べ物名、reasonにゼロカロリー理論を入力します。Sendボタンを押すと登録されることが確認できます。

```json
{
	"queryResult": {
		"parameters": {
			"food": "ドーナツ",
			"reason": "形がゼロを表しているから"
		}
	}
}
```

<img width="1083" alt="スクリーンショット_2019-07-10_11_19_45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/e08cd248-ae8f-97cc-654a-c95b666b0255.png">


## 1-8. POSTMANで確認してみる（取得編）
`GET`にしてAPI GatewayのURLの最後に、`?food=ドーナツ` というようにパラメーターを付与します。
Snedボタンを押すと、DynamoDBに登録されているものが返ってきます。

<img width="1091" alt="スクリーンショット_2019-07-10_11_20_05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/185c8ae8-24aa-a121-c97e-101951183fc5.png">

# 2. LINE Botの作成
ゼロカロリー理論はLINE Botから誰でも登録できるようにしています。
LINE DeveloperページからMessaging APIで新規チャネルを作成します。

https://developers.line.biz/ja/

<img width="720" alt="s200.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/ad67299d-6834-f868-89cb-92e54479e95a.png">

アプリ名、説明、カテゴリー、メールアドレスを設定して、確認ボタンをクリックします。

![s201.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/d5900e23-407d-ecc8-ce13-91559b5902d1.png)

ゼロカロリーのチャネルが出来たので、クリックします。

<img width="254" alt="s202.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/3a1b31d5-3ad5-b538-0695-cf797fc611ea.png">

再発行ボタンを押してください。発行されたアクセストークンは後ほど使うのでメモしておきましょう。

<img width="843" alt="s203.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/0661f04f-3759-c940-0c3f-3d67251dfadb.png">

# 3. Dialogflowの設定
LINE Botの自然言語処理はDialogflowを使って行います。

https://dialogflow.com/

## 3-1.新規エージェントを作成する
新規エージェントを作成します。エージェント名とデフォルト言語を日本語にします。

<img width="682" alt="s300.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/461be202-d0a2-7f36-17d3-94b82b7e155d.png">

## 3-2. StartIntentの設定
スタートという言葉に反応して、食べ物名とゼロカロリーの理由の入力を促すようにします。

Intentsの右側にある「＋」をクリックします。

|項目|値|
|:--|:--|
|②インテント名|StartIntent|
|④Training phrases|スタート|

|④のfoodの設定|値|
|:--|:--|
|REQUIRED|チェック入れる|
|PARAMETER NAME|food|
|ENTITY|@sys.any|
|PROMPTS|登録する食べ物を教えてください|

|④のreasonの設定|値|
|:--|:--|
|REQUIRED|チェック入れる|
|PARAMETER NAME|reason|
|ENTITY|@sys.any|
|PROMPTS|ゼロカロリーの理由を教えてください|

|項目|値|
|:--|:--|
|⑤Enable webhook call for this intent|ONにする|

<img width="1305" alt="s301.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/2ea55550-12df-d299-e16f-b77ecd087edd.png">

## 3-3. Fulfillmentの設定
WebhookのURLを設定します。`1-3`で取得したAPI GatewayのURLを設定します。入力したら、下にあるSaveボタンを教えてください。

<img width="1000" alt="s302.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/54229280-fa4b-2220-078a-b92d892bd4ae.png">

## 3-4. IntegrationsでLINEと連携する
左側メニューのIntegrationsをクリックして、LINEをクリックします。

<img width="1039" alt="s303.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/637ea916-ff3f-6405-e3dd-3e1d43200c45.png">

表示されるポップアップにLINEの情報を入力して、スタートボタンを押して、連携をスタートします。

<img width="1639" alt="s304.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/d690a598-95ec-8ebd-1079-96f747d6129d.png">

## 3-5. LINE Botと友だちになる
LINE DeveloperページにQRコードが表示されているので、それを読み取ってBotと友だちになっておきます。
「スタート」と入力すると食べ物と理由が聞かれるので答えます。

<img width="1283" alt="s305.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/5ceb3abb-2c25-3d4d-a863-cc7a1d3456b5.png">

# 4. Azure Functionsの設定をする
Alexaのプログラムを今回はAzure Functionsでやってみます。AWS Lambdaだけでもできますが、クラウド技術を駆使したかったので、今回は特別です。
前半のセットアップに関しては、こちらも参考にしてください。

https://qiita.com/h-takauma/items/25ab6937842d52412ac1

## 4-1. Azure FunctionsのCLIツールをインストール

サクッとCLIツールを入れる

```shell
$ npm install -g azure-functions-core-tools 
```

## 4-2. プロジェクトの作成
CLIコマンドを実行していきます。適当なフォルダを作ってそこに作成していきます。
必要なモジュールをインストールしていきます。

```shell
$ mkdir zerocalorie-alexa
$ cd zerocalorie-alexa
$ func init                      # 選択肢が出てくるのでnodeとjavascriptを選ぶ
$ func new                       # Http triggerを選択し、zerocalorie-alexaという名前で作成する
$ npm init -y
$ npm i -s ask-sdk-core          # Alexa用SDK
$ npm i -s ask-sdk-model
$ npm i -s aws-sdk
$ npm i -s request
```

## 4-3. functions.jsonを編集する
HTTPで受け入れるリクエストが定義されているのですが、デフォルトだと GET, POST なので今回は GET が不要なので削除しましょう。誰でもアクセスできるようにanonymousにもしておきます。

```json:zerocalorie-alexa/functions.json
{
  "bindings": [
    {
      "authLevel": "anonymous",        // anonymousにしておく
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "post"      // getは使わないので消しておく
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"     // $returnにしておく
    }
  ]
}
```

## 4-4. プログラムを記述する
実際に動作するプログラムを入力していきます。これでAzure Functionsにデプロイすれば動かすことができます。

```javascript:zerocalorie-alexa/index.js
'use strict';

const AWS = require('aws-sdk');
const Alexa = require('ask-sdk-core');
const Util = require('./util.js');

// s3初期化
AWS.config.update({
  "accessKeyId": process.env.ACCESSKEY,       // アクセスキーID
  "secretAccessKey": process.env.SECRETKEY,   // シークレットアクセスキー
  "region": "ap-northeast-1"                  // Tokyoリージョン
});
const s3 = new AWS.S3();

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  async handle(handlerInput) {
    const speechText = '食べ物のカロリーをお答えします。好きな食べ物を言ってください。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .getResponse();
  }
};

// 画像存在チェック
const getS3Object = async (foodVal) => {
  var params = {
    Bucket: 'zerocalfoods',
    Key: `${foodVal}.jpg`
  };
  try {
    const metadata = await s3.headObject(params).promise();
    console.log(metadata);
    return `${process.env.IMAGEURL}/${foodVal}.jpg`;
  } catch (err) {
    if (err.code === 'NotFound') {
      return `${process.env.IMAGEURL}/errorCat.jpg`;
    }
    throw err;
  }
};

// 食べ物取得
function getFood(intent) {
  if (intent.slots.Food && intent.slots.Food.resolutions && intent.slots.Food.resolutions.resolutionsPerAuthority) {
    if (intent.slots.Food.resolutions.resolutionsPerAuthority[0].status.code == 'ER_SUCCESS_MATCH') {
      if(intent.slots.Food.resolutions.resolutionsPerAuthority[0].values.length == 1) {
        return intent.slots.Food.resolutions.resolutionsPerAuthority[0].values[0].value.name;
      }
    }
  }
  return intent.slots.Food.value;
}

const FoodIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'FoodIntent';
    },
    async handle(handlerInput) {
      const intent = handlerInput.requestEnvelope.request.intent;
      const foodVal = getFood(intent);

      // 理由取得
      const ret = await Util.getReason(foodVal);
      const myRet = JSON.parse(ret);
      var speechText = '';
      var myURL = `${process.env.IMAGEURL}/errorCat.jpg`;
      
      if (myRet.Count === 0) {
          speechText = `${foodVal}は、まだ登録されていません。他に知りたい食べ物はありますか？`;

      } else {
          const reasonIndex = Math.floor(Math.random() * myRet.Items.length);
          speechText = `${foodVal}は、${myRet.Items[reasonIndex].reason}、カロリーゼロ。他に知りたい食べ物はありますか？`;
          myURL = await getS3Object(foodVal);
      }

      if (supportsDisplay(handlerInput)) {
        // 画面付きデバイス
        return handlerInput.responseBuilder
        .speak(speechText)
        .addDirective({
            type : 'Alexa.Presentation.APL.RenderDocument',
            version: '1.0',
            token: "token",
              document: require('./apl_document.json'),
              datasources: 
              {
                "skilldata": {
                  "url": myURL
                }
              }
            })
        .withShouldEndSession(false)
        .getResponse();

      } else {
        return handlerInput.responseBuilder
          .speak(speechText)
          .reprompt(speechText)
          .getResponse();

      }
    }
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    return handlerInput.responseBuilder
      .withShouldEndSession(true)
      .getResponse();
  }
};

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)
        .getResponse();
  }
};

const CancelAndStopIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.NoIntent');
  },
  handle(handlerInput) {
    const speechText = 'さようなら！';
    return handlerInput.responseBuilder
      .speak(speechText)
      .withShouldEndSession(true)
      .getResponse();
  }
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    console.log(`~~~~ Error handled: ${error.message}`);
    const speechText = `エラーが発生しました。`;

    return handlerInput.responseBuilder
      .speak(speechText)
      .getResponse();
  }
};

// 画面付きかどうかチェック
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;
}

let event = function (req) {
  let EventEmitter = require('events');
  let ee = new EventEmitter();
  for (let prop in req.body) {
    if(prop){
      ee[prop] = req.body[prop];
    }
  }
  return ee;
};

let skill;
module.exports = async function (context, req) {
  if(!skill){
    // ハンドラ設定
    skill = Alexa.SkillBuilders.custom()
      .addRequestHandlers(
        LaunchRequestHandler,
        FoodIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
      )
      .addErrorHandlers(ErrorHandler)
      .create();
  }

  // 処理実行＆Alexa応答
  return skill.invoke(event(req));
};
```

```javascript:zerocalorie-alexa/util.js
'use strict';
const request = require('request');

// ゼロカロリー理論を取得する
module.exports.getReason = async function getReason(food) {
    // オプションを定義
    const headers = {
        'User-Agent': 'Request-Promise'
    };

    const options = {
      url: process.env.URL,
      method: 'GET',
      headers: headers,
      qs: {
          'food': food
      }
    };

    return new Promise(function (resolve, reject) {
      request(options, function (error, response, body) {
        if (!error) {
          const myBody =  JSON.parse(body);
          const count = myBody.Count;
          console.log("body_count="+count);
          
          resolve(body);
          
        } else {
          console.log('error: ' + response + body);
          resolve(true);
        }
      });
    });
};
```

```json:zerocalorie-alexa/apl_document.json
{
        "type": "APL",
        "version": "1.1",
        "settings": {
            "idleTimeout": 120000
        },
        "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",
                "values": [
                    {
                        "color": "@colorTextPrimary"
                    }
                ]
            },
            "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"
                }
            }
        },
        "onMount": [
            {
                "type": "Sequential",
                "commands": [
                    {
                        "type": "Parallel",
                        "commands": "<COMPONENT_ON_MOUNT_COMMANDS>"
                    }
                ],
                "finally": "<DOCUMENT_ON_MOUNT_COMMAND>"
            }
        ],
        "graphics": {
            "parameterizedCircle": {
                "type": "AVG",
                "version": "1.0",
                "height": 100,
                "width": 100,
                "items": {
                    "type": "path",
                    "fill": "red",
                    "stroke": "blue",
                    "strokeWidth": 4,
                    "pathData": "M 50 0 L 100 50 L 50 100 L 0 50 z"
                }
            }
        },
        "commands": {
            "slideInFromRight": {
                "parameters": [
                    "distance"
                ],
                "command": {
                    "type": "AnimateItem",
                    "easing": "ease-in-out",
                    "duration": 300,
                    "values": [
                        {
                            "property": "opacity",
                            "from": 0,
                            "to": 1
                        },
                        {
                            "property": "transformX",
                            "from": "${distance}",
                            "to": 0
                        }
                    ]
                }
            }
        },
        "layouts": {},
        "mainTemplate": {
            "parameters": [
                "payload"
            ],
            "items": [
                {
                    "when": "${viewport.shape == 'round'}",
                    "type": "Container",
                    "direction": "column",
                    "items": [
                        {
                            "type": "Container",
                            "grow": 1,
                            "alignItems": "center",
                            "justifyContent": "center",
                            "items": [
                                {
                                    "type": "Image",
                                    "width": "100vw",
                                    "height": "100vh",
                                    "source": "${payload.skilldata.url}",
                                    "scale": "best-fill",
                                    "align": "center"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "Container",
                    "height": "100vh",
                    "width": "100vw",
                    "items": [
                        {
                            "type": "Image",
                            "width": "100vw",
                            "height": "100vh",
                            "source": "${payload.skilldata.url}",
                            "scale": "best-fill",
                            "position": "absolute"
                        }
                    ]
                }
            ]
        }
    }
```

## 4-5. Azure Functionsへデプロイする
リソースを作成します。

<img width="312" alt="s400.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/8f615dc3-b702-3828-b7aa-8290ded18740.png">

VSCodeでデプロイします。

<img width="353" alt="s401.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/a443dc95-f229-9c75-a9f5-daa2f09784f6.png">

関数をクリックして、アクセスするためのURLをメモしておきましょう。

<img width="1165" alt="s402.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/9a2dfc53-712a-e79f-2f5c-fa5576375eba.png">

## 4-6. 環境変数を設定する
プログラムで設定している環境変数を記述します。
構成をクリックします。

<img width="719" alt="s403.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/cdcaf3b4-af54-187b-0d3b-eef29133ff34.png">

新規でキーを追加して、アクセスURLを指定します。

<img width="935" alt="s404.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/37edf1a5-e37f-5a1b-8e8f-d57173a495d3.png">


# 5.Alexaの設定を行う
Amazon DeveloperにアクセスしてAlexaスキルを新規作成します。

https://developer.amazon.com/ja/alexa-skills-kit

## 5-1. 新規スキル作成
スキル名は`ゼロカロリー`、モデルは`カスタム`、`ユーザー定義のプロビジョニング`を選択。
右上のスキルを作成ボタンをクリックします。

![s500.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/554d3ef3-66a1-bea0-b13b-2755d5c96c7f.png)

## 5-2. 呼び出し名を設定する
呼び出し名を`ゼロカロリー`にする

<img width="693" alt="s501.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/83bb5dbe-8d6e-b925-f25c-f518768b9b42.png">

## 5-3. スロットタイプを追加する
スロットタイプを追加します。`food`で絞り込んで、AMAZON.Foodのスロットタイプを追加します。

<img width="1157" alt="s502.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/f934e454-7488-2325-07d7-a38ec97412d0.png">

## 5-4. インテントを追加する
追加ボタンをクリックして、`FoodIntent`と入力します。カスタムインテントを作成ボタンをクリックしてインテントを追加します。

<img width="1142" alt="s503.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/d52589e0-cd0a-a5e0-161a-6461e91df10c.png">

インテントスロットに`Food`と入力して、「＋」を押します。右側のプルダウンメニューから`AMAZON.Food`を選択します。

<img width="831" alt="s504.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/42f9ec79-d0a2-502e-1595-6235805bdb55.png">

考えられる会話のパターンを登録していきます。登録が終われば保存とビルドを行います。

<img width="827" alt="s505.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/8d04ac7f-d84e-b9d0-4065-77d64e78deab.png">

## 5-5. インターフェースを設定する
`Displayインターフェース`と`Alexa Presentation Language`の設定をそれぞれ有効にします。

<img width="1680" alt="s506.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/99837d67-b8a6-4a4b-9cf9-218d21ffce30.png">

## 5-6. エンドポイントを設定する
左側メニューのエンドポイントから`HTTPS`を選択します。デフォルトの地域に4-5で取得したAzureのアクセルURLを設定します。

<img width="1680" alt="s507.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/7f2be925-52d6-9a91-2b4a-9e9a2f453007.png">

## 5-7. シミュレーターで確認する
テストタブをクリックして、`ゼロカロリーをひらいて`と入力して、登録した食べ物を入力します。
※ここで画像が出てこない方はご自身のS3に画像が入っていないからです。

<img width="1189" alt="s508.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154355/8c5836dd-2b98-d155-ae37-3b1261030ecc.png">


# まとめ
ちょっと無理やりですが、様々なクラウド技術を使ってゼロカロリースキルを作ってみました。
アイデア次第でいろんな連携ができるので参考にしてみてください！

システム化のご検討やご相談は弊社までお問い合わせください。
https://i-enter.co.jp/contact/