Alexaスキルアワード公式ハッカソン東京（2018/06/30-07/01）

#Alexaスキルアワード公式ハッカソン東京
2018.06.30-07/01　@御茶ノ水
[connpassサイト](https://alexaskillawards.connpass.com/event/91338/)

---

###■イベント概要（connpassより引用）
『Amazonでは多くのユーザーにとってさらに魅力的なスキルが提供され、Alexaを通じテクノロジーと人がもっとつながりあう未来が来る事を期待し、[Alexaスキルアワード2018](http://alexaskillawards.jp/)を開催することとなりました。
※開催期間：6/13（水）〜8/27（月）
今回のハッカソンはチームビルディングやアイデアソンから始まり、2日間でスキル開発完了を目指します。
2日目の最後には懇親会と参加者全員によるスキル体験会を通して、Alexaスキルの開発者同士の交流の場を設けたいと思います。』

---

###■スケジュール

| 時間 | コンテンツ |
|:--|:--|
| 1日目 |
| 9:30| Opening |
| 10:00 | インプット、サポート紹介 |
| 11:00 | アイデアソン |
| 12:30 | ランチ |
| 14:00 | アイデア発表 |
| 14:30 | ハッキングスタート |
| 20:00 | 一日目終了 |
| 2日目 |
| 9:30| Opening |
| 10:00 | ハッキングタイム |
| 12:00 | ランチ |
| 15:30 | 作業終了 |
| 16:00 | 各作品プレゼン |
| 17:30 | タッチ&トライ |
| 18:00 | 懇親会 |

---

###■支援サポーター
* (Twilio)[https://twilio.kddi-web.com/]
* (Kintone)[https://kintone.cybozu.com/jp/]
* (nifity cloud mobile backend)[http://mb.cloud.nifty.com/]

---

###■コーディングメモ
基本手順はこの[HP](https://dev.classmethod.jp/etc/first-step-of-making-alexa-custom-skills/)

####【Skill作成の大きな流れ】
AlexaSkillのAmazon開発者ポータルにSkill名等の基本設定を実施
→　AWSからLamdaの設定（AlexaSkillとの紐付けを実施）
→　AlexaSkillからもLamdaの紐付けを実施
→　Lamdaに処理をコーディング
→　インスタント及びスロットをLamdaの処理に合わせて作成
→　AlexaSkillの設定をいじったらbildする
→　テスト環境もしくは実機でtest

####【参考にしたサイト】
* [はじめてのAlexa](https://qiita.com/kei_0324/items/e97fe52df9e0a91ad5bf)
* [【祝Alexa日本上陸】とりあえず日本語でスキルを作ってみる ](https://dev.classmethod.jp/etc/first-step-of-making-alexa-custom-skills/)
* [[日本語Alexa] Alexa Skills Kit for Node.js はじめの一歩](https://dev.classmethod.jp/cloud/alexa-sdk/)
* [Node書くならEventEmitterについて知っとくべし](https://qiita.com/yuku_t/items/d69d3a2c7dafa7d04e87)
* [Alexa Skillのエラーハンドリング](https://qiita.com/monisoi/items/da451b2e40c2360ce69a)

####【詰まったところ】
* スロット：音声ワードを格納する変数みたいなもの。
* インテント：ユーザーの音声によるリクエストを満たすアクションを表す。
* emit()は１つのfunction（インテント）に１つまで
* functionを呼び出す方法は2パターン
　「音声入力（キーワード）で呼び出す」、「別のfunctionから呼び出す」
* global変数を多く定義するのは良くないが、関数間出渡す方法が分からない
* statesを指定できるが、何故かうまくいかない・・・（カウントで何とかする）
* 音声入力のワード認識が難しい。。。（amazon.personにしたとき地名を言うと、人の名前に近いワードで認識される）
* エラーハンドリングの考慮が難しい。（音声入力だけに間違え方、認識の仕方が無限・・・）
* タイミングや長いワードへの対応に考慮が必要
* 読み上げ文章を漢字で書くと、読み方を間違えたり、ひらがなにすると変な発音になる。
* bildに時間がかかるのと、debugがピンポイントでできない（技術不足）のでtry&errorに疲れる・・・

---

###■成果物
####【概要】
子供向け物語り作成Skill『ストーリーメーカー』
子供が2人の登場人物と舞台をAlexaに答えるだけで、物語を作成してくれる。
作成された物語を読んでいく中で、応対形式でストーリーを分岐させる参加型物語。
　ex)応援することで相手を倒すことができる。

![【プレゼン用】ストーリーメイカーforKIDS (1).jpg](https://qiita-image-store.s3.amazonaws.com/0/160422/f31268f3-5b05-a87d-b0d8-9db3dfabc66a.jpeg)
![【プレゼン用】ストーリーメイカーforKIDS (1)2.jpg](https://qiita-image-store.s3.amazonaws.com/0/160422/32b5d43a-0fb0-d2f5-431e-f1f538992aa1.jpeg)
![【プレゼン用】ストーリーメイカーforKIDS (1)3.jpg](https://qiita-image-store.s3.amazonaws.com/0/160422/33e99b8c-236f-d885-939a-2d694d07ccb2.jpeg)


####【実演】
<img width="1265" alt="Google_Chrome_-_developer_amazon_com_-_Alexa_Skills_Kit_Developer_Console_-_Screen_Shot_2018-07-01_22_37_13.png" src="https://qiita-image-store.s3.amazonaws.com/0/160422/607e30f3-2d84-e7dc-730c-f14c957fb8ec.png">

####【構成】
<img width="1280" alt="Google_Chrome_-_ap-northeast-1_console_aws_amazon_com_-_Lambda_Management_Console_-_Screen_Shot_2018-07-01_22_37_34.png" src="https://qiita-image-store.s3.amazonaws.com/0/160422/ab9ad2e8-3389-793c-3f52-4646ce18de6e.png">

####【ソースコード】

```node:index.js

'use strict';

const Alexa = require('alexa-sdk');

const APP_ID = undefined;  // TODO replace with your app ID (OPTIONAL).

// global変数を定義
var character = [];     //inputワード配列(キャラクター)
var location;           //inputワード変数(場所)
var count = 1;          //応答回数を記録する変数
var fightVoltage = 0;   //頑張れカウント変数
var storyContent = [];  //物語の格納配列
var chooseStory = true; //trueの場合は桃太郎のはなしを、falseの場合はお金持ちの話をする

// メイン処理
const handlers = {
    //スキル応答（初期動作）
    'LaunchRequest': function () {

        if(count==1 || count == 2){     //登場人物二人を登録
          var speechOutput = 'このスキルでは、君たちから聞いた登場人物や場所の名前から、'
                  + '短い物語を作るよ。';
          var speechOutput2 = '登場人物' + count + 'の名前を教えてくれるかな。' ;
          if(count == 1){
              this.emit(':ask', speechOutput + speechOutput2);
          }
          this.emit(':ask', speechOutput2);
        }else if(count==3){             //場所を登録
          var speechOutput = '登場人物の次はお話の舞台となる場所の名前を教えてくれるかな。';
          this.emit(':ask', speechOutput);
        }else if(count == 4){           //最終確認
          var speechOutput = character[1] + 'と' +　character[2] + 'が' + location + 'で活躍するお話を作るね。';
          var speechOutput2 = '今から作るから少し間ってね。'
                            + 'うーーーーーーーーーーーーーーん。　　　　　'
                            + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/scifi/amzn_sfx_scifi_engines_on_02.mp3'/>"
                            + 'できたよ。これから作ったお話をするから静かに聞いててね。みんな準備はいいかな？';
          this.emit(':ask', speechOutput + speechOutput2);
        }else{
          this.emit('FallbackIntent');
        }
    },

    //温泉入力
    'InputAdIntent':function(){
        if(count==1 || count == 2){
            character[count] = this.event.request.intent.slots.Input.value;
        }else{
            location = this.event.request.intent.slots.Input.value;
        }
        this.emit('InputCheck');
    },

    // 入力内容の確認
    'InputCheck':function(){
        if(count==1 || count==2){
            this.emit(':ask', character[count] + 'であってる？')
        }
        else{
            this.emit(':ask', location + 'であってる？')
        }
    },

    //物語の作成
    'ReadStory':function(){
        if (count == 5) {

            //物語をランダムに指定。
            if((Math.floor(Math.random() * 10) % 2) == 0){
                chooseStory = true;
            }else{
                chooseStory = false;
            }
            if(chooseStory == true){    //物語１
                //起（起承転結）
                storyContent.push('むかしむかしあるところに、おじいさんとおばあさんが住んでいました。'
                    + "おばあさんが川で洗濯していると、どんぶらこどんぶらこと大きな桃が流れてきました"
                    + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/nature/amzn_sfx_small_stream_02.mp3'/>。"
                    + "驚いたおばあさんが桃をふたつにわると、中から元気な男の子が飛び出してきました。"
                    + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/human/amzn_sfx_baby_big_cry_01.mp3'/>'"
                    + 'おじいさんとおばあさんはその男の子に'
                    + character[1]
                    + 'と名づけ、うちで育てることにしました。');
                //承（起承転結）
                storyContent.push('おじいさん、おばあさんと'
                    + character[1]
                    + 'が仲良く暮らしていると、'
                    + location
                    + 'で'
                    + character[2]
                    +'が悪さをしているという噂が飛び込んできました。そこで'
                    + character[1]
                    + 'は、'
                    + character[2]
                    + '退治にでかけることになりました。途中、おばあさんが作ってくれたきびだんごで'
                    + "サル。<audio src='https://s3.amazonaws.com/ask-soundlibrary/animals/amzn_sfx_monkey_chimp_01.mp3'/>"
                    + "イヌ。<audio src='https://s3.amazonaws.com/ask-soundlibrary/animals/amzn_sfx_dog_med_bark_growl_01.mp3'/>"
                    + "キジ。<audio src='https://s3.amazonaws.com/ask-soundlibrary/animals/amzn_sfx_raven_caw_1x_01.mp3'/>"
                    + 'を仲間にしました。');
                //転（起承転結）
                storyContent.push(location
                    + 'に到着した'
                    + character[1]
                    + 'と仲間たち。'
                    + "<prosody pitch='x-low'>「がおーーー」</prosody>"
                    + "<prosody pitch='x-high'>「な、なんて強さだ...みんな僕に元気を分けてくれっ...！！」</prosody>"
                    + 'みんな'
                    + character[1]
                    + 'を応援してあげてくれる？');
                const speechOut = storyContent[0] + storyContent[1] + storyContent[2];
                this.emit(':ask',speechOut);
            }else{      //物語２
                //起（起承転結）
                storyContent.push("むかしむかし、"
                    + location
                    + "に、一人のおおがねもち。"
                    + character[1] + "がいました。"
                    + " この人は大金持ちですが、とてもけちでした。"
                    + " 　貧しい人がたずねて行っても、決して助けてはくれません。"
                    + "<prosody pitch='x-low'>「何て、けちな男だ」</prosody>"
                    + location
                    + "の人たちは、みんな"
                    + character[1]
                    + "の悪口を言いました。さて、"
                    + location
                    + "に、"
                    + character[2]
                    + "がいました。");

                //承（起承転結）
                storyContent.push("この"
                    + character[2]
                    + "はとても親切で、貧しい人を見ると必ず、お金や食べ物を与えて助けてあげたのです。"
                    + " 　ある日の事、"
                    + character[1]
                    + "が急な病気で死んでしまいました。 ");
                //転（起承転結）
                storyContent.push(location
                    + "のせわやくは、"
                    + character[1]
                    + "を"
                    + location
                    + "のお墓の一番はしに埋めました。"
                    + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/musical/amzn_sfx_church_bell_1x_05.mp3'/>"
                    + "しかし葬式には、"
                    + location
                    + "の人たちは誰ひとり行きませんでした。"
                    + " それから何日かして、一人の貧しい人が"
                    + character[2]
                    + "を尋ねました。"
                    + "<prosody pitch='x-high'>「何か、めぐんでください」</prosody>。"
                    + "みなさんならどうしまする？ なにかあげる？なにもあげない？");
                const speechOut = storyContent[0] + storyContent[1] + storyContent[2];
                this.emit(':ask',speechOut);
            }
            
        }else if(count == 6){
            if(chooseStory == true){
                storyContent.push('それじゃあ、一緒に「頑張れ」っていってね。'
                    + 'せーの、がんばれー！」');
                const speechOut = storyContent[3];
                this.emit(':ask',speechOut);
            }else{
                const speechOut = storyContent[3];
                this.emit(':tell',speechOut);
                count = 1;
              character[1] = null;
              character[2] = null;
              location = null;
              fightVoltage = 1;
              chooseStory = null;
              for (var i = 0; i < storyContent.length; i++) {
                storyContent[i] = null;
              }
            }
        }else if(count == 7){
            if(chooseStory == true){
                storyContent.push("<prosody pitch='x-high'>「みんな、ありがとう！うおおおおおお」</prosody>"
                    + "<prosody pitch='x-low'>「ぐへーーーやられたーーー」</prosody>。"
                    + character[2]
                    + 'を倒しました。'
                    + character[2]
                    + 'をどうしましょう？みんなどうする？仲良くする？たべちゃう？');
                const speechOut = storyContent[4];
                this.emit(':ask',speechOut);
            }else{
            }
        }else if(count == 8){
            var speechOut = storyContent[5]
            this.emit(':tell',speechOut);
            count = 1;
            character[1] = null;
            character[2] = null;
            location = null;
            fightVoltage = 1;
            chooseStory = null;
            for (var i = 0; i < storyContent.length; i++) {
              storyContent[i] = null;
            }
        }
    },

    'AgreeIntent':function(){
        count++;
        if (count < 5 ){
            this.emit('LaunchRequest');
        } else{
            this.emit('ReadStory');
        }
    },

    'DisagreeIntent':function(){
      this.emit('FallbackIntent');
    },

    'JudgeFriend':function(){
        count++;
      　storyContent.push(character[2]
                    + 'は、すっかり改心して悪さをしなくなりました。そして'
                    + character[2]
                    + 'と'
                    + character[1]
                    + "は、今日も仲良く鬼ごっこをして遊んでいるようですよ。めでたし、めでたし。"
                    + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/human/amzn_sfx_large_crowd_cheer_02.mp3'/>");
        this.emit('ReadStory');
    },

    'JudgeEat':function(){
      count++;
      storyContent.push(character[1]
                      + 'は'
                      + character[2]
  //                    + 'を火鍋に入れ、しゃぶしゃぶにしました。'
                      + "<audio src='https://s3.amazonaws.com/ask-soundlibrary/foley/amzn_sfx_large_fire_crackling_01.mp3'/>"
                      + '意外と美味しかったので、'
                      + character[1]
                      + 'は'
                      + character[2]
                      + 'を探し求め旅を始めました。めでたし、めでたし。');

      this.emit('ReadStory');
    },

    'JudgeFight':function(){
      if(fightVoltage == 1){
        fightVoltage++;
        this.emit(':ask','みんなもっと応援してあげて');
      }else if(fightVoltage > 1 && fightVoltage < 4){
        fightVoltage++;
        this.emit(':ask','もっと');
      }else{
        count++;
        this.emit('ReadStory');
      }
    },

    'JudgeGet':function(){
      count=6;
      storyContent.push(character[2]
                      + "は貧しい人にごはんをわけてあげました。"
                      + "貧しい人は、不満そうにいいました。"
                      + "<prosody pitch='x-high'>「どうしてですか？　あなたはいつも、お金をめぐんでくれたのに」</prosody>。"
                      + "すると"
                      + character[2]
                      + "は、こんな事を話し出しました。"
                      + " <prosody pitch='low'>「この前に亡くなったお金持ちのご主人が、お金を持って来て、わたしにこんな事を頼んだんだ。 『このお金を、貧しい人にあげてくれ。ただし、誰のお金かは、決して言わないように』と」</prosody>。"
                      + "この話は、すぐに"
                      + location
                      + "中に広まりました。"
                      + " そしてみんなは、すぐにお墓へ飛んでいきました。"
                      + " そして、今まで悪口を言っていた事を、心からあやまったのです。");
      this.emit('ReadStory');
    },

    'JudgeNoGet':function(){
      count=6;
      storyContent.push("すると、"
                      + character[2]
                      + "がこう言ったのです。"
                      + " <prosody pitch='low'>「あげる物なんて、何もないよ」</prosody>"
                      + "それを聞いた貧しい人は、びっくりしました。"
                      + " <prosody pitch='x-high'>「どうしてですか？　あなたはいつも、わたしたちを助けてくれたのに」。</prosody>"
                      + "すると"
                      + character[2]
                      + "は、こんな事を話し出しました。"
                      + " <prosody pitch='low'>「この前に亡くなったお金持ちのご主人が、お金を持って来て、わたしにこんな事を頼んだんだ。 『このお金を、貧しい人にあげてくれ。ただし、誰のお金かは、決して言わないように』と」。</prosody>"
                      + "この話は、すぐに"
                      + location
                      + "中に広まりました。"
                      + "そしてみんなは、すぐにお墓へ飛んでいきました。"
                      + "そして、今まで悪口を言っていた事を、心からあやまったのです。");
        this.emit('ReadStory');
    },

    //エラーインテント
    'FallbackIntent':function(){
      this.emit(':ask', 'ゴメンね。うまく聞き取れなかった。もう一度お願いできる？')
    },

    'AMAZON.HelpIntent': function () {
        this.emit('LaunchRequest');
    },
    'AMAZON.CancelIntent': function () {
        count = 1;
        character[1] = null;
        character[2] = null;
        location = null;
        fightVoltage = 1;
        chooseStory = null;
        for (var i = 0; i < storyContent.length; i++) {
          storyContent[i] = null;
        }
        this.emit(':tell', 'じゃあね');
    },
    'AMAZON.StopIntent': function () {
        count = 1;
        character[1] = null;
        character[2] = null;
        location = null;
        fightVoltage = 1;
        chooseStory = null;
        for (var i = 0; i < storyContent.length; i++) {
          storyContent[i] = null;
        }
        this.emit(':tell', 'じゃあね');
    },
};

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.registerHandlers(handlers);
    alexa.execute();
};

```

```json:json.js

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ストーリーメーカー",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "LaunchRequest",
                    "slots": [],
                    "samples": [
                        "ストーリーメーカーをお願い",
                        "ストーリーメーカーを起動",
                        "ストーリーメーカーを開いて",
                        "ストーリーメーカーを起動して"
                    ]
                },
                {
                    "name": "InputAdIntent",
                    "slots": [
                        {
                            "name": "Input",
                            "type": "AMAZON.Person"
                        }
                    ],
                    "samples": [
                        "{Input}",
                        "{Input} が登場",
                        "{Input} を登場 ",
                        "{Input} を出して"
                    ]
                },
                {
                    "name": "InputCheck",
                    "slots": [],
                    "samples": []
                },
                {
                    "name": "StartGame",
                    "slots": [],
                    "samples": []
                },
                {
                    "name": "AgreeIntent",
                    "slots": [],
                    "samples": [
                        "うん",
                        "いい",
                        "そう",
                        "あってる"
                    ]
                },
                {
                    "name": "DisagreeIntent",
                    "slots": [],
                    "samples": [
                        "いいえ",
                        "あってない",
                        "ちがう",
                        "まちがってる"
                    ]
                },
                {
                    "name": "JudgeFriend",
                    "slots": [],
                    "samples": [
                        "仲良くする"
                    ]
                },
                {
                    "name": "JudgeEat",
                    "slots": [],
                    "samples": [
                        "食う",
                        "喰う",
                        "食べちゃう",
                        "食べる"
                    ]
                },
                {
                    "name": "JudgeFight",
                    "slots": [],
                    "samples": [
                        "がんばれ",
                        "頑張れー",
                        "頑張れ"
                    ]
                },
                {
                    "name": "JudgeGet",
                    "slots": [],
                    "samples": [
                        "わけてあげる",
                        "提供する",
                        "プレゼントする",
                        "あげる"
                    ]
                },
                {
                    "name": "JudgeNoGet",
                    "slots": [],
                    "samples": [
                        "わけてあげない",
                        "提供しない",
                        "プレゼントしない",
                        "いじわるする",
                        "あげない"
                    ]
                }
            ],
            "types": []
        }
    }
}

```

####今後の課題
* まだ現状2エピソードしかないので、拡充したい。
* コードが煩雑なので、整理。
* エラーコードが上手く処理で来ていないので、修正
* 子供が使いやすい（しゃべる言葉を誘導する、騒いでも大丈夫）などが考慮できてないので、改善。
* そもそもNode.jsがお初なので、もっと勉強して使いこなしたい。
などなど課題は山済み。