はかせのラボ

私の頭の中を書いていく雑記ブログです

Alexa Alexaに自発的に喋って欲しかったんだ

あいさつ

どうも、はかせです。
前回Alexa→Unityの連携に成功しました。
hakase0274.hatenablog.com

なので今回はUnity→Alexaの連携に挑戦した話です。

なぜやろうとしたのか

基本Alexaは発話→レスポンスという動きしかしません。
どんなスキルであっても必ずユーザーが
「Alexa○○して」といった風に話しかけて初めて動き出します。

ただUnityひいてはゲームのAIと言ったら
特定の条件下になったら今の状況を教えてくれたり
かるく実況じみた発言をしたりと何かと勝手に喋ります。

やはりUnityでゲーム開発にAlexaを組み込むんだから
こういったことをしてほしいわけです。

どうやってやろうとしたか

基本受け身なAlexaですが、
いくつか能動的に動いてくれる場面があります。
それがこれらです。
・通知
・リマインダー

通知はあった場合黄色に光り
通知の有無を確認したら教えてくれます。

一見よさそうですが確認が必要で、
最終的に受動的になっているので今回は却下です。

次にリマインダーです。
予め予定を入れておいて、
その予定が来たら教えてくれるってやつですね。

これもユーザーが予定入れなきゃいけないので
受動的ではあるんですが、
予定を入れるのはプログラムからできるので
パッと見Alexaが勝手に喋ったように感じます。

これならばうまいこと使えば
Alexaに実況してもらったりできるかもしれません。

リマインダーを使う

Alexaでリマインダーを使うにはAPIを使います。
APISDKがサポートしてるそうなのでそのやり方に従ってやります。
developer.amazon.com

ではやってみましょう。
リファレンスやらGitHubやら見てて
みんなNode.js使ってたんで右にならいます。
(とはいえオンラインのエディタ使ってやるんで環境構築とかはしません)

const RemindIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'RemindIntent';
    },
    // handle の前に async 追加
    async handle(handlerInput) {
        const sec = handlerInput.requestEnvelope.request.intent.slots.sec.value;
        const requestEnvelope = handlerInput.requestEnvelope;
        const responseBuilder = handlerInput.responseBuilder;
        const consentToken = requestEnvelope.context.System.apiAccessToken;

        // check for confirmation.  if not confirmed, delegate
        switch (requestEnvelope.request.intent.confirmationStatus) {
        case 'CONFIRMED':
            // intent is confirmed, so continue
            console.log('Alexa confirmed intent, so clear to create reminder');
            break;
        case 'DENIED':
            // intent was explicitly not confirmed, so skip creating the reminder
            console.log('Alexa disconfirmed the intent; not creating reminder');
            return responseBuilder
                .speak(`わかりました。リマインドしません。`)
                .getResponse();
        case 'NONE':
        default:
            console.log('delegate back to Alexa to get confirmation');
            return responseBuilder
                .addDelegateDirective()
                .getResponse();
        }

        if (!consentToken) {
            return responseBuilder
                .speak(`アレクサアプリにカードを送信しました。リマインダーの権限を有効にしてください。`)
                .withAskForPermissionsConsentCard(PERMISSIONS)
                .getResponse();
        }
        try {
            const client = handlerInput.serviceClientFactory.getReminderManagementServiceClient();
            const reminderRequest = {
                trigger: {
                    type: 'SCHEDULED_RELATIVE',  // 指定秒数後にリマインド
                    offsetInSeconds: sec,
                },
                alertInfo: {
                    spokenInfo: {
                        content: [{
                            locale: 'ja-JP', // 日本国の日本語
                            text: 'お時間です', // スマホに通知する文言
                        }],
                    },
                },
                pushNotification: {
                    status: 'ENABLED',
                },
            };
            const reminderResponse = await client.createReminder(reminderRequest);
            console.log(JSON.stringify(reminderResponse));
        } catch (error) {
            if (error.name !== 'ServiceError') {
                console.log(`error: ${error.stack}`);
                const response = responseBuilder.speak(`おっと。エラーが発生しました。`).getResponse();
                return response;
            }
            throw error;
        }
        return responseBuilder
            .speak(`分かりました。${sec}秒後にリマインドします。`)
            .getResponse();
    },
};

まずハンドラーに来たら必要な値を取得しておきます。
基本はNode.jsでAlexaスキル使うのと大差ないですが、
あまり見ない点としてapiAccessTokenを取得しています。

これはリマインダーAPIを使うときのヘッダー情報に
アクセストークンが必要だからです。

そして一通り値を取得したら次にこんなやつが来ます。

switch (requestEnvelope.request.intent.confirmationStatus)

これはリクエストの確認ですね。
リマインダーAPIを使用する際
当然ながらAlexaの対話モデルの方にインテントを追加するのですが、
このときにインテントの確認を有効にする必要があるそうです。
f:id:hakase0274:20190827212950p:plain

この設定を有効にしたら
Alexaがリクエストを受けた時確認メッセージを出してきます。

上記の一文はその返答を待ち、
返答によって処理を切り分ける役割を持っています。

あとは各種値によって処理を進めていくだけですね。

あとはエラーハンドリングとハンドラーの登録です。

const ErrorHandler = {
  canHandle(handlerInput, error) {
    return error.name === 'ServiceError';
  },
  handle(handlerInput, error) {
    // console.log(`ERROR STATUS: ${error.statusCode}`);
    console.log(`ERROR MESSAGE: ${error.message}`);
    // console.log(`ERROR RESPONSE: ${JSON.stringify(error.response)}`);
    // console.log(`ERROR STACK: ${error.stack}`);
    switch (error.statusCode) {
      case 401:
        return handlerInput.responseBuilder
          .speak(`Alexaアプリのホーム画面で、スキルに権限を付与してください。`)
          .withAskForPermissionsConsentCard(PERMISSIONS)
          .getResponse();
      case 403:
        return handlerInput.responseBuilder
          .speak(`このデバイスではリマインダーを設定することができません。`)
          .getResponse();
      default:
        return handlerInput.responseBuilder
          .speak(`リマインダーの設定でエラーが発生しました。`)
          .getResponse();
    }
  },
};

exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        RemindIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    // 追加
    .withApiClient(new Alexa.DefaultApiClient())
    .lambda();

特別説明することもないですね。
そのまんまです。

さてコーディングは終わりです。
あとはモデルをビルドしつつ
ビルド時間の間にスキルの権限設定をしましょう。

Alexaでリマインダーを使うためには
・開発コンソールからアクセス権限の設定
スマホアプリからのアクセス権限の設定

この二つ両方が必要です。
(私はスマホから設定し忘れてちょい詰まりましたw)

開発コンソールからは下の方に行くと
アクセス権限ってとこがあるのでそこからリマインダーの項目を。

スマホからはアプリ立ち上げて
スキル・ゲームの欄からスキル選んで
そこから設定です。

そして設定しているうちにビルドが終わったので
試してみましょう。

う~~ん?
0点ではないが100点とは程遠い気がするぞ・・・?

あとがき

今回はAlexaでリマインダーAPIを使ってみました。
なんか設定してない文言が追加されていたり、
変に繰り返されたりなんか・・ちょっと・・・って感じでした。

この辺が直せればワンチャンスですが、
Alexaってこういうとこ融通聞かないんでどうですかね。

それでは今回はこの辺でノシ

参考

qiita.com