ごきげんよう
この記事は
「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」
の21日目の記事です。
Echo Showのために4つのスキルを作成した方々、お疲れ様です。
さて、今回は僕が完全に深夜テンションで作成したAlexaスキル「ノリノリのサンタ」について記載します。
相変わらず、AmazonJapanの方を大至急協議させてしまいましたので、それについても触れていきます。
#Alexaスキル「ノリノリのサンタ」とは
「なんかクリスマスのスキルを創るかー」
と思って、いらすとや ではない素材 を探していたら、テンションが高そうなサンタクロースの画像素材を見つけました。
なので、サンタクロースとかクリスマスとかについての雑学をググって、まとめて、 僕が深夜テンションで自分で声を収録しました 。
#もちろん画面対応
Echo Showが来る前にリリースしたので、Echo Spotでの画面表示に対応しています。
テンション高めなサンタクロースの画像と、ノリノリな声がぞれぞれランダムに再生されます。
#ソースコードの一部
めっちゃ一部ですが、たいしたことはしてないです。
ユーザーが初回起動か否かの判定と、操作説明をオーディオファイルで流しているだけです。
画像とオーディオファイルは別々にランダムで表示させています。
'use strict';
const Alexa = require('ask-sdk');
const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'});
//音声を定義
//起動時
const smartmacchiato = '<audio src=\"https://s3XXXXXXX.mp3\" />';
//2回目以降、わしがのりのりサンタじゃあ
const washiganorinori = '<audio src=\"https://s3-XXXXXX.mp3\" />';
//初回ユーザー用説明
const first_user = '<audio src=\"https://s3-XXXXXXXXXXX.mp3\" />';
:
:
:
//トリビアの数
const norinori_01 = '<audio src=\"https://s3-XXXXXXXXXXX/norinori1.mp3\" />';
const norinori_02 = '<audio src=\"https://s3-XXXXXXXXXXX/norinori2.mp3\" />';
const norinori_03 = '<audio src=\"https://s3-XXXXXXXXXXX/norinori3.mp3\" />';
:
:
:
//トリビアのの音声配列
var norinori_speak_array = [
norinori_01,
norinori_02,
norinori_03,
:
:
:
];
//画像
const DisplayImg1 = {
title: 'のりのりサンタ1',
url: 'https://s3-XXXXXXXXXXXX/img/santa1.png'
};
const DisplayImg2 = {
title: 'のりのりサンタ2',
url: 'https://s3-XXXXXXXXXXXX/img/santa2.png'
};
const DisplayImg3 = {
title: 'のりのりサンタ3',
url: 'https://s3-XXXXXXXXXXXX/img/santa3.png'
};
:
:
:
//サンタ画像の配列
var norinori_img_array = [
DisplayImg1,
DisplayImg2,
DisplayImg3,
:
:
];
//////////////////////////////////////////////////////////////////////////
const LaunchRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
},
async handle(handlerInput) {
//サンタ話のうち1つをランダムで選ぶ
var factSpeakArr = norinori_speak_array;
var factSpearkIndex = Math.floor(Math.random() * factSpeakArr.length);
var randomSpearkFact = factSpeakArr[factSpearkIndex];
//サンタ画像のうち1つをランダムで選ぶ
var factImgArr = norinori_img_array;
var factImgIndex = Math.floor(Math.random() * factImgArr.length);
var randomImgFact = factImgArr[factImgIndex];
// Template 6
if (supportsDisplay(handlerInput)){
const myImage1 = new Alexa.ImageHelper()
.addImageInstance(randomImgFact.url)
.getImage();
const myImage2 = new Alexa.ImageHelper()
.addImageInstance(randomImgFact.url)
.getImage();
const primaryText = new Alexa.RichTextContentHelper()
.withPrimaryText('')
.getTextContent();
handlerInput.responseBuilder.addRenderTemplateDirective({
type: 'BodyTemplate6',
token: 'string',
backButton: 'HIDDEN',
backgroundImage: myImage2,
image: myImage1,
title: "",
textContent: primaryText
});
}
//JSONを扱う関連
let handlerInput_json = await JSON.stringify(handlerInput, null, 2);
(略)
let norinori_start = washiganorinori + randomSpearkFact;
try{
const queryItems = await docClient.query({
TableName: "norinoriSantaTable",
KeyConditionExpression: "#userId = :userId",
ExpressionAttributeNames: {"#userId": "userId"},
ExpressionAttributeValues: {":userId": JSONのユーザーID}
}).promise();
try{
console.log("queryItems.Items[0].userId: " + queryItems.Items[0].userId);
} catch (err){ //よろしくない実装
//初回ユーザー用のオーディオファイルにする
norinori_start = first_user;
console.log("user Nothing");
}
} catch(err){
console.error(`[query Error]: ${JSON.stringify(err)}`);
}
//DynamoDBにputする情報
var item = {
userId: JSONからのユーザーID,
};
var params = {
TableName: 'テーブル名',
Item: item
};
//DynamoDBにPut
await putDynamo(params);
//sessionAttributeを、起動後であることを示すように格納
var sessionAttribute = '';
sessionAttribute = {
"SHA_state": "after_start"
};
handlerInput.attributesManager.setSessionAttributes(sessionAttribute);
//しゃべる音声スキルと、間に0.7秒の待機を挟み、ノリノリの話とユーザーへの操作説明をする
let speechText = smartmacchiato + '<break time="0.7s"/>' + norinori_start + ask_next;
return handlerInput.responseBuilder
.speak('<speak>' + speechText + '</speak>')
.reprompt('<speak>' + speechText + '</speak>')
.withShouldEndSession(false)
.getResponse();
}
};
//起動直後、本アプリの継続に「はい」「次」「もっと」「のりのり」「きかせて」と答えた場合の処理
const continueHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& ((handlerInput.requestEnvelope.request.intent.name === 'AMAZON.YesIntent')
|| (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.NextIntent')
|| (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.MoreIntent')
|| (handlerInput.requestEnvelope.request.intent.name === 'norinoriIntent')
|| (handlerInput.requestEnvelope.request.intent.name === 'kikitaiIntent')
)
&& handlerInput.attributesManager.getSessionAttributes().SHA_state == 'after_start';
},
async handle(handlerInput,event) {
//サンタ話のうち1つをランダムで選ぶ
var factSpeakArr = norinori_speak_array;
var factSpearkIndex = Math.floor(Math.random() * factSpeakArr.length);
var randomSpearkFact = factSpeakArr[factSpearkIndex];
//サンタ画像のうち1つをランダムで選ぶ
var factImgArr = norinori_img_array;
var factImgIndex = Math.floor(Math.random() * factImgArr.length);
var randomImgFact = factImgArr[factImgIndex];
// Template 6
if (supportsDisplay(handlerInput)){
const myImage1 = new Alexa.ImageHelper()
.addImageInstance(randomImgFact.url)
.getImage();
const myImage2 = new Alexa.ImageHelper()
.addImageInstance(randomImgFact.url)
.getImage();
const primaryText = new Alexa.RichTextContentHelper()
.withPrimaryText('')
.getTextContent();
handlerInput.responseBuilder.addRenderTemplateDirective({
type: 'BodyTemplate6',
token: 'string',
backButton: 'HIDDEN',
backgroundImage: myImage2,
image: myImage1,
title: "",
textContent: primaryText
});
}
//ランダムに話して、ユーザー操作を促す
const speechText = randomSpearkFact + ask_next;
return handlerInput.responseBuilder
.speak('<speak>' + speechText + '</speak>')
.reprompt('<speak>' + speechText + '</speak>')
.withShouldEndSession(false)
.getResponse();
}
};
//DynamoDBにputする関数
function putDynamo(params) {
console.log("=== putDynamo function ===" + params);
docClient.put(params, function (err, data) {
console.log("=== put ===");
if (err) {
console.log(err);
} else {
console.log(data);
}
});
}
// returns true if the skill is running on a device with a display (show|spot)
function supportsDisplay(handlerInput) {
var hasDisplay =
handlerInput.requestEnvelope.context &&
handlerInput.requestEnvelope.context.System &&
handlerInput.requestEnvelope.context.System.device &&
handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display;
console.log("Supported Interfaces are" + JSON.stringify(handlerInput.requestEnvelope.context.System.device.supportedInterfaces));
return hasDisplay;
}
#リジェクト内容で協議させてしまうパターン
僕にとっては、もう慣れているので構わないのですが、リジェクト理由に納得がいかずに、進言したら協議の上、承認されました。
リジェクト理由は
「ノリノリ」は名詞ではないから。
日本語でスキルの呼び出し名を決める際には、原則として名詞を2つにする必要があります。
「ヒロインの告白」みたいな。
「の」は無くてもOKの場合も多いです。
で、納得いかないので以下のように伝えたら承認されました。
##1.「ノリノリ」は名詞でも使われる
goo辞書で検索すると、以下のように記載されてます。
[名・形動]《動詞「乗る」の連用形を重ねた語。「ノリノリ」と書くことも多い》調子がよくて気分が高揚していること。乗りがよくて、リズミカルであること。また、そのさま。いけいけ。「乗り乗りな曲で踊る」「乗り乗りムードで一気に勝ち進む」
[名・形動]ってあるじゃん。
##2.テンションが高い場合はどう表現するのか
テンションが高い状態を示す場合、具体的にAmazonさんはどういう表現なら良いのか求めました。
そしてその際に、
仮に「ハイテンションなサンタ」にした場合、【ハイテンション】こそ日本語ではないと私は解釈します。
日本語のみで状況・状態を説明するための具体的なガイドラインをください。
と、伝えました。
##そしたら通った
いつも通り?、「大至急協議します」のメールが飛んでくるので、しばし待ちます。
Amazonさんから「大至急協議します」のメールが来た場合、たいていその日のうちに返答がきます。
今回は
「協議の上、認められることになりましたので、再申請をお願いします。」
とのことで、再申請したら通りました。
#その後のAlexaDevSummitでつかまる
AlexaDevSummitで会場をウロウロしてたら、中の人からお声がけがありました。
A「showさんですよね!この間の件ですが・・・」
僕「(やらかしまくっているので)どの件でしょうかごめんなさi・・・」
A「ノリノリの件です。ご指摘ありがとうございました。あれはたしかに認められないと表現できないですよね。フィードバックありがとうございました!」
僕「こちらこそ、ご対応ありがとうございましたぁぁぁ!!!」
全力で土下座する体制にしようとしましたが、むしろ感謝されました。
何が言いたいかというと、最近、スキル審査結果に対してフィードバックが画面ポチポチで選べるようになったんですけども、審査員側だって、ユーザーからのご意見が欲しいわけです。
明らかに開発者側のミスは置いておいて、リジェクトされたから黙って従うだと、AmazonさんのAlexaスキル審査員さんが独りよがりの神になってしまうわけです。
なので、ちゃんと根拠を示して、自分の意見を伝えると、AmazonのAlexa担当の方々はちゃんと対応してくれますよ。
リジェクトを頂いても、認定されても、フィードバックは送りましょう。
その方がお互いにハッピーだと思います。
以上。