Amazonのアレクサで自分用の「ゴミの日」スキルを作ってみた件

できました!

というわけで、前回、ゴミの日を教えてくれるAmazonのAlexa(アレクサ)のスキルを作ってみようと思い立ったのです。

慣れないJavaScriptにいろいろ悩むところはありましたけど、どうにか完成しました。

問い合わせすると、音声と画面で教えてくれる感じです。

詳しい様子は撮影して動画にしてみました。

しかも勢い余りまして、夏ということでホラー仕立てにしてしまいました。

よろしければご覧ください。

なんだこれは

勢い余って作ったホラー部分(BGMも後付けです)はどうでもいいんですけど、とにかく自分が望むゴミの日を教えてくれるスキルができました。

なお、動画内のゴミの収集日は架空のものですし、このスキルは一般公開していなくてあくまでも自分用のものです。

とにかく、こんな感じで自分だけが使うスキルであれば割とお手軽に作れるので遊ぶと面白いですよ。 

ソースコード

せっかくなので、自分用のバックアップもかねて、できあがったソースコードをここに載せておきたいと思います。(ブログに書くには長すぎるような気はしますが)

チュートリアルをベースにして改造しているので無駄な部分も多く、JavaScriptの書き方もまだよくわかっていないので決してエレガンスなプログラムではないと思いますけど動いたには動いたので……。

なお、チュートリアルの状態から変更が入っているソースコードはたぶん2つなので、スキル開発の画面までいったら以下の2つのコードの部分をそっくり置き換えれば、さっきの動画と同じ動作をするスキルができあがるのではないかと思います。

ビルド画面の「JSONエディター」のコード

このコードは、開発画面(ブラウザ上)で、どういう言葉を受け付けるかを入力していくと勝手にできあがるコードです。

自分で作る必要はないのです。 

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ゴミの日",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "RuleIntent",
                    "slots": [
                        {
                            "name": "gomi",
                            "type": "GOMI"
                        }
                    ],
                    "samples": [
                        "ゴミの曜日は",
                        "ゴミの曜日を教えて",
                        "{gomi} は何曜日",
                        "{gomi} の曜日を教えて",
                        "ルールを教えて",
                        "{gomi} の曜日は",
                        "ルールは"
                    ]
                },
                {
                    "name": "DayIntent",
                    "slots": [
                        {
                            "name": "date",
                            "type": "AMAZON.DATE"
                        }
                    ],
                    "samples": [
                        "{date} を教えて",
                        "{date} のゴミは",
                        "{date} のゴミ",
                        "{date} は何の日か教えて",
                        "{date} は何の日",
                        "{date} は何",
                        "{date} は",
                        "{date} のゴミを教えて"
                    ]
                },
                {
                    "name": "NextIntent",
                    "slots": [
                        {
                            "name": "gomi",
                            "type": "GOMI"
                        }
                    ],
                    "samples": [
                        "今度の {gomi} の日を教えて",
                        "今度の {gomi} の日は",
                        "今度の {gomi} はいつ",
                        "今度の {gomi} は",
                        "{gomi} はいつ",
                        "{gomi} は",
                        "{gomi} の日を教えて",
                        "{gomi} の日は",
                        "次の {gomi} の日は",
                        "次の {gomi} は"
                    ]
                },
                {
                    "name": "AllNextIntent",
                    "slots": [],
                    "samples": [
                        "今度の収集はいつ",
                        "次の収集はいつ",
                        "次の一覧を教えて",
                        "今度の一覧を教えて",
                        "次の一覧は",
                        "今度の一覧は",
                        "全ての次の日を教えて",
                        "今度は",
                        "今度の収集日を教えて",
                        "今度の日を教えて",
                        "次の収集日を教えて",
                        "次の日を教えて",
                        "今度の日は",
                        "今度のゴミの日は",
                        "次の日は",
                        "全ての次の日は",
                        "次のゴミの日は",
                        "次の収集日は",
                        "次は",
                        "収集日は",
                        "収集日一覧は"
                    ]
                },
                {
                    "name": "AMAZON.MoreIntent",
                    "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": []
                }
            ],
            "types": [
                {
                    "name": "GOMI",
                    "values": [
                        {
                            "id": "3",
                            "name": {
                                "value": "プラスチック"
                            }
                        },
                        {
                            "id": "2",
                            "name": {
                                "value": "空き缶"
                            }
                        },
                        {
                            "id": "1",
                            "name": {
                                "value": "不燃ごみ",
                                "synonyms": [
                                    "不燃"
                                ]
                            }
                        },
                        {
                            "id": "0",
                            "name": {
                                "value": "可燃ごみ",
                                "synonyms": [
                                    "可燃"
                                ]
                            }
                        }
                    ]
                }
            ]
        }
    }
}

コードエディタ画面のindex.js

こちらは作りたいスキルを実現するために自分で書いたほうです。(半分くらいはチュートリアルで出来上がっていたものがそのまま残っていると思います。)

今回は「次にやってくる第2金曜日は何日か」といった日付の計算をするようにしたので、そこがちょっと長くなったと思います。 

// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
// session persistence, api calls, and more.
const Alexa = require('ask-sdk-core');

// 曜日コード
var youbiCD = ['日','月','火','水','木','金','土'];

// ゴミ種類コード
// ゴミ種類名はスロットと一致させること。
var gomiCD = {0:'可燃ごみ', 1:'不燃ごみ', 2:'空き缶', 3:'プラスチック'};

// 収集日判定関数
// 日付とゴミ種類コードより、収集日かどうかをBooleanで返す。
function syusyubiHantei(date, gomiCDkey){
    switch(Number(gomiCDkey)){
        case 0:
            // 可燃ごみ 月曜か木曜
            if(date.getDay() === 1 || date.getDay() === 4){
                return true;
            }
            return false;
        case 1:
            // 不燃ごみ 火曜か木曜
            if(date.getDay() === 2 || date.getDay() === 4){
                return true;
            }
            return false;
        case 2:
            // 空き缶 第2第4金曜
            var c2a = daiYoubi(date.getFullYear(), date.getMonth(), 2, 5); // 第2金曜
            var c2b = daiYoubi(date.getFullYear(), date.getMonth(), 4, 5); // 第4金曜
            if((date.getFullYear() === c2a.getFullYear() && date.getMonth() === c2a.getMonth() && date.getDate() === c2a.getDate()) ||
               (date.getFullYear() === c2b.getFullYear() && date.getMonth() === c2b.getMonth() && date.getDate() === c2b.getDate())){
                return true;
            }
            return false;
        case 3:
            // プラスチック 第1第3火曜
            var c3a = daiYoubi(date.getFullYear(), date.getMonth(), 1, 2); // 第1火曜
            var c3b = daiYoubi(date.getFullYear(), date.getMonth(), 3, 2); // 第3火曜
            if((date.getFullYear() === c3a.getFullYear() && date.getMonth() === c3a.getMonth() && date.getDate() === c3a.getDate()) ||
               (date.getFullYear() === c3b.getFullYear() && date.getMonth() === c3b.getMonth() && date.getDate() === c3b.getDate())){
                return true;
            }
            return false;
    }
    // Error
    console.log('syusyubiHantei()でエラー')
    return false; 
}

// 第x曜日返却関数
// 年月から第i j曜日(第2土曜日など)の日付(Date型)を返す。
function daiYoubi(ay, am, ai, aj){
    var y = Number(ay);
    var m = Number(am);
    var i = Number(ai);
    var j = Number(aj);
    
    var date;
    var w = 0;
    for(var d = 1; d <= 31; d++){
        date = new Date(y, m, d);
        if(date.getDay() === j){
            w++;
            if(i === w){
                return date;
            }
        }
    }
    // Error
    console.log('daiYoubi()でエラー');
    return;
}

// 次のゴミの日のテキスト返却関数(spc:スピーチ用、dsp:表示用)
function getNextGomiText(gomiCDkey){
    // 今日の日付を取得する。UTC+9の調整をする。また、日付変数にコピーする。
    var today = new Date(); 
    today.setHours(today.getHours() + 9);
    var hiduke = new Date(today.getTime());

    // 1日ずつチェックする(日付が決まったらループ終了)
    do{
        hiduke.setDate(hiduke.getDate() + 1); //1日進める
    }while (! syusyubiHantei(hiduke, gomiCDkey));
    
    // 残り日数を算出
    var nokoriDate = (hiduke.getTime() - today.getTime())/(1000*60*60*24)

    // 応答文の頭作成
    var speechText = `次の${gomiCD[gomiCDkey]}の日は、${hiduke.getDate()}日 ${youbiCD[hiduke.getDay()]}曜日です。`;
    var displayText = `<font size="2">・${gomiCD[gomiCDkey]}<br/>&#160;&#160;&#160;&#160;${hiduke.getDate()}日 ${youbiCD[hiduke.getDay()]}曜日</font>`;

    // 応答文残り日数作成
    displayText += '<font size="2">';
    switch (nokoriDate) {
        case 1:
            speechText += 'あしたです。';
            displayText += '(あした)';
            break;
        case 2:
            speechText += 'あさってです。';
            displayText += '(あさって)';
            break;
        default:
            speechText += `あと${nokoriDate}日です。`;
            displayText += `(${nokoriDate}日後)`;
    }    
    displayText += '</font>';
    
    return {'spc': speechText, 'dsp': displayText};
}

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speechText = '「ゴミの日」へようこそ';
        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};

// 指定した日付に該当するゴミ種類を応答する
const DayIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'DayIntent';
    },
    handle(handlerInput) {
        // slotからdateを取得
        var dateValue = handlerInput.requestEnvelope.request.intent.slots.date.value;
        
        // dateValueを分解、Date型へ
        var year = Number(dateValue.substr(0,4));
        var month = Number(dateValue.substr(5,2))-1;
        var day = Number(dateValue.substr(8,2));
        var date = new Date(year, month, day);
        
        // dateは日付かチェックして分岐(週とかが指定されている場合があるので)
        //var tMonth = targetDate.getMonth();
        if(month === date.getMonth()){
            // 日付が入ってきている
            //const speechText = `${day}`;
            // 収集日チェックしながら応答文作成
            var speechText = '';
            var displayText1 = '';
            var displayText2 = '';
            for (var g in gomiCD){
                if(syusyubiHantei(date, g)){
                    if(speechText.length > 0){
                        speechText += 'と、';
                        displayText2 += '<br/>';
                    }
                    speechText += gomiCD[g];
                    displayText2 += '・' + gomiCD[g];
                }
            }
            
            // 応答文作成
            if(speechText.length === 0){
                // 収集日なし
                speechText = '収集日ではありません。'
                displayText2 = '収集日では<br/>ありません'
            }else{
                // 収集日あり
                speechText += 'の日です。'
            }
            
            // 応答文の完成
            speechText = `${day}日 ${youbiCD[date.getDay()]}曜日は` + speechText;
            displayText1 = `${day}日 ${youbiCD[date.getDay()]}曜日`;
            
            //var speechText = syusyubiHantei(date,1);
            return handlerInput.responseBuilder
                .speak(speechText)
                .addRenderTemplateDirective({
                    type: 'BodyTemplate1',
                    title: displayText1,
                    textContent: {
                        primaryText: {
                            type: 'RichText',
                            text: displayText2
                        }
                    }
                })
                //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
                .getResponse();
        }else{
            // 日付でないものが入ってきている
            const speechText = '日付を指定してください';
            return handlerInput.responseBuilder
                .speak(speechText)
                .reprompt(speechText)
                .getResponse();
        }
    }
};

// 指定されたゴミ種類の次の収集日を応答する
const NextIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'NextIntent';
    },
    handle(handlerInput) {
        // slotからgomiのidを取得
        var gomiID = handlerInput.requestEnvelope.request.intent.slots.gomi.resolutions["resolutionsPerAuthority"][0]["values"][0]["value"]["id"];
        
        // 次のゴミの日の応答文を取得
        var retText = getNextGomiText(gomiID);
        
        return handlerInput.responseBuilder
            .speak(retText.spc)
            .addRenderTemplateDirective({
                type: 'BodyTemplate1',
                title: '次の収集日',
                textContent: {
                    primaryText: {
                        type: 'RichText',
                        text: retText.dsp
                    }
                }
            })
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

// すべてのゴミ種類の次の収集日を応答する
const AllNextIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'AllNextIntent';
    },
    handle(handlerInput) {
        var funcRet;
        var speechText = '次の収集日をお知らせします。';
        var displayText = '';
        
        // すべてのゴミの次回の日の応答文を連結
        for (var g in gomiCD){
            funcRet = getNextGomiText(g);
            // スピーチテキスト結合
            speechText += funcRet.spc;
            // 表示テキスト結合
            if(displayText.length > 0){
                displayText += '<br/>';
            }
            displayText += funcRet.dsp;
        }
        
        //debug
        speechText += '。。あなたは、、3日後です。';
        displayText += '<br/><br/><font size="2">・あなた<br/>&#160;&#160;&#160;&#160;V虞ヌ日 テU駆曜日(3日後)</font>'

        return handlerInput.responseBuilder
            .speak(speechText)
            .addRenderTemplateDirective({
                type: 'BodyTemplate1',
                title: '次の収集日 一覧',
                textContent: {
                    primaryText: {
                        type: 'RichText',
                        text: displayText
                    }
                }
            })
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

// ゴミ収集ルール(曜日)を読み上げる
const RuleIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'RuleIntent';
    },
    handle(handlerInput) {
        const speechText = '収集日をお知らせします。可燃ごみは月曜と木曜、不燃ごみは火曜と木曜、空き缶は第二第四金曜、プラスチックは第一第三火曜です。';
        return handlerInput.responseBuilder
            .speak(speechText)
            /* リスト形式で表示しようとしたがはみ出るのでボツ
            .addRenderTemplateDirective({
                type: 'ListTemplate1',
                backButton: 'HIDDEN',
                listItems:  [
                    {
                        token: '0',
                        textContent: {
                            primaryText: {
                                type: 'RichText',
                                text: '可燃ごみ:月曜と木曜'
                            }
                        }
                    },
                    {
                        token: '1',
                        textContent: {
                            secondaryText: {
                                type: 'RichText',
                                text: '不燃ごみ:火曜と木曜'
                            }
                        }
                    }
                ],
                title: '収集日'
            })
            */
            .addRenderTemplateDirective({
                type: 'BodyTemplate1',
                title: '収集日一覧',
                textContent: {
                    secondaryText: {
                        type: 'RichText',
                        text: '<font size="2">・可燃ごみ:月,木<br/>・不燃ごみ:火,木<br/>・空き缶:第2/第4金<br/>・プラスチック:第1/第3火</font>'
                    }
                }
            })
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .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');
    },
    handle(handlerInput) {
        const speechText = 'またね';
        return handlerInput.responseBuilder
            .speak(speechText)
            .getResponse();
    }
};
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};

// The intent reflector is used for interaction model testing and debugging.
// It will simply repeat the intent the user said. You can create custom handlers
// for your intents by defining them above, then also adding them to the request
// handler chain below.
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = handlerInput.requestEnvelope.request.intent.name;
        const speechText = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speechText)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.message}`);
        const speechText = `ちょっと何言っているかわからない。もう一度話してください。`;

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

// This handler acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        DayIntentHandler,
        NextIntentHandler,
        AllNextIntentHandler,
        RuleIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    .addErrorHandlers(
        ErrorHandler)
    .lambda();

いろいろやろうとしたので長くなりましたけど、欲を出さなければもっと簡単に作れるはずです。

とにかく前回書いた記事にある、チュートリアルのページを見ると簡単さがわかると思います。

あと、関係ないですけどブログにプログラムのソースコードを書くとプログラマーみたいですね。

そういえば私、学生のころプログラマーになりたいと思ったこともあったのを思い出しました。

プログラマーの道に進んでいたとしたら今頃は何やっていたんだろうか。(アレクサとは全く関係ない感想)

まとめ

というわけで、無料で、そして比較的簡単に自分で使うAlexaスキルを作ることができました。

「こういうのあればいいな」と思った簡単なものがあればまた作ってみたいものです。

あと、Google(OK, Google)の方も簡単ならば作ってみたいなと思いました。

おわり。

コメント

タイトルとURLをコピーしました