Alexaスキル:AudioPlayer で音楽配信スキル作成から公開まで
Alexaスキルで、音楽配信スキルを作成しました!
スキル作成経験者であるお父ちゃんに教えてもらいながら無事公開へ至りました。
お父ちゃん作成スキル「夏休みの友」
目次
準備:Alexa道場でお勉強
作成するにあたって基本的な知識は動画で配信されているAlexa道場を見る事をおすすめされました。
オンラインセミナー「Alexa道場」 | Amazon Alexa | アレクサ
その中でも抜粋してもらった3つの動画を視聴しました。
1. 第9回Alexa道場:ASK SDK for Node.js Version 2
Alexa道場 第3回と4回に作り方が説明されているのですが、SDKのVersionが 1 のものなので、まずは Version2 の仕様説明を見る事をおすすめされました。
正直詳しいところはよくわかっていませんが、わからなかったらあとで見直そうと思いざっくり視聴しました。
2. 第3回Alexa道場:音声インターフェイスをデザインしよう
3. 第4回Alexa道場:Node jsを使ってAlexaスキルを作ろう
コーヒーショップを作りながら基本的な作成の流れがつかめました!
スキルの設計
基本的な機能や作成方法を学んで、スキルの設計をしました。
最初から難しいものは厳しいのでまずは必要最小限の機能にしました。
機能は「アレクサ、ウェルカム脳の音楽を開いて」と言ったら音楽がシャッフル再生される、というシンプルなものです。(再生は常にシャッフル)
リピート再生や一時停止には対応していません。一時停止・停止した所からの再生に対応するにはセッションをAmazon DynamoDB に保存する必要がありますが、今回は DynamoDB へは保存せず、常にランダムな再生としています。
参考にさせて頂いた記事など
Alexa スキルで音楽再生するには「AudioPlayer」が必要とのことで、こちらの記事は色々読ませて頂き大変勉強になりました。
Alexa-SDK Ver2(その8) AudioPlayer | DevelopersIO
また、音楽を連続再生するにあたってはこちらのソースコードを参考にさせて頂きました
skill-sample-nodejs-audio-player/multiple-streams/lambda/src at mainline · alexa/skill-sample-nodejs-audio-player · GitHub
Alexa Developer Console
AudioPlayerを有効化
まずは Alexa-SDK Ver2(その8) AudioPlayer の通り AudioPlayerを有効化
=> AMAZON.PauseIntent と AMAZON.ResumeIntent が追加される
スキルの呼び出し名を設定
ビルトインライブラリの追加
「次へ」を実装したいので、「Alexaのビルトインライブラリから既存のインテントを使用」から「next」を検索し「AMAZON.NextIntent」を追加
カスタムインデントの追加
カスタムインテントで「PlayIntent」を作成
※ PlayIntent はビルトインライブラリにもあるのですがスキルには最低1つ以上のカスタムスキルが必要なので、PlayIntentをカスタムで作成しました。
スキルビルダーのチェックリスト
これで2までチェックがつきました。
Lambda Management Console
関数を作成
ダッシュボード > 関数の作成
AWS Serverless Application Repository > alexa-skills-kit-nodejs-factskill をクリック
名前を入力しDeploy をクリック
デプロイ完了後(完了するまで少し時間がかかる)「alexaskillskitnodejsfactskill」をクリック
エンドポイントの設定
上記で開いた画面上部にある「ARN」をコピーする
上記を Alexa Developer Console の エンドポイント > AWS LambdaのARN > デフォルトの地域 にペースト > エンドポイントを保存
「スキルのマニフェストは正常の保存されました」と表示される
カスタム > スキル ビルダーのチェックリスト
「エンドポイント」にチェックが入る
ビルド
「モデルをビルド」をクリック
「正常にビルドされました」と表示されすべてにチェックが入る
Lambda Management Console
最終的に公開に至ったソースコード
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
const Alexa = require('ask-sdk'); const constants = require('./constants'); const speechOutput = 'ウェルカム脳の音楽をシャッフル再生します'; /* audioData ------------------------------------------------------------------------------------------ */ function audioData() { const no = 0 + Math.floor( Math.random() * constants.audioData.length ); let audioData = {}; audioData.title = constants.audioData[no].title; audioData.url = constants.audioData[no].url; return audioData; } /* 起動 ------------------------------------------------------------------------------------------ */ const GetNewFactHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'LaunchRequest' || (request.type === 'IntentRequest' && request.intent.name === 'GetNewFactIntent'); }, handle(handlerInput) { const audio = audioData(); return handlerInput.responseBuilder .speak(speechOutput) //.withSimpleCard('New', audio.title) .addAudioPlayerPlayDirective('REPLACE_ALL', audio.url , audio.title , 0, null) .getResponse(); }, }; /* AudioPlayerEventHandler ------------------------------------------------------------------------------------------ */ const AudioPlayerEventHandler = { canHandle(handlerInput) { return handlerInput.requestEnvelope.request.type.startsWith('AudioPlayer.'); }, async handle(handlerInput) { const { requestEnvelope, attributesManager, responseBuilder } = handlerInput; const audioPlayerEventName = requestEnvelope.request.type.split('.')[1]; switch (audioPlayerEventName) { case 'PlaybackStarted': //responseBuilder.withSimpleCard('Play', 'test'); break; case 'PlaybackFinished': break; case 'PlaybackStopped': break; case 'PlaybackNearlyFinished': { const audio = audioData(); const token = handlerInput.requestEnvelope.request.token; responseBuilder.addAudioPlayerPlayDirective('ENQUEUE', audio.url , audio.title , 0, token); break; } } return responseBuilder.getResponse(); }, }; /* PlayIntentHandler ------------------------------------------------------------------------------------------ */ const PlayIntentHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return (request.type === 'IntentRequest' && request.intent.name === 'PlayIntent'); }, async handle(handlerInput) { const audio = audioData(); return handlerInput.responseBuilder .speak(speechOutput) //.withSimpleCard('Play', audio.title) .addAudioPlayerPlayDirective('REPLACE_ALL', audio.url , audio.title , 0, null) .getResponse(); } }; /* PauseIntentHandler ------------------------------------------------------------------------------------------ */ const PauseIntentHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.PauseIntent'); }, async handle(handlerInput) { return handlerInput.responseBuilder .addAudioPlayerStopDirective() .getResponse(); } }; /* ResumeIntentHandler ------------------------------------------------------------------------------------------ */ const ResumeIntentHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.ResumeIntent'); }, async handle(handlerInput) { const AudioPlayer = handlerInput.requestEnvelope.context.AudioPlayer; const offset = AudioPlayer.offsetInMilliseconds; const audio = audioData(); return handlerInput.responseBuilder .speak(speechOutput) //.withSimpleCard('Resume', audio.title) .addAudioPlayerPlayDirective('REPLACE_ALL', audio.url , audio.title , offset, null) .getResponse(); } }; /* NextIntentHandler ------------------------------------------------------------------------------------------ */ const NextIntentHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.NextIntent'); }, async handle(handlerInput) { const audio = audioData(); return handlerInput.responseBuilder //.withSimpleCard('Next', audio.title) .addAudioPlayerPlayDirective('REPLACE_ALL', audio.url , audio.title , 0, null) .getResponse(); } }; const HelpHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.HelpIntent'; }, handle(handlerInput) { return handlerInput.responseBuilder .speak(HELP_MESSAGE) .reprompt(HELP_REPROMPT) .getResponse(); }, }; const ExitHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'IntentRequest' && (request.intent.name === 'AMAZON.CancelIntent' || request.intent.name === 'AMAZON.StopIntent'); }, handle(handlerInput) { return handlerInput.responseBuilder .addAudioPlayerStopDirective() .speak(STOP_MESSAGE) .getResponse(); }, }; const SessionEndedRequestHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'SessionEndedRequest'; }, handle(handlerInput) { console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`); return handlerInput.responseBuilder.getResponse(); }, }; const ErrorHandler = { canHandle() { return true; }, handle(handlerInput, error) { console.log(`Error handled: ${error.message}`); return handlerInput.responseBuilder .speak('その機能はありません、ごめんね') .getResponse(); }, }; //const SKILL_NAME = 'Space Facts'; //const GET_FACT_MESSAGE = 'Here\'s your fact: '; const HELP_MESSAGE = 'You can say tell me a space fact, or, you can say exit... What can I help you with?'; const HELP_REPROMPT = 'What can I help you with?'; const STOP_MESSAGE = 'またね!'; const skillBuilder = Alexa.SkillBuilders.standard(); exports.handler = skillBuilder .addRequestHandlers( GetNewFactHandler, PlayIntentHandler, PauseIntentHandler, ResumeIntentHandler, NextIntentHandler, HelpHandler, ExitHandler, AudioPlayerEventHandler, SessionEndedRequestHandler ) .addErrorHandlers(ErrorHandler) .lambda(); |
constants.js
skill-sample-nodejs-audio-player を参考に、曲リストは別ファイルに作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* CONSTANTS */ exports.audioData = []; for( var i=1; i <= 29; i++ ){ let track = {}; track.title = 'Track' + zeroPadding(i,2); track.url = 'https://shop.homemadegarbage.com/alexa/Track' + zeroPadding(i,2) + '.mp3'; exports.audioData.push(track); }; function zeroPadding(number, length){ return (Array(length).join('0') + number).slice(-length); } |
音楽ファイルはこのブログの XSERVER にアップしましたが、skill-sample-nodejs-audio-player ではsoundcloudのフィードから取得しているようですね。
(そんなことできるんだ。それだとサーバも必要なく音楽配信スキル作成できちゃいますね。)
テスト
Alexa Developer Console の「テスト」を有効にし、テキスト(チャット形式)でテストできる
AudioPlayer のテストは出来ない
Alexa Developer Console の「テスト」はオーディオ再生に対応していないので、実際の音楽再生は実機で試していく必要がありました。
その他補足
AudioPlayer ではヘルプは機能しない
再生中に「アレクサ、ヘルプ」と言うと、アレクサのヘルプが返って来てしまいます。どうしたものかと思いましたが、アレクサの音楽再生でも同様だったのでひとまずヘルプは搭載しないままにしておきました。
審査でも大丈夫でした。
公開
公開タブをクリックし、情報を入力していく。
特に利用者の情報を収集するものでもないのでプライバシーポリシーは空欄で、また利用規約も設定なしにしました。
プライバシーとコンプライアンス
「テストの手順」には簡単な機能の内容を記載しました。
※公開後のフィードバック時にも、ここに修正内容を記載していく形になります。
公開範囲
設定
上記をすべて保存すると「設定」タブの検証画面へ進みます。
実行し、問題なければ「機能テスト」も同様に実行します。
両方クリアすると申請が可能になります。
申請
動作確認をして、申請。
ちょっと心配だったのは下記の点でした。
- DynamoDB には保存していないので、一時停止 > 再開 のときも「シャッフル再生します」と開始させていること
=> 機能説明にも、「すべてシャッフル再生」であることを明記 - ループ再生や最初からもう一度といった機能は対応していない
=> 「ループ再生」など言われたら「その機能はありません、ごめんね」と返している
フィードバック1
金曜夜に申請したのですが、週明け月曜日の午前中にフィードバックが来ました。早い!
内容は「知的財産権」の確認でした。
知的財産権の権利者からの確認状または適用されるライセンスの写し
が必要とのことだったので、自作曲の場合はどのような書類が必要なのかお問い合わせフォームから質問させて頂きました。
すると翌日に、自作曲の場合は Youtube や Website のリンク等をスキルの詳細な説明またはテストの手順に明記で大丈夫とのお返事を頂きました。
修正して早速再申請。
フィードバック2
再申請した夕方には第二のフィードバックを頂きました。(やっぱり早い)
1 キャンセルで音楽が止まらない
キャンセルで音楽が止まらないとのことで、ソースを確認したところストップさせるところ(.addAudioPlayerStopDirective())が何故か間違ってコメントアウトされていたので元に戻し、申請チェックリスト でテストして問題ないことを確認してから、2回目の最新性を行いました。
.addAudioPlayerStopDirective()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const ExitHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'IntentRequest' && (request.intent.name === 'AMAZON.CancelIntent' || request.intent.name === 'AMAZON.StopIntent'); }, handle(handlerInput) { return handlerInput.responseBuilder .addAudioPlayerStopDirective() .speak(STOP_MESSAGE) .getResponse(); }, }; |
2 ホームカードの表記について
こちらは補足として頂いたご指摘でしたが、ホームカードが英語で表記されており、ユーザーエクスペリエンス向上のため日本語表記を検討くださいとのことでした。
ホームカードはテストで実装していたものだったので、一旦すべてコメントアウトしました。
というのも、キューに入れて連続再生している中でホームカードの表示させかたがわからなかったからです。「次へ」「再生」ではインテントをきっかけにホームカードを送れるのですが、リクエストだとAlexaにアクションさせられません。
やり方がわかったら実装したいです。
上記修正して2度めの再申請。
公開
翌日の夕方に「Alexaスキル welcome脳の音楽 が公開されました!」とのメールが!🙌
10/20 に申請して、週末をはさみ 10/24に公開となりました。
お父ちゃんに教えて貰ったので早く出来たし必要最低限の機能ではありますが、alexaスキルは思っていたより作成しやすく面白かったです。
「新しい音楽配信ツールとして」Alexaスキルの可能性も感じます。
今後も随時曲を追加したり、機能をアップデートしたりもしていきたいと思います。