あらすじ
今から2年前の2018年
「アレクサにアニメの名台詞を言わせたい」
その一心でひたすら、アレクサのスキル開発に躍起になっていたことがありました。
奮闘記は以下参照。。。
再挑戦の夏
ことの始まりは仕事で
「アレクサのスキル申請の方法を身に付けたいらしいから調べといて」
と言われたことがきっかけでした。
さて、久々に開発しようかと思って、
デベロッパーコンソールを開きました。
・・・ログインパスワードなんだっけ???
おお〜〜〜〜懐かし〜〜〜
俺がいろいろ試行錯誤でためしたスキルたち。
どうしてもアレクサに「破道の九十 黒棺」を詠唱させたくて頑張っていたスキルたち。
結局Amazonの中の人からリジェクトたくさんもらって作れなかったよね。
というわけで懲りずに再チャレンジしました。
基本的にスキルの作り方は変わらず
- 呼び出し名を作成
- インテント作成
- スロット作成
- コードエディタで編集
- スキル申請
- リジェクト
- 申請
- リジェクト
- 申請
- 公開!!!!!!!
と行った流れです。
呼び出し名作成
写真のようにスキルの呼び出し名を決めます。
ここは「アレクサ、〇〇を開いて」
の〇〇の部分になります。
インテント作成
インテントはスキルが発動した後にアレクサに話しかけるワードを決めます。
ここでは{person}という変数を使用しています。この変数に決められたワードを設定できるのがスロットになります。
このスロットに何も入れないと何にも反応しないことになります。
スロットの作成
というわけで今回のスロットの中身は「どの偉人の名言が聞きたいか?」という観点を置いて設定しています。
サンプルで作成してこれから成長させていくスキルなので現在は3名となっています。
ルフィ
レナスヴァルキュリア
藍染惣右介
この3名の偉人に名言を話してもらおうかなと思います。
*お金が稼げたら実際に声優さん起用したいな。
コードエディタ
// 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 Alexa = require('ask-sdk-core');
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle(handlerInput) {
const speakOutput = '誰の名言が知りたいですか?';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const QuotationsIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'QuotationsIntent';
},
handle(handlerInput) {
var person = handlerInput.requestEnvelope.request.intent.slots.person.value;
// const LenathSpeakTextArray = [
// '死の先をいくものたちよ',
// '我と共に生きるは霊験なる勇者よいでよ'
// ];
// let AizenSpeakText = speakTextArray[ Math.floor( Math.random() * speakTextArray.length ) ] ;
// let LenathSpeakText = LenathSpeakText[ Math.floor( Math.random() * speakTextArray.length ) ];
let speakText = `${person}様ですね`;
const AizenSpeakTextArray = [
'人は皆、猿のまがいもの 神は皆、人のまがいもの',
'ようこそ 私の尸魂界へ',
'本当に恐ろしいのは 目に見えない裏切りですよ 平子隊長',
'君の知る藍染惣右介など 最初から何処にも居はしない',
'人はその歩みに特別な名前をつけるのだ 勇気と',
'私が天に立つ',
'一体いつから 鏡花水月を遣っていないと錯覚していた?',
'勝者とは常に世界がどういうものかでは無くどう在るべきかについて語らなければならない!',
'憧れは 理解から最も遠い感情だよ',
'あまり強い言葉を遣うなよ…弱く見えるぞ'
];
const LennethSpeakTextArray = [
'死の先を逝くものたちよ',
'ニーベルンヴァレスティー'
];
const LuffySpeakTextArray = [
'海賊王に俺はなる!!',
'何が嫌いかより、何が好きかで自分を語れよ!!!'
]
let speakOutput = 'すみません、よくわかりません';
if ( person === '藍染惣右介' ) {
speakOutput = AizenSpeakTextArray[ Math.floor( Math.random() * AizenSpeakTextArray.length ) ] ;
} else if ( person === 'レナスヴァルキュリア' ) {
speakOutput = LennethSpeakTextArray[ Math.floor( Math.random() * LennethSpeakTextArray.length ) ];
} else if ( person === 'ルフィ') {
speakOutput = LuffySpeakTextArray[ Math.floor( Math.random() * LuffySpeakTextArray.length ) ];
}
return handlerInput.responseBuilder
.speak(speakText + '「' + speakOutput + '」')
//.reprompt('add a reprompt if you want to keep the session open for the user to respond')
.getResponse();
}
};
const HelpIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speakOutput = '試しに「藍染惣右介」と応答していただけますと、元5番隊隊長の藍染様の名言が聞くことができます';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
|| Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
},
handle(handlerInput) {
const speakOutput = 'それでは100年後までご機嫌よう';
return handlerInput.responseBuilder
.speak(speakOutput)
.getResponse();
}
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === '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 Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
},
handle(handlerInput) {
const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
const speakOutput = `You just triggered ${intentName}`;
return handlerInput.responseBuilder
.speak(speakOutput)
//.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.stack}`);
const speakOutput = `すいません、もう一度お願いできますでしょうか?`;
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
// The SkillBuilder 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,
QuotationsIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
)
.addErrorHandlers(
ErrorHandler,
)
.lambda();
インテントでQuotationsIntentというインテントを作成したので
QuotationsIntentHandlerという定数を作成します。
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'QuotationsIntent';
},
その中のcanHandleでスロットの中身とあっている内容を話せばhandleの中身に該当するものを探し出して話すという内容になっています。
var person = handlerInput.requestEnvelope.request.intent.slots.person.value;
ここでpersonという変数に話した内容が代入されます。
長いですがこのslotsの後に設定したスロットの名前が来て変わるだけなのでその部分さえ抑えておけばどんなスロットを作成しても問題ないです。
そして
let speakText = `${person}様ですね`;
とさせてslotsの中身を一度確認します。
console.logみたいな使い方ですかね。結構使っています。
あとは
const AizenSpeakTextArray = [
'人は皆、猿のまがいもの 神は皆、人のまがいもの',
'ようこそ 私の尸魂界へ',
'本当に恐ろしいのは 目に見えない裏切りですよ 平子隊長',
'君の知る藍染惣右介など 最初から何処にも居はしない',
'人はその歩みに特別な名前をつけるのだ 勇気と',
'私が天に立つ',
'一体いつから 鏡花水月を遣っていないと錯覚していた?',
'勝者とは常に世界がどういうものかでは無くどう在るべきかについて語らなければならない!',
'憧れは 理解から最も遠い感情だよ',
'あまり強い言葉を遣うなよ…弱く見えるぞ'
];
const LennethSpeakTextArray = [
'死の先を逝くものたちよ',
'ニーベルンヴァレスティー'
];
const LuffySpeakTextArray = [
'海賊王に俺はなる!!',
'何が嫌いかより、何が好きかで自分を語れよ!!!'
]
let speakOutput = 'すみません、よくわかりません';
if ( person === '藍染惣右介' ) {
speakOutput = AizenSpeakTextArray[ Math.floor( Math.random() * AizenSpeakTextArray.length ) ] ;
} else if ( person === 'レナスヴァルキュリア' ) {
speakOutput = LennethSpeakTextArray[ Math.floor( Math.random() * LennethSpeakTextArray.length ) ];
} else if ( person === 'ルフィ') {
speakOutput = LuffySpeakTextArray[ Math.floor( Math.random() * LuffySpeakTextArray.length ) ];
}
それぞれの偉人の名言を配列処理して
personの中身が誰だったら
speakOutputの中身に配列の中からランダムに一つ選択する
というJavaScriptで処理を書きます。
return handlerInput.responseBuilder
.speak(speakText + '「' + speakOutput + '」')
//.reprompt('add a reprompt if you want to keep the session open for the user to respond')
.getResponse();
あとはreturnしてあげれば完成
申請
1回目のリジェクト内容
この度は 偉人たちの名言 スキルを申請いただき、誠にありがとうございます。
誠に残念ではございますが、申請されたスキルをAlexaスキルストアに公開することができませんでした。審査結果については、下記をご覧ください。修正が完了しましたら、再度ご申請いただけますと幸いです。
また、フィードバックの不明点に関して、チャットでの対応(予約制)が可能となりました。ご希望の場合、お問い合わせフォームにてチャットサポート希望の旨と希望日時を複数お知らせください。
1. スキルの詳細カードに表示される説明には、ユーザーがスキルの使用方法について十分に理解できる正確な情報が含まれていません。
補足:
スキルの詳細カードに表示される説明に、タイポや文法的な誤りがないかどうかを今一度ご確認ください。また、スキルのコア機能や使用方法についての説明を、長い説明にも追記してください。
スキルの説明の書き方については、申請チェックリストのテストケース3.2を参照してください。
2. QuotationsIntentに含まれるスロットPERSONに含まれるスロット値[ルフィ]を呼び出した際、スキルが関連性のない応答を返しました。
スキルのバックエンドコードを更新し、定義されているすべてのスロット値が正しく関連性のある応答を返すことを確認してください。
再現手順
ユーザー: アレクサ、偉人たちの名言を開いて
スキル : 誰の名言が知りたいですか?
ユーザー: ルフィの名言
スキル : ルフィ様ですね「すみません、よくわかりません」
補足:
スロット値によっては、名言を聞くことができないため修正をお願いします。なお、「藍染惣右介」、「レナスヴァルキュリア」を発話したとき、スキルから正しく応答が返ってくることを確認済みです。
スロット値の処理方法についてはこちらを参照してください。
インテントとスロットの組み合わせの詳細については、こちらを参照してください。また、ビルトインインテントの実装については、こちらを参照してください。
3. ヘルププロンプトはユーザーへスキルのコア機能の使用方法を示す必要があります。あわせて、ヘルププロンプト内で質問または発話の促しを行い、最後にはセッションが開く必要があります。
再現手順
ユーザー: アレクサ、偉人たちの名言を実行して
スキル : 誰の名言が知りたいですか?
ユーザー: ヘルプ
スキル : You can say hello to me! How can I help?
(セッションオープン)
補足:
ヘルププロンプトがテンプレートのままになっているようです。日本語にて修正を行い、ユーザーがヘルプの後に何を発話すべきか分かるよう、コア機能に繋がる促し言葉または質問を追加してください。
ヘルプの詳細については、こちらを参照してください。また、セッションの状態についてはこちらを参照してください。
理由:
- スキルの概要が適当すぎ
- ルフィの名言がない
- ヘルプの時がデフォのまま
という理由でした。
2回目
1. QuotationsIntentに含まれるスロットPERSONに含まれるスロット値[ルフィ]を呼び出した際、スキルが関連性のない応答を返しました。
スキルのバックエンドコードを更新し、定義されているすべてのスロット値が正しく関連性のある応答を返すことを確認してください。
再現手順
ユーザー: アレクサ、偉人たちの名言を開いて
スキル : 誰の名言が知りたいですか?
ユーザー: ルフィの名言
スキル : ルフィ様ですね「すみません、よくわかりません」
補足:
[ルフィ]の名言をリクエストした際、「すみません、よくわかりません」と応答が流れ名言を聞くことができないため修正をお願いします。なお、「藍染惣右介」、「レナスヴァルキュリア」を発話したとき、スキルから正しく応答が返ってくることを確認済みです。
理由:
あ、ルフィ忘れてた
3回目
この度は 偉人たちの名言 スキルを申請いただき、誠にありがとうございます。
おめでとうございます。スキルは認定プロセスに合格し、まもなくスキルストアに公開されます。
スキルストアに公開されると、サービスへのトラフィックが増えることが見込まれます。十分なキャパシティがあることをご確認ください。 また、ユーザーが想定外のオペレーションを行なった場合にもサービスが機能し続けることを確認してください。
わーい通った!!!
といった流れになります。
この申請のところはデータ収集とかするスキルの場合(例:誕生日のスキルとかをDynamoDBやS3でデータ収集する場合)
プライバシーポリシーが必須なので要注意です。
さて、ここで少しだけどんな名言が出てくるかご紹介します
おあとがよろしいようで。
ひきつづきスキルを成長させていきます!!
別にたくさん使っているユーザがいるからお金とかもらえないからね。とる気もないし。(有料スキルではないので)
それでは楽しい開発ライフを。