鍋での炊飯をサポートするAlexaスキルを作った

Posts

近ごろ、鍋でお米を炊いています。鍋で炊飯すると、炊飯器で炊飯するよりもおいしく感じられる気がします。
おいしさの印といわれる「かに穴」ができた日はうれしくなります(集合恐怖症ではあるけど)。

ボタンひとつで炊飯できる炊飯器と違い、鍋で炊飯する場合はいくつかのステップが必要です。
私の場合は次のフローで炊飯しています。

  1. お米を30分くらい浸水させる
  2. 火にかける
  3. 沸騰したら弱火にして5分待つ
  4. 火をやめて10分蒸らす

ここで問題になるのが、弱火にするときと蒸らすときのタイマーセッティングです。浸水時間は適当なのでタイマーはセットしていません。
お米を炊くたびに、タイマーで5分、10分と設定するのは非常に面倒くさいです。
なので、会話の中でタイマーを設定してくれるAlexaスキル「かまどタイマー」を作りました。

この記事では、作成したAlexaスキルの内容と、スキルを実装する上で難しかったことを記載します。

会話フロー

「かまどタイマー」では、次の流れでAlexaと会話します。

全体のフロー

Alexaの応答の待ち時間は8秒なので、タイムアウトしてしまう

問題

Alexaからの質問に対してユーザーが答えるのに待ってくれる時間は、8秒です。 リプロンプトを入れてもさらに8秒ユーザーからの応答がなければ、スキルは自動的に終了し、今までのセッション情報は破棄されます。

かまどタイマーの場合、「沸騰したら次へと言ってくれ」とAlexaが言ってきた場合には 、ユーザーは8秒以内に「次へ」といわなければなりません。

応答時間は最大8秒

解決策

Alexa Audio Playerを使って音楽を流すと、擬似的にセッションが続いているように見せることができます。

音楽を流す

ただし、実際にはAlexa Audio Playerでの音楽再生はスキル外で行われるため、Alexaスキルのセッションは再生開始すると破棄されます。
そのため、セッションにおける「沸騰を待っている」「弱火の5分を待っている」といった情報が消えてしまいます。

ここで登場するのが、Audio Playerで音楽を流すときに付与できるトークンです。

このTokenに「沸騰を待っている」という情報を付与して音楽を再生します。
「アレクサ、次へ」と言ったタイミングでは、再生しているAudio PlayerのTokenを取得して照合し、「沸騰待ちであるなら弱火のフローに進む」といった処理を行います。

音楽に付与したトークンで前回のセッションの内容を把握する

コード

トークンを付与する方の処理

const YesIntentHandler: RequestHandler = {
  canHandle(handlerInput: HandlerInput): boolean {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name === "AMAZON.YesIntent"
    );
  },
  handle(handlerInput: HandlerInput): Response {
    const speechText = "沸騰したら「アレクサ、次へ」と言ってください。";
    return handlerInput.responseBuilder
      .speak(speechText)
      .addAudioPlayerPlayDirective("REPLACE_ALL", MP3_URL, "沸騰待ち", 0) // 第3引数がトークン
      .getResponse();
  },
};

判定側の処理

const NextIntentHandler: RequestHandler = {
  canHandle(handlerInput: HandlerInput): boolean {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name === "AMAZON.NextIntent"
    );
  },
  async handle(handlerInput: HandlerInput): Promise<Response> {
    if (!handlerInput.requestEnvelope.context.AudioPlayer) {
      return handlerInput.responseBuilder
        .speak("なんかおかしいです")
        .getResponse();
    }
    if (handlerInput.requestEnvelope.context.AudioPlayer.token === "沸騰待ち") {  // ここで判定
      // 沸騰
      try {
        await registerTimer(
          handlerInput,
          5,
          "お知らせです。火を止めてください。火を止めたら「アレクサ、次へ」と言ってください。",
          "弱火タイマー"
        );
      } catch (e) {
        console.log(e);
      } finally {
        const speechText = "弱火タイマー5分をセットしました。";
        return handlerInput.responseBuilder
          .speak(speechText)
          .addAudioPlayerPlayDirective("REPLACE_ALL", MP3_URL, "蒸らし待ち", 0)
          .getResponse();
      }
    }
  // ...
};

参考