あままこのブログ

役に立たないことだけを書く。

Amazon Echoに今NHKで放送している番組を教えてもらう (Alexa Skills Kit & AWS Lambda & NHK番組表API)

Amazon Echo Dotがやっと家に来ました。

Amazon Echo Dot (Newモデル)、ブラック

Amazon Echo Dot (Newモデル)、ブラック

ので、こいつでも何か自作のアプリを作ってみようと思い、前の記事 amamako.hateblo.jp で作ったものと同じような機能を持つアプリの、Amazon Alexa版を作ってみることにします。 結果がこちらです。


Amazon Echoに今NHKで放送している番組を教えてもらう

作り方

下記のページの「スキル開発を始める」をクリックし、Amazonのアカウント情報を入力して開発を始めます。 developer.amazon.com https://gyazo.com/56e3c2188f80c9e562078668d0c0311b
Alexa Skill Kitの「始める」をクリック。
https://gyazo.com/7bd9aeebc64557c08de2480b9618218a
「新しいスキルを開発」をクリック。 https://gyazo.com/a233219432163db35de4e0207a9f58eb 「カスタム対話モデル」を選択し、言語・スキル名・呼び出し名などを入力していき、画面下部の「次へ」をクリックします。
https://gyazo.com/fb95eb9021ea74fb1d3cc87a6566189b
そうするとこの画面になります。
ここで前回の記事でも行ったように、IntentやSlots(前回の記事で言うEntities)の登録を行っていきます。 Intentとは要するに「こういう文がきたらこういう機能を呼び出すよ」という文例の定義で、Slotsは「文中にこういう単語が入りうるよ」という単語集の定義です。詳しくは公式のドキュメント
developer.amazon.com
や解説記事
dev.classmethod.jp
などをご参照ください。
ただ、実はこの画面からは簡易的な定義しかできません。ちょっと複雑な定義、例えば同じ単語に2つ以上呼び名がある場合などのSlotsの定義はここからはできないので、画面の「スキルビルダーを起動する」からスキルビルダーを起動します。
https://gyazo.com/fd9407a6d7fd2dc9f0f6608e0ae255dc
そうするとこんな画面が出てきます。ここでCode Editorを選択し
https://gyazo.com/9562fc63138f4e13e1768b3e9da33e6e
以下のコードを入力。「Apply Changes」、「Save Model」、「Build Model」とクリックしていきます。

{
  "languageModel": {
    "types": [
      {
        "name": "LIST_OF_STATIONS",
        "values": [
          {
            "id": "sougou",
            "name": {
              "value": "NHK総合テレビ",
              "synonyms": [
                "総合テレビ",
                "NHK",
                "総合",
                "nhk 総合テレビ",
                "nhk"
              ]
            }
          },
          {
            "id": "etv",
            "name": {
              "value": "NHK教育テレビ",
              "synonyms": [
                "教育テレビ",
                "教育",
                "nhk 教育テレビ"
              ]
            }
          },
          {
            "id": "bs1",
            "name": {
              "value": "NHKBS1",
              "synonyms": [
                "BS1",
                "nhkbs1"
              ]
            }
          },
          {
            "id": "bspremium",
            "name": {
              "value": "NHKBSプレミアム",
              "synonyms": [
                "BSプレミアム",
                "nhk bsプレミアム",
                "bsプレミアム"
              ]
            }
          }
        ]
      }
    ],
    "intents": [
      {
        "name": "AMAZON.CancelIntent",
        "samples": []
      },
      {
        "name": "AMAZON.HelpIntent",
        "samples": []
      },
      {
        "name": "AMAZON.StopIntent",
        "samples": []
      },
      {
        "name": "GetTVProgram",
        "samples": [
          "{Station} で放送している番組を教えて",
          "{Station}"
        ],
        "slots": [
          {
            "name": "Station",
            "type": "LIST_OF_STATIONS"
          }
        ]
      }
    ],
    "invocationName": "ほたるの番組表"
  }
}

それぞれの定義の意味、特にidやsynonymsについては、下記のドキュメントを見てください。
developer.amazon.com
次に、画面の「Configuration」をクリックします。するとこんな画面が出てきます。
https://gyazo.com/60c2bf20383609acc07925b545780e1a
前回の記事ではFirebaseを用いてバックグラウンドの処理を実装したわけですが、今回はAmazonのサービスなのでAWS Lambdaを用います。
というわけでAWSのコンソールに入り、Lambdaを選択。
https://gyazo.com/c5457b6336fe8245f32f5d9f514bd6ad 「新しい関数を作成」を選択。 https://gyazo.com/2c3552f64222a01150052ee4cca87ce1
「設計図」を選択し、下の検索欄に「Alexa」と入力するとAlexaのテンプレートが出てくるので、「alexa-skills-kit-color-expert」を選択し、「設定」をクリックします。
https://gyazo.com/574c431f7e1b3ce6cc3dcbe12667c96f
すると以下のような画面が出てくるので、名前を入力し、ロール(アプリが他のAWSのリソースにアクセスする権限をまとめたもの)としてテンプレートから新しいロールを作成、ページ下部にある「関数を作成」をクリックします。 そうすると以下のような画面が出てきます。
https://gyazo.com/ab3d12f8d6a07630059376970f60dec2
コード編集欄でコードを編集していきます。今回のアプリではテンプレートをもとに以下のようなコードにしました。

'use strict';

const http = require('http');
const AREA = '';//取得したい番組表の地域ID
const API_KEY = '';//ここにNHK番組表のAPI_KEYを入れておきます。

/**
 * This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
 * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
 * testing instructions are located at http://amzn.to/1LzFrj6
 *
 * For additional samples, visit the Alexa Skills Kit Getting Started guide at
 * http://amzn.to/1LGWsLG
 */


// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: title,
            content: output,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    const sessionAttributes = {};
    const cardTitle = 'こんにちわ';
    const speechOutput = 'こんにちわ、ほたるの番組表です。 ' +
        'いま放送している番組を知りたい、テレビ局を教えて。';
    // If the user either does not reply to the welcome message or says something that is not
    // understood, they will be prompted again with this text.
    const repromptText = 'いま放送している番組を知りたい、テレビ局を教えて。';
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'さようなら。';
    // Setting this to true ends the session and exits the skill.
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

/**
 * Sets the color in the session and prepares the speech to reply to the user.
 */
function getTVProgram(intent, callback) {
    const cardTitle = "いま放送している番組";
    const stationNameSlot = intent.slots.Station;
    let repromptText = '';
    let sessionAttributes = {};
    let shouldEndSession = true;
    let speechOutput = '';
    const tvStation = {
        "sougou": "g1",
        "etv": "e1",
        "bs1": "s1",
        "bspremium": "s3"
    };
    console.log("called Station Name");
    console.log(stationNameSlot.value);
    let tvStationId = undefined;
    let tvStationName = undefined;
    if(stationNameSlot.resolutions 
        && stationNameSlot.resolutions.resolutionsPerAuthority[0].values
        && tvStation[stationNameSlot.resolutions.resolutionsPerAuthority[0].values[0].value.id] ){
        tvStationId = tvStation[stationNameSlot.resolutions.resolutionsPerAuthority[0].values[0].value.id];
        tvStationName = stationNameSlot.resolutions.resolutionsPerAuthority[0].values[0].value.name;
    }
    if (tvStationId) {
        shouldEndSession = true;
        //オプションを定義
        var url = 'http://api.nhk.or.jp/v2/pg/now/' + AREA + '/' + tvStationId + '.json?key=' + API_KEY;
        http.get(url, function(res) {
          var body = '';
          res.setEncoding('utf8');
          res.on('data', function(chunk) {
            body += chunk;
          });
          res.on('end', function() {
            var ret = JSON.parse(body);
            speechOutput = `いま ${tvStationName} で放送している番組は` + ret.nowonair_list[tvStationId].present.title + 'です'; // Send simple response to user\
            repromptText = speechOutput;
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
          });
        }).on('error', function(e) {
          console.log(e.message);
        });
    } else {
        shouldEndSession = false;
        speechOutput = "すみません、よくわかりません。";
        repromptText = "すみません、よくわかりません。";
        callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }

}


// --------------- Events -----------------------

/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}`);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest,callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'GetTVProgram') {
        getTVProgram(intent, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
        throw new Error('Invalid intent');
    }
}


// --------------- Main handler -----------------------

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {


        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            callback();
        }
    } catch (err) {
        callback(err);
    }
};

コードを入力後、関数を保存、ARNをコピーし、Configurationの画面に戻り入力、「次へ」を入力すると、自身のAmazon Echoでアプリを動かすことが可能になります。

感想

スマートスピーカーのアプリの作り方は、Google AssistantもAmazon Alexaも似たようなものみたいですね。