LoginSignup
5
4

More than 5 years have passed since last update.

Amazon Echo Spotで発言をSlackに記録&画面に表示するスキルを作成してみた

Last updated at Posted at 2018-08-29

開発経緯

Alexaと遊んでいると適切に発言を聞き取ってもらえないことが多々あり、発言内容を手軽に確認したいと思っていました。

Alexaを使ってSlackにメモを保存するを参考に発言内容をSlackに記録する部分を作成したので、さらにAmazon Echo Spotで画面上で発言内容を確認できるようにしました。

これで意図した発言内容とAlexaの解析結果のズレが簡単に確認できるはずです。

今回は、スマートスピーカー初心者が以下の点を中心にスキル開発の流れを記載しています。

多少見づらい点があるかと思いますが、コメント等でフィードバックをいただけたら嬉しいです。

スキルの流れ

image.png
Lambdaでaxiosを使ってHTTP通信をすることでSlackを叩きます。

スキル作成

画像インターフェースの有効化

Amazon開発者ポータルで「インターフェース」の「画像インターフェース」をONにして再ビルドするだけです。
image.png
有効化するとビルドインインターフェースが沢山増えますが、今回は何も変更せずビルドし直すだけで良いです。

スキーマの作成

ユーザーの自由発話に対応する Alexa Skill を作るを参考に発話を受け取る対話モデルを作成します。
スキル名は「スラックメモ」にしました。
画像インターフェースを有効化したので、上記の記事と比べてインテントが増えています。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "スラックメモ",
            "intents": [
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "MemoIntent",
                    "slots": [
                        {
                            "name": "Memo",
                            "type": "any"
                        }
                    ],
                    "samples": [
                        "{Memo}"
                    ]
                },
                {
                    "name": "AMAZON.MoreIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateSettingsIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NextIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.PageUpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.PageDownIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.PreviousIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ScrollRightIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ScrollDownIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ScrollLeftIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ScrollUpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                }
            ],
            "types": [
                {
                    "name": "any",
                    "values": [
                        {
                            "name": {
                                "value": "ほげほげ"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

AWS lambda関数の作成

言語はNode.jsで実装しました。Slackに記録する部分はAlexaを使ってSlackにメモを保存すると同様に行いました。

環境変数

  • APP_ID
    スキルのID。Amazon開発者ポータルから確認できる。
  • BOT_TOKEN
    Slackのトークン。Slack APIのTokenの取得・場所を参考に取得。
  • CHANNEL
    投稿先のチャンネル名。

ライブラリ

今回、HTTP通信のためにaxiosを使ったので、こちらを参考にLambda関数を「一から作成」しました。
以下のコマンドを叩き、pakage.jsonを作成します。

npm init
npm install --save alexa-sdk axios

そして、下記のindex.jsを作成し、zipで圧縮します。
そのzipを関数コードとしてアップロードしました。

index.js
"use strict";
const Alexa = require('alexa-sdk');
const axios = require('axios');

const makePlainText = Alexa.utils.TextUtils.makePlainText;
const makeRichText = Alexa.utils.TextUtils.makeRichText;
const makeImage = Alexa.utils.ImageUtils.makeImage;

var SUCCESS_MESSAGE = "をSlackに送ったよ";
var HELP_MESSAGE = "何かしゃべってね";
var FAILED_MESSAGE = "Slackに送れなかったよ、ごめんね";
var EXPLANATION_MESSAGE = "Slackに記録するよ";

var TITEL = 'Slack Memo';
var LISTEN_IMAGE_URL = 'https://hoge/hoge.png';
var RESULT_IMAGE_URL = 'https://foo/foo.png';

function supportsDisplay() {
    var hasDisplay =
        this.event.context &&
        this.event.context.System &&
        this.event.context.System.device &&
        this.event.context.System.device.supportedInterfaces &&
        this.event.context.System.device.supportedInterfaces.Display
    return hasDisplay;
}

var handlers = {
    'LaunchRequest': function () {
        this.emit('AMAZON.HelpIntent');
    },
    'AMAZON.HelpIntent': function () {
        this.response.speak(HELP_MESSAGE)
            .listen(EXPLANATION_MESSAGE + HELP_MESSAGE);

        if (supportsDisplay.call(this)) {
            const builder = new Alexa.templateBuilders.BodyTemplate1Builder();
            const template = builder.setTitle(TITEL)
                .setBackgroundImage(makeImage(LISTEN_IMAGE_URL))
                .build();
            this.response.renderTemplate(template);
        }
        this.emit(':responseReady');
    },
    'MemoIntent': function () {
        var memo = this.event.request.intent.slots.Memo.value;
        console.log("memo:" + memo);

        if (supportsDisplay.call(this)) {
            const builder = new Alexa.templateBuilders.BodyTemplate1Builder();
            const template = builder.setTitle(TITEL)
                .setBackgroundImage(makeImage(RESULT_IMAGE_URL))
                .setTextContent(makeRichText('' + memo + ''), null, null)
                .build();
            this.response.renderTemplate(template);
        }

        if (!memo) {
            this.response.speak(FAILED_MESSAGE);
            this.emit(':responseReady');
            return;
        }

        axios.get(
            'https://slack.com/api/chat.postMessage',
            { 
                params:{
                    token: process.env.BOT_TOKEN,
                    channel: process.env.CHANNEL,
                    text: memo
                }
            })
            .then(res => {
                this.response.speak(memo + SUCCESS_MESSAGE);
                this.emit(':responseReady');
            })
            .catch(err => {
                this.response.speak(FAILED_MESSAGE);
                this.emit(':responseReady');
            });
    }
};

exports.handler = function (event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.appId = process.env.APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

今回は発言内容を画面上に表示したいので、ディスプレイディレクティブを作成しました。
日本語版 Echo Spot に対応したスキルの作り方を参考にコードの説明を少し記載します。

ディスプレイ付きデバイスかどうか判断する

下記の関数で、ユーザーのデバイスがディスプレイがあるか判断しています。

function supportsDisplay() {
    var hasDisplay =
        this.event.context &&
        this.event.context.System &&
        this.event.context.System.device &&
        this.event.context.System.device.supportedInterfaces &&
        this.event.context.System.device.supportedInterfaces.Display
    return hasDisplay;
}

Amazon開発者ポータルの「テスト」から先ほど作成したスキルを「Alexaシュミレータ」でテストすると分かるのですが、画像インターフェースの有効化を有効化にすると「スキルI/O」の「JSON入力」にあるsupportedInterfacesノードの下にDisplayノードが新たに作成されていることが分かります。
image.png

よって、上記の関数ではDisplayノードまでの各ノードに値が入っていたらtrueが返るようになっているため、デバイスの画面の有無を確認できます。

ディスプレイディレクティブを作成する

インテントの関数の中でcallメソッドを使って上記のsupportsDisplay関数を呼び出すことで、ディスプレイの有無を確認してからディスプレイディレクティブを作成しています。
こちらにも記載があるようにディスプレイの無いデバイスにDisplay.RenderTemplateディレクティブを送るとエラーが起こり、スキルが上手く動いてくれません。

ディスプレイテンプレートは、「BodyTemplate1」を使いました。
他のテンプレートを知りたい場合は、Displayテンプレートのリファレンスを見ると良いです。

テンプレートにはタイトル、背景の画像、テキストを設定しました。
今回はAlexaが問いかけている時と発言を画面に表示する時で画像を変えたかったので、2回ディスプレイディレクティブを作成しています。

こちらによれば文字列はデフォルトで白になっており、明るい画像を背景にすると文字が見えづらくなるため注意が必要です。

Display.RenderTemplateディレクティブは、Displayインターフェースのリファレンスを参考にしました。

        if (supportsDisplay.call(this)) {
            const builder = new Alexa.templateBuilders.BodyTemplate1Builder();
            const template = builder.setTitle(TITEL)
                .setBackgroundImage(makeImage(RESULT_IMAGE_URL))
                .setTextContent(makeRichText('' + memo + ''), null, null)
                .build();
            this.response.renderTemplate(template);
        }

ハマったところ

  • zipのアップロードが上手くいかない問題
    Windows環境で作業しているのですが、7-Zipの7z a sample.zip *コマンドで圧縮したら上手くいきました。Windowsの標準機能だと上手くいきませんでした。

  • 発言が途中で切れてしまう問題
    最初は下記のコードのように変数に話す内容を入れておいて最後にemitメソッドを叩こうと思ってました。

        axios.get(
            'https://slack.com/api/chat.postMessage',
            { 
                params:{
                    token: process.env.BOT_TOKEN,
                    channel: process.env.CHANNEL,
                    text: memo
                }
            })
            .then(res => {
                console.log("SUCCESS_MESSAGE:" + SUCCESS_MESSAGE);
                speachOutput = memo + SUCCESS_MESSAGE;
            })
            .catch(err => {
                console.log("FAILED_MESSAGE:" + FAILED_MESSAGE);
                speachOutput = FAILED_MESSAGE;
            });

        this.response.speak(speachOutput);
        this.emit(':responseReady');

しかし、これだとmemoの内容のみでspeachOutputの内容を話してくれませんでした。
そこで、下記のコードのようにgetが成功した時失敗した時それぞれでemitメソッドを叩くと上手くいきました。
非同期処理が関係してるのかもしれません。

axios.get(
            'https://slack.com/api/chat.postMessage',
            { 
                params:{
                    token: process.env.BOT_TOKEN,
                    channel: process.env.CHANNEL,
                    text: memo
                }
            })
            .then(res => {
                this.response.speak(memo + SUCCESS_MESSAGE);
                this.emit(':responseReady');
            })
            .catch(err => {
                this.response.speak(FAILED_MESSAGE);
                this.emit(':responseReady');
            });
  • Lambda関数の定数の文字化け問題
    メッセージの定数が文字化けしており、Alexaが上手く喋ってくれませんでした。原因は、index.jsファイルの文字コードが「ANSI」だからでした。「UTF-8」にしたら文字化けしなくなりました。zipでコードを上げるときは文字コードは「UTF-8」にした方が良さそうです。

課題

発言内容がSlackではなく、スプレッドシートに出力されれば解析などにも使えて面白いかなと思ってます。

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4