LoginSignup
4
0

More than 5 years have passed since last update.

深夜テンションで創ってAmazonさんの審査員さんも動かしてしまった「ノリノリのサンタ」の話

Posted at

ごきげんよう

この記事は
「するめごはんのVUI・スマートスピーカー Advent Calendar 2018」
の21日目の記事です。

Echo Showのために4つのスキルを作成した方々、お疲れ様です。

さて、今回は僕が完全に深夜テンションで作成したAlexaスキル「ノリノリのサンタ」について記載します。

相変わらず、AmazonJapanの方を大至急協議させてしまいましたので、それについても触れていきます。

Alexaスキル「ノリノリのサンタ」とは

nori_R108.png

「なんかクリスマスのスキルを創るかー」

と思って、いらすとや ではない素材 を探していたら、テンションが高そうなサンタクロースの画像素材を見つけました。

なので、サンタクロースとかクリスマスとかについての雑学をググって、まとめて、 僕が深夜テンションで自分で声を収録しました

もちろん画面対応

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担当の方々はちゃんと対応してくれますよ。

リジェクトを頂いても、認定されても、フィードバックは送りましょう。
その方がお互いにハッピーだと思います。

以上。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0