今まで実家に住んでいたのですが、去年の12月に目黒区へ引越ししました。

そこで問題になったのがゴミの日がよくわからんということです。

なので Amazon Echo Dot (第3世代) に教えてもらうことにしました。

Amazon Echo Dot

実家にいた頃も Amazon Echo を所有していたのですが、それは親が主に音楽を聴くのに使用していたので置いていきました。

そこで新しく買ったのが Amazon Echo Dot です。第3世代は前の世代より少し大きくなったみたいですが、その分音質が良くなったみたいです。大きくなったと言っても手のひらサイズで十分小さいです。

開発環境

Alexa Skills Kit (ASK) SDK for Node.js のバージョン2を使用しました。Alexa Skills について前回書いた記事は1年以上前ですが、当時より大分状況が異なります。

SDK のバージョンが2に上がり、全然別物になりました。Alexa デベロッパーコンソールも随分見違えました。今はコンソールとエディタが一体化しており、試しにやってみる敷居がかなり下がったように思います。

従来通り複雑な処置を行うには Lambda 関数とのコンソール外での連携が必要ですが、簡単な処理であればコンソールに一体化されたエディタで充分です。

----------2019-03-06-1.05.00

動作イメージ

デベロッパーコンソール上から Alexa に文字や音声で呼びかけることができます。これも以前より大分わかりやすくなっています。

demp

コード

例によってサンプルスキルに継ぎ足したものです。見辛いですがなんとか…。

  • JST にするために無理やり9時間足してます。
  • スキルの呼び出しのみで何ゴミか教えて欲しかったので、呼び出し時にインテント (スキルの呼び出し名の後に続くサブコマンド的なもの) の入力を待たずに返答するようにしています。
    • 呼び出し名のみの場合、使い方を説明する等の間を用意するのが通例。
  • ヘルプやキャンセル等のインテントは考慮していない。
// 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 date = new Date();
date.setTime(date.getTime() + 1000*60*60*9); // JST

const dayOfWeek = date.getDay();	
const dayOfWeekStr = [ "日", "月", "火", "水", "木", "金", "土" ][dayOfWeek];

const GarbageCollectionRule = function() {
    if(dayOfWeek === 0 || dayOfWeek === 1 || dayOfWeek === 5) {
        return '今日は' + dayOfWeekStr + '曜日なのでゴミの回収はありません。';
    } else if(dayOfWeek === 2) {
        return '今日は' + dayOfWeekStr + '曜日なので資源ごみです。';
    } else if(dayOfWeek === 3 || dayOfWeek === 6) {
        return '今日は' + dayOfWeekStr + '曜日なので燃えるごみです。';
    } else if(dayOfWeek === 4) {
        return '今日は' + dayOfWeekStr + '曜日です。第一は回収なし、第二、第四は不燃ゴミ、第三は古着、古布、第五は回収なしです。';
    } else {
        return 'エラーです。';
    }
};
const msg = GarbageCollectionRule();

const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
        //const speechText = 'ようこそ。「何ゴミか教えて」と言ってみてください。';
        //return handlerInput.responseBuilder
        //    .speak(speechText)
        //    .reprompt(speechText) //インテント待ちになる
        //    .getResponse();

        // インテント待たないで、直接答えを返すようにする
        const speechText = 'ようこそ。' + msg;
        return handlerInput.responseBuilder
            .speak(speechText)
            .getResponse();
    }
};
const GarbageCollectionIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'GarbageCollectionIntent';
    },
    handle(handlerInput) {
        const speechText = msg;
        return handlerInput.responseBuilder
            .speak(speechText)
            //.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,
        GarbageCollectionIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    .addErrorHandlers(
        ErrorHandler)
    .lambda();

ハマりどころ

自分を信じるな

前回書いた記事でもぼやいていましたが、どうにも呼びかけ周りが難しいです。ゼロから設定したものと、サンプルスキルを元に書き換えたもので、最終的な成果物が同じにも関わらず、ゼロから設定したものは呼びかけても認識されず、サンプルを書き換えたものはうまく動きます。

感覚的でモヤモヤしますが、どうやらまず確実に動くサンプルスキルを作成して、そこから改造していくやり方が良いみたいです…。自分のやり方が何かおかしいのかもしれませんが…。

開発者ポータルの罠

開発者ポータルの日本語ページを開き、自然な流れで登録を行うと、以下のハマりポイントに引っかかります。

まあ .com の方でデベロッパーアカウントを作成してもスキルの作成に問題は無いのですが、Amazon Echo 実機を .co.jp の方に登録してあると、開発中のスキルが以下のように Amazon Alexa サイト上に表示されてくれません (US アカウントの扱いになるので)。

---------_2019-03-06_1_30_29

私はこの罠を踏んでしまっていたので、JP アカウントを新規に作り、スキルを作り直しました…。

スキルのベータ版を登録することで、ドメイン関係なく開発中スキルを実機にインストールできるのですが、ちょっと面倒。

感想

前よりスキル作成の敷居が下がったので嬉しい。

今の所当日のゴミしか教えてくれないので、明日のゴミも教えてくれるようにしたい。

作りたいスキルは他にもあるのでやっていくぞ!!