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

January 04, 2021

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

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

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

ここで問題になるのが、弱火にするときと蒸らすときのタイマーセッティングです。浸水時間は適当なのでタイマーはセットしていません。 お米を炊くたびに、タイマーで5分、10分と設定するのは非常に面倒くさいです。 なので、会話の中でタイマーを設定してくれる 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();
      }
    }
  // ...
};

参考