Amazon Echoに話しかけるとTwitterでつぶやくAlexaスキルを作ってみる

こんにちは。

前回は『アレクサ(Amazon Echo)で何ができるのか?』について記事にしました。 blog.wackwack.net


その中で「自分でAlexaスキルも作れる!」と紹介していまして、実際に自分でAlexaスキルを作ってみました。

と言うことで今回は、私が実際に作った『Alexaからツイッターに投稿するスキル』を例に、Alexaスキルの作り方をまとめます。

作成したスキル

今回作成したスキルは、

Alexaに「行ってきます」「ただいま」と声をかけるとツイッターにその時間を投稿する

というシンプルなものです。

ついでにAlexaが「つぶやきました」と喋ります。


ツイッター」での「ログ」ということで、『ツイログ』と名付けました。

  • 「アレクサ、ツイログで行ってきます」
  • 「アレクサ、ツイログでただいま」

と、不自然な発話をすることでツイートします。


なお開発用スキルであるため、公開申請はしていません。(というか今回の実装内容だと公開できない)

また今回のスキルは、TwitterAPIに必要なアカウント情報(ACCESS_TOKEN、ACCESS_TOKEN_SECRET)をプログラム中に埋め込むため、アプリ連携許可を飛ばして固定のTwitterアカウントでツイートされます。

全体イメージ

Alexaスキルのイメージはこんな感じです。
f:id:WorldWorldWorld:20180118231549p:plain

ユーザーがAmazon Echoに話しかけると、Amazon EchoはAlexaスキル(Alexa Skills Kit)を起動します。実際のスキルはAWS Lambda上に実装されており、AWS LambdaのプログラムはTwitterAPIを呼び出してツイートを行います。

参考資料

本家Amazonが全6回で「Alexaスキル開発トレーニング」が公開されています。

developer.amazon.com

一日あれば全部学べるボリュームで、かつこれを見れば最低限のスキル開発ができるようになります。興味がある方はまずここから始めましょう。

Alexaスキルの開発

TwitterApplicationManagementの登録

まず初めに、外部からTwitterAPIを叩くための各種キーを取得します。


Twitter Application Managementへアクセスし、自分のTwitterアカウントでログインします。

次に右上の「Create New App」をクリックします。 f:id:WorldWorldWorld:20171224102650p
(私の画面では登録済みのアプリケーションが表示されていますが、初利用時にはこの画面は空白です。)

登録するアプリケーションの入力フォームが出ます。
Create an application

「Name」「Description」「Website」は必須入力項目です。適当な内容で入力します。

  • Name:アプリケーションの登録名
  • Description:アプリケーションの説明文
  • Website:アプリケーションの問い合わせ先が分かるようなURLを入力します。私の場合はとりあえず当ブログのURLを登録しています。
  • CallbackURL:今回は設定不要です。

入力したらページ最下部の「Create your application」をクリックします。 Create your twitter application

これでアプリケーションの登録が完了しました。
f:id:WorldWorldWorld:20171224104225p:plain

続いてAPIの利用に必要となるAccess Tokenの発行を行います。表示されたページのタブ「Keys And Access Tokens」から「Create my access token」をクリックします。
f:id:WorldWorldWorld:20171224104509p:plain

これで準備は完了です。ページに表示されている「Consumer Key」「Consumer Secret」「Access Token」「Access Token Secret」をプログラム内で利用します。
f:id:WorldWorldWorld:20180119074855p:plain

f:id:WorldWorldWorld:20180119074858p:plain

Amazon開発者コンソール

いよいよAlexaスキルを作成していきます。まずAmazon開発者コンソールにログインします。

上段メニュー「ALEXA」を選択して「Alexa Skills Kit」へ進みます。 f:id:WorldWorldWorld:20180117222525p:plain

右上の「新しいスキルを追加する」をクリックします。(下の画面には作成済みのスキル一覧が表示されていますが、初めての場合は何もありません。) f:id:WorldWorldWorld:20180117222856p:plain

スキルの基本情報として「言語」「スキル名」「呼び出し名」を入力して保存、次へ進みます。

  • 言語:スキルの対応言語。
  • スキル名:このスキルの名前。ユーザーにはスキル名が表示される。
  • 呼び出し名:スキルを呼び出す時のフレーズ。「アレクサ、◯◯を開いて」の「◯◯」の部分。
    f:id:WorldWorldWorld:20180117223841p:plain

続いて対話モデルを設定します。ここがAlexaスキル開発のポイントです。インテントスキーマ「サンプル発話」を設定します。
f:id:WorldWorldWorld:20180117225410p:plain

インテントスキーマ

インテントスキーマ「Alexaスキルで実行するアクションの種類」です。JSON形式で以下のように入力します。

{
  "intents": [
    {
      "intent": "GoOutTweet"
    },
    {
      "intent": "ComeHomeTweet"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}
  • GoOutTweet:「行ってきます」に対応するアクション。
  • ComeHomeTweet:「ただいま」に対応するアクション。
  • AMAZON.HelpIntent:「使い方」や「説明」に対応するアクション。
  • AMAZON.StopIntent:「中止」「停止」に対応するアクション。
  • AMAZON.Cancelntent:「キャンセル」に対応するアクション。

AMAZON.~」と付いているインテントは、標準で用意されているこれを「標準インテントです。インテントスキーマを定義するだけで使えるようになります。

一方で「GoOutTweet」「ComeHomeTweet」は今回作成するAlexaスキルのオリジナルアクションである「カスタムインテントです。カスタムインテントは次に説明するサンプル発話でキーフレーズを設定する必要があります。

サンプル発話

サンプル発話ではカスタムインテントに対して「そのインテントがアクションを起こすためのキーフレーズ」を定義します。例えば「行ってきます」っぽいことを言えばGoOutTweetが、「ただいま」っぽいことを言えばComeHomeTweetが起動するイメージです。

今回はGoOutTweet、ComeHomeTweetに対して以下のように発話フレーズを設定します。

GoOutTweet 行く
GoOutTweet 行って
GoOutTweet 行きます
GoOutTweet 出かける
ComeHomeTweet ただいま
ComeHomeTweet 帰った
ComeHomeTweet 帰って
ComeHomeTweet 戻った

入力が完了したら次に進みます。

AWS Lambda

ここで一度Amazon開発者コンソールを離れ、AWSにログインします。サーバーレスでプログラムを実行することができるAWS Lambda』を通して、Alexaスキルを起動する仕組みです。

ログインしたらAWSサービスからLambdaを選択します。 f:id:WorldWorldWorld:20180118194022p:plain

右上の「関数の作成」を実行します。
f:id:WorldWorldWorld:20180118220501p:plain

作成フォームで「一から作成」を選択します。「名前」は任意の名前を入力し、「ランタイム」はNode.js 6.10を選択します。 f:id:WorldWorldWorld:20180118194140p:plain

続いてロールの項目から「カスタムロールの作成」を選択します。 f:id:WorldWorldWorld:20180118194521p:plain

ロールの作成画面が表示されます。全てそのままで「許可」をクリックします。 f:id:WorldWorldWorld:20180118194556p:plain

ここまでできたら右下の「関数の作成」をクリック。


実際に動かすプログラムを直接コーディングすることもできますが、今回はzipファイルをアップロードする形式を取ります。zipファイルとコードはgithub上にアップロードしています。 github.com

画面の中頃に「関数コード」という項目があるので、ここでコードエントリータイプ「.ZIPファイルをアップロード」を選択して、ファイル「skill.zip」をアップロードします。 f:id:WorldWorldWorld:20180118194758p:plain

f:id:WorldWorldWorld:20180118221455p:plain

プログラムの中身

skill.zipの内容について少しだけ触れておきます。index.jsがメインの実行プログラムです。Amazon開発者コンソールで定義したインテントごとにAlexaの振る舞いをコーディングします。

'use strict';

const Alexa = require('alexa-sdk');

const Twitter = require('twitter');

const APP_ID = undefined;  // TODO replace with your app ID (OPTIONAL).

const JSTOffset = 60 * 9 * 60 * 1000; // JST時間を求めるためのオフセット

const ErrorMessage = 'ごめんなさい、つぶやけませんでした。';

function calculateJSTTime() {
    var localdt = new Date(); // 実行サーバのローカル時間
    var jsttime = localdt.getTime() + (localdt.getTimezoneOffset() * 60 * 1000) + JSTOffset;
    var dt = new Date(jsttime);
    return dt;
}

const handlers = {
    'LaunchRequest': function () {
        this.emit('AMAZON.HelpIntent');
    },
    'GoOutTweet' : function() {
        var dt = calculateJSTTime();
        var stringTime = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日 " + dt.getHours() + "時" + dt.getMinutes() + "分" + dt.getSeconds() + "秒 ";
        var message = stringTime + "に出かけました。(Alexaスキルでつぶやいています)";
        Twitter.postTweet(message).then(()=>{
            this.emit(':tell','つぶやきました。いってらっしゃい。');
        },(error)=>{
            console.log(error);
            this.emit(':tell',ErrorMessage);
        })
    },
    'ComeHomeTweet' : function() {
        var dt = calculateJSTTime();
        var stringTime = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日 " + dt.getHours() + "時" + dt.getMinutes() + "分" + dt.getSeconds() + "秒 ";
        var message = stringTime + "に帰ってきました。(Alexaスキルでつぶやいています)";
        Twitter.postTweet(message).then(()=>{
            this.emit(':tell','つぶやきました。おかえりなさい。');
        },(error)=>{
            console.log(error);
            this.emit(':tell',ErrorMessage);
        })
    },
    'AMAZON.HelpIntent': function () {
        const speechOutput = '「いってきます」「ただいま」、とつぶやくと、その時間をツイートします。';
        const reprompt = speechOutput;
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'AMAZON.StopIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
};

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.registerHandlers(handlers);
    alexa.execute();
};

Alexa.handlerオブジェクトのthis.emitを実行することで、Alexaが喋ります。

ツイッターに投稿するTwitter.postTweetの部分は同梱のtwitter.jsに自作のOAuthプログラムを実装しています。

"use strict";

const AWS = require('aws-sdk');

const https = require('https');
const request = require('request');
const crypto = require('crypto');

const url='https://api.twitter.com/1.1/statuses/update.json';

function postTweet(message) {
    return new Promise((resolve,reject) => {
        const include_entities = {
            status: message,
            // include_entities: true
        };
        const params = {
            oauth_consumer_key: process.env.CONSUMER_KEY,
            oauth_token: process.env.ACCESS_TOKEN,
            oauth_signature_method: 'HMAC-SHA1',
            oauth_timestamp: (() => {
                const date = new Date();
                return Math.floor(date.getTime() / 1000);
            })(),
            oauth_nonce: (() => {
                const date = new Date();
                return date.getTime();
            })(),
            oauth_version: '1.0'
        };

        let auth_params = Object.assign(include_entities,params);

        let encoded_auth_params = Object.keys(auth_params).map(function(key){
            return `${encodeURIComponent(key)}=${encodeURIComponent(this[key])}`;
        },auth_params);
        encoded_auth_params.sort((a,b) => {
            if(a < b) return -1;
            if(a > b) return 1;
            return 0;
        });

        const sigunature_base = `${encodeURIComponent('POST')}&${encodeURIComponent(url)}&${encodeURIComponent(encoded_auth_params.join('&'))}`;

        const keyOfSign = `${encodeURIComponent(process.env.CONSUMER_SECRET)}&${encodeURIComponent(process.env.ACCESS_TOKEN_SECRET)}`;
        const signature = crypto.createHmac('sha1',keyOfSign).update(sigunature_base).digest('base64');
        params.oauth_signature = signature;

        let authorization = Object.keys(params).map(function(key) {
            return `${encodeURIComponent(key)}="${encodeURIComponent(this[key])}"`;
        },params);
        authorization.sort((a,b) => {
            if(a < b) return -1;
            if(a > b) return 1;
            return 0;
        });


        const headers = {
            Authorization: `OAuth ${authorization.join(', ')}`
        };

        const options = {
            url: url+"?status="+encodeURIComponent(message),
            headers: headers
        };

        request.post(options ,(error,response,body) => {
            if(error) {
                reject("reject:"+error);
            } else if(response.statusCode!=200) {
                reject("reject:statusCode="+response.statusCode);
            } else {
                resolve();
            }
        });
    });
};

module.exports.postTweet=postTweet;

const AWS = require('aws-sdk');を実行することでprocess.env.CONSUMER_KEYという形式で、後述するAWS lambdaに設定した環境変数を利用することができます。


続いて環境変数を設定します。「CONSUMER_KEY」「CONSUMER_SECRET」「ACCESS_TOKEN」「ACCESS_TOKEN_SECRET」をキーにとして、最初にTwitterApplicationManagementで取得したそれぞれの値を設定します。
f:id:WorldWorldWorld:20180118221830p:plain

次に画面上段のDesignerの「トリガーの追加」から「Alexa Skills Kit」を選択します。 f:id:WorldWorldWorld:20180118200556p:plain

画面最下部に「トリガーの追加」メニューが表示されるので「追加」を実行します。 f:id:WorldWorldWorld:20180118200604p:plain

f:id:WorldWorldWorld:20180118200607p:plain

最後に画面右上の「保存」を実行します。 f:id:WorldWorldWorld:20180118200707p:plain


ここまでで関数の作成は完了です。正常に関数が動作するかをテストします。

「テスト」をクリックすると子画面が表示されるので、「新しいイベントの作成」を選びイベントテンプレート「Alexa Start Session」を選択します。 f:id:WorldWorldWorld:20180118200840p:plain

f:id:WorldWorldWorld:20180118200843p:plain

すると何やらコードが表示されます。イベント名に適当な名前を入力し「作成」を実行します。 f:id:WorldWorldWorld:20180118201018p:plain

画面に戻り再び「テスト」を実行して「実行結果:成功」と表示されれば関数のテストは完了です。 f:id:WorldWorldWorld:20180118201139p:plain

最後に画面右上に表示された「ARN」をコピーしておきます。この後の設定に利用します。
f:id:WorldWorldWorld:20180118202520p:plain

Amazon開発者コンソールで仕上げとテスト

関数の作成が終わったらAmazon開発者コンソールに戻ります。

設定メニューの項目「エンドポイント」でAWS LambdaのARN(Amazonリソースネーム)」を選択します。すると、下の方に「デフォルト」というフォームが表示されるので、先ほどコピーしたARNを貼り付けて次へ進みます。 f:id:WorldWorldWorld:20180118202603p:plain

テスト画面が表示されるので、ここで最終的な動作確認を行います。この画面では作成したAlexaスキルが起動した状態になっているため、発話サンプルで設定したキーフレーズを入力します。

フォームに「ただいま」と入力し、「ツイログを呼び出す」を実行します。

すると左側にはAlexaへ送信した入力情報、右側にはAlexaが情報を受け取りAWS Lambdaで実行された関数の結果が表示されます。右側の「outputSpeech」がAlexaが喋る内容です。ここに想定通りのセリフが表示されていればOKです!
f:id:WorldWorldWorld:20180118234000p:plain

AmazonEchoに導入

ここまででAlexaスキルの作成は全て完了です。最後は実機へ導入します。

次の画面へ進むと公開情報の設定になります。今回は本当に公開するわけではないので、適当に入力します。
f:id:WorldWorldWorld:20180118203311p:plain

f:id:WorldWorldWorld:20180118203315p:plain

f:id:WorldWorldWorld:20180118203339p:plain

最後にプライバシーとコンプライアンスの設定です。とりあえず「いいえ」「輸出コンプライアンスにチェックすればOKです。
f:id:WorldWorldWorld:20180118203559p:plain

保存を実行すると画面左側のメニューから「スキルのベータテストがクリックできるようになります。
f:id:WorldWorldWorld:20180118203654p:plain

「テスターのメールアドレス」に自分のアドレスを入力して追加します。他にも使ってもらいたい人がいれば最大500件まで登録できます。

最後に「テストを開始」を実行します。
f:id:WorldWorldWorld:20180118203833p:plain

自分のアドレスにメールが届くので、その中のリンクをクリックします。なお日本語環境では2番目の「JP customer~」リンクを選択します。
f:id:WorldWorldWorld:20180118203922p:plain

Alexaスキルのページ(アプリ画面)が表示されるので「スキルテスト」でスキルを有効化します。
f:id:WorldWorldWorld:20180118204001p:plain

f:id:WorldWorldWorld:20180118204050p:plain

これで全て完了です!実際にAlexaに呼びかけてみましょう!

Alexaスキル開発、骨は折れるが楽しいぞ!

以上、自作のAlexaスキルを開発して実機に導入するところまでを紹介しました。

Amazon DeveloperコンソールやAWS、必要に応じてアプリの認証も必要なので「手軽にできる!!」ものではありません。

しかしながら自分で作成したプログラムでAlexaが喋ってくれるは、なかなかに達成感があります。

暇な人、ガジェット好きな人、ぜひあなたのAlexaにオリジナルのスキルを装備させてみてください!

Amazon Echo (Newモデル)、チャコール (ファブリック)

Amazon Echo (Newモデル)、チャコール (ファブリック)