Alexaスキルアワード公式ハッカソン東京
2018.06.30-07/01 @御茶ノ水
connpassサイト
■イベント概要(connpassより引用)
『Amazonでは多くのユーザーにとってさらに魅力的なスキルが提供され、Alexaを通じテクノロジーと人がもっとつながりあう未来が来る事を期待し、Alexaスキルアワード2018を開催することとなりました。
※開催期間: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
【Skill作成の大きな流れ】
AlexaSkillのAmazon開発者ポータルにSkill名等の基本設定を実施
→ AWSからLamdaの設定(AlexaSkillとの紐付けを実施)
→ AlexaSkillからもLamdaの紐付けを実施
→ Lamdaに処理をコーディング
→ インスタント及びスロットをLamdaの処理に合わせて作成
→ AlexaSkillの設定をいじったらbildする
→ テスト環境もしくは実機でtest
【参考にしたサイト】
- はじめてのAlexa
- 【祝Alexa日本上陸】とりあえず日本語でスキルを作ってみる
- [日本語Alexa] Alexa Skills Kit for Node.js はじめの一歩
- Node書くならEventEmitterについて知っとくべし
- Alexa Skillのエラーハンドリング
【詰まったところ】
- スロット:音声ワードを格納する変数みたいなもの。
- インテント:ユーザーの音声によるリクエストを満たすアクションを表す。
- emit()は1つのfunction(インテント)に1つまで
- functionを呼び出す方法は2パターン
「音声入力(キーワード)で呼び出す」、「別のfunctionから呼び出す」 - global変数を多く定義するのは良くないが、関数間出渡す方法が分からない
- statesを指定できるが、何故かうまくいかない・・・(カウントで何とかする)
- 音声入力のワード認識が難しい。。。(amazon.personにしたとき地名を言うと、人の名前に近いワードで認識される)
- エラーハンドリングの考慮が難しい。(音声入力だけに間違え方、認識の仕方が無限・・・)
- タイミングや長いワードへの対応に考慮が必要
- 読み上げ文章を漢字で書くと、読み方を間違えたり、ひらがなにすると変な発音になる。
- bildに時間がかかるのと、debugがピンポイントでできない(技術不足)のでtry&errorに疲れる・・・
■成果物
【概要】
子供向け物語り作成Skill『ストーリーメーカー』
子供が2人の登場人物と舞台をAlexaに答えるだけで、物語を作成してくれる。
作成された物語を読んでいく中で、応対形式でストーリーを分岐させる参加型物語。
ex)応援することで相手を倒すことができる。
【実演】

【構成】

【ソースコード】
'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){ //物語1
//起(起承転結)
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{ //物語2
//起(起承転結)
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();
};
{
"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がお初なので、もっと勉強して使いこなしたい。
などなど課題は山済み。