鍋での炊飯をサポートするAlexaスキルを作った
近ごろ、鍋でお米を炊いています。鍋で炊飯すると、炊飯器で炊飯するよりもおいしく感じられる気がします。
おいしさの印といわれる「かに穴」ができた日はうれしくなります(集合恐怖症ではあるけど)。
ボタンひとつで炊飯できる炊飯器と違い、鍋で炊飯する場合はいくつかのステップが必要です。
私の場合は次のフローで炊飯しています。
- お米を30分くらい浸水させる
- 火にかける
- 沸騰したら弱火にして5分待つ
- 火をやめて10分蒸らす
ここで問題になるのが、弱火にするときと蒸らすときのタイマーセッティングです。浸水時間は適当なのでタイマーはセットしていません。
お米を炊くたびに、タイマーで5分、10分と設定するのは非常に面倒くさいです。
なので、会話の中でタイマーを設定してくれるAlexaスキル「かまどタイマー」を作りました。
この記事では、作成したAlexaスキルの内容と、スキルを実装する上で難しかったことを記載します。
会話フロー
「かまどタイマー」では、次の流れでAlexaと会話します。
Alexaの応答の待ち時間は8秒なので、タイムアウトしてしまう
問題
Alexaからの質問に対してユーザーが答えるのに待ってくれる時間は、8秒です。 リプロンプトを入れてもさらに8秒ユーザーからの応答がなければ、スキルは自動的に終了し、今までのセッション情報は破棄されます。
かまどタイマーの場合、「沸騰したら次へと言ってくれ」とAlexaが言ってきた場合には 、ユーザーは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();
}
}
// ...
};