使用Alexa技能开发包(ASK)来实现AudioPlayer技能(NodeJS版本)
这次我们将讲解如何使用Alexa技能播放自己的MP3音乐。
背景 – 背景信息
我在亚马逊的促销活动中,以999日元的价格买到了Echo Dot和Amazon Music Unlimited(一个月的使用权)。从现在开始,我将过着沉浸在音乐中的家庭生活!
“Alexa!播放我的最爱歌单!”
哎呀?你不能播放自己的MP3吗?
中国市场上有适用于Alexa的音乐服务。
在Alexa上可用的音乐服务基本上都是付费的,买了才意识到真是太愚蠢了…
-
- Amazon Music (Prime | Unlimited)
-
- iTunes Music
-
- Spotify (Premium)
-
- dヒッツ
- うたパス
我使用Amazon Prime,但是想听的歌还是不够啊。
因为我只是偶尔使用AI音箱,没有计划订阅音乐服务,所以很困扰呢……
让我们创建Alexa技能吧!(简介篇)
在这里,我们将解释Alexa技能的前提知识。如果您不需要这些知识,请跳过阅读并阅读实施部分。
ASK是什么?
在Alexa中,功能以“技能”作为单位提供,用户可以自由选择和添加。
用户也可以开发和发布自己的技能,整合平台是Alexa技能套件(ASK)。
有非常友好的教程和参考资料,可以毫不犹豫地进行开发(太棒了!)
请注意,Alexa SDK目前处于2.x版本,但因网络上有许多关于1.x版本的文章,容易引发混淆。
Alexa技能的配置
Alexa技能可以分为两种大类。
如果需要大量访问或需要大量资源的技能,出于费用考虑,用户将选择使用自己提供的技能;而不是这种情况的大多数技能将使用标准的“Alexa Hosted Skill”。
以下是用于Alexa Hosted Skill的组件。
可以将Amazon Echo替换为智能手机上的Alexa应用程序。
在开发中的重要点有以下三个。
-
- Alexaへのインテント(ユーザがしゃべる内容)を決める
-
- Lambdaで実行されるスキル(任意のスクリプト)を実装する
- データの持ち回り方(どのデータをS3に置くか)を決める
不一定需要将需要持续存在的数据(如MP3等)放置在S3上,但必须满足以下要求。
-
- HTTPSアクセスできること(Lambdaの要件)
- プライベートアクセスできること(著作権等法令の要請)
对于这一点,使用S3就不需要考虑太多,所以很方便!顺便提一下,Lambda和S3会自动分配给ASK创建的技能,并适用于AWS的免费使用额度。
由于技能在云端的Lambda上执行,无法处理本地或内网环境的数据。为了将数据放置在云端,运营技能时需要十分注意安全性。
ASK控制台的配置
在ASK的Web控制台上,您可以完成从技能开发、调试到发布的一整套操作。
开发
-
- スキルテンプレートからスキルの生成
-
- Alexaへのインテントの実装(GUI)
-
- Lambdaで実行されるスキルの実装(コーディング)
- S3のデータ操作(GUI)
调试
-
- Webコンソールでのデバッグ
- 開発中のスキルの実機動作デバッグ
稍后我会解释,但AudioPlayer需要在实际设备上进行调试。
公开
- Amazonによる審査・公開
让我们来制作Alexa技能吧!(实施部分)
从这里开始,我们将解释Alexa技能的实施。
Alexa技能的需求
这次我们将开发符合以下要求的技能。
-
- ユーザが指定したプレイリストを再生する
-
- 曲はMP3形式で、S3に格納する
-
- 音楽プレイヤーの要件(一時停止・再開・次へ・前へ)を満たす
- 極力シンプルに(少ない仕組みで)実装する
实现意图(用户说的内容)
意图将在使用ASK的GUI中进行实现。
由于是无代码编程,所以只要看着教程操作就不会有困难。
意图是对技能的指令。
亚马逊提供的标准意图和用户定义的意图两种。
等等。无法通过标准意图实现的指令,用户可以自由定义。
播放列表意图
{playlist} を再生して
在中文中,{playlist}的部分被称为插槽,可以事先指定候选项,例如”胡子组合”或”国王之夜”。
在候选槽中,可以为值(用户所说内容)分配唯一的ID。
(在此实现中,稍后将在技能方面使用)
技能的实施
技能将在ASK上进行编码。
标准支持NodeJS和Python,但这次选择了具有丰富记录和参考资料的NodeJS。
Lambda的文件结构如下。
lambda/
index.js : スキルを実装するファイル
util.js : S3にアクセスするユーティリティが定義されたファイル
# 以下は今回のスキル用に追加
playlists/
プレイリスト名.json : プレイリストをJSON形式で格納する
在播放列表中,您需要使用数组来定义存储在S3中的MP3文件的键值(而不是URL)。
您可以通过查看在S3存储的文件,来了解键值的格式是类似于Media/xxx.mp3的形式。
技能的触发(开始请求)
首先,我们会启动技能。
如果你进行教程,你会明白,技能就是为了实现与Alexa的请求相对应的处理程序。
const LaunchRequestHandler = {
// (1)
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
// (2)
handle(handlerInput) {
const speakOutput = 'どのプレイリストを再生しますか?';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
(1)
在 canHandle 方法中,会选择相应的请求。
Alexa 请求可以分为以下两种类型。
LaunchRequest:スキルの起動
IntentRequest:スキルに対する命令
(2)
在handle方法中,我们实现技能的处理过程和对Alexa的响应。
我们使用responseBuilder来构建响应,使用speak让Echo说话,使用reprompt让Echo等待用户的下一条指令。
启动音乐播放器(IntentRequest)
从已选播放列表中播放音乐。
// ファイルヘッダで読み込み
const Util = require('./util.js');
const PlaylistIntentHandler = {
// (1)
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlaylistIntent';
},
handle(handlerInput) {
// (2)
const resolvedSlot = handlerInput.requestEnvelope.request.intent.slots.playlist.resolutions.resolutionsPerAuthority[0].values;
if (resolvedSlot === undefined) {
return handlerInput.responseBuilder
.speak('プレイリストが検索できませんでした。')
.getResponse();
}
const file = resolvedSlot[0].value.id;
const playlist = require(`./playlists/${file}.json`);
// (3)
const track = playlist[0];
const url = Util.getS3PreSignedUrl(track);
const token = [ file, track ].join(':');
return handlerInput.responseBuilder
.addAudioPlayerPlayDirective('REPLACE_ALL', url, token, 0, null)
.getResponse();
}
};
(1)
在IntentRequest的处理程序中,我们选择了相应的意图。
在这里,我们选择了我自己定义的PlaylistIntent。
(2)
用户选择的槽位将被存储在intent.slots.{槽位}.resolutions下面。
本次根据用户选择的播放列表,将获取playlists/{播放列表}.json文件。
我希望以日语给予槽位候选项(用户说的内容)不同的称呼,但为了将播放列表的文件名设置为英文字母的唯一名称,我使用了附加在槽位候选项上的ID进行处理。
使用addAudioPlayerPlayDirective指令,响应Alexa的AudioPlayer播放音乐的指示。
请按照以下顺序为addAudioPlayerPlayDirective函数提供参数。
playBehavior:AudioPlayerは曲をキューで管理します。REPLACE_ALLはキューをクリアしてセットしなおします。
url:曲のURLです。S3のファイルキーからgetS3PreSignedUrlで取得した時限性URLを指定しています。
S3 Presigned URLとは、プライベートなファイルへのアクセスを一時的に可能とする仕組みで、認可の複雑さを回避しつつファイルのセキュリティを確保することができます。
ASKのデフォルト実装では60秒間のみ公開するよう実装されています。
token:再生中の曲を操作するためのトークンです。曲ごとに一意になる必要があります。次節で解説します。
offsetInMilliseconds:曲の再生開始ポイントです。最初から再生するときは0で、一時停止から再開するときは任意のミリ秒になります。
expectedPreviousToken:キューに次の曲を追加する場合に、前の曲のトークンを指定することで意図した順序で曲が追加されることを保証します。ここでは不要です。
还有其他的audioItemMetadata,用于指定在Echo Show等屏幕上显示的封面和歌曲名称。在Echo Dot上则不需要。如果想要追求到这个程度,会相当麻烦。
只需一种选择,以下是对原文的中文本地化改写:
在Web控制台进行调试时,只会收到“不支持”的消息,而实际上音乐不会播放。一旦确认调用无误,接下来就可以使用实际设备进行调试了。
转移再生状态
在播放列表开始时,只需要播放第一首歌曲即可,但是如果想播放下一首或前一首歌曲,则需要判断当前正在播放哪个播放列表的哪首歌曲。这需要在多个Alexa请求之间传递信息。
在数据存储的迁移中,可以考虑以下三种选项。
トークンは自由にセットできるため、プレイリストと曲の情報を保持できます。〇セッション前述の
reprompt
でユーザ入力を受け付ける場合など、Alexaスキルはセッションを利用して前リクエストの状態を保持します。しかし、AudioPlayerを応答する場合はセッションを利用することができません。×S3(永続化)再生中のプレイリストと曲をS3に一時保存します。Alexa SDKにS3永続化するためのAPIが用意されています。
S3へのアクセス数が増えるのと、単純に仕組みが複雑になり面倒です。△
所以,为了简单实现,我们将利用传递给AudioPlayer的令牌。
令牌指定了{播放列表}:{曲目的S3键值}。(仅为了满足本次要求的实现。)
如果要支持随机播放,将需要数据存储,但是本次不处理此功能。
(因为如果仅通过Math.random从播放列表中获取歌曲,会导致同一首歌多次播放)
下一首歌的播放(AudioPlayer.PlaybackNearlyFinished)
当正在播放的歌曲结束后,会播放下一首歌曲。
可以通过从AudioPlayer发送的请求来检测正在播放的歌曲是否结束。
当曲播放完毕时,将会触发AudioPlayer.PlaybackFinished请求,而当曲即将结束时,将会触发AudioPlayer.PlaybackNearlyFinished请求,从而可以在结束前准备好下一曲。
const PlaybackNearlyFinishedHandler = {
// (1)
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'AudioPlayer.PlaybackNearlyFinished';
},
handle(handlerInput) {
// (2)
const AudioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
const t = AudioPlayer.token.split(':');
const played = { file: t[0], track: t[1] };
const playlist = require(`./playlists/${played.file}.json`);
const cursor = playlist.indexOf(played.track);
const track = cursor === playlist.length - 1 ? playlist[0] : playlist[cursor + 1];
// (3)
const url = Util.getS3PreSignedUrl(track);
const token = [ file, track ].join(':');
return handlerInput.responseBuilder
.addAudioPlayerPlayDirective('REPLACE_ENQUEUED', url, token, 0, null)
.getResponse();
}
};
(1) 实现AudioPlayer.PlaybackNearlyFinished的处理程序。
从AudioPlayer对象中获取令牌,然后获取播放列表的下一首歌曲。
(3)
与初始播放时一样,使用addAudioPlayerPlayDirective将歌曲添加到队列中。
同时,如果将playBehavior设置为ENQUEUE,那么应该需要提供expectedPreviousToken。
如果使用S3的预签名URL,即使将下一首歌曲及其后续的URL设置到队列中,由于无法访问,所以无法使用ENQUEUE。如果队列中只有一首歌曲,实际上REPLACE_ALL和REPLACE_ENQUEUED没有什么区别。
IntentRequest的AMAZON.NextIntent处理程序也可以以相同的方式实现,但是我认为如果playBehavior不设置为REPLACE_ALL,那么歌曲将无法结束。
暂停和继续(AMAZON.PauseIntent和AMAZON.ResumeIntent)
从这里开始只说重点。
一時停止
AMAZON.PauseIntentをハンドルしてaddAudioPlayerStopDirectiveを応答します。
再開
AMAZON.ResumeIntentをハンドルしてaddAudioPlayerPlayDirectiveで曲を開始します。
その際offsetInMillisecondsにAudioPlayerから取得した現在のoffsetInMillisecondsをセットすることで曲の途中から開始できます。
对于Echo Show和智能手机的兼容性
在Echo Show和智能手机上,需要支持”屏幕触摸操作”。
屏幕触摸操作将作为来自”播放控制器的请求”通知给技能。
虽然处理程序的实现方式基本上没有改变,但需要注意以下几点。
PlaybackController.PlayCommandIssuedが最初からの再生と一時停止からの再生を兼ねる
一時停止は基本的にスキルを経ないでクライアント側で行われる
总结
通过使用Alexa技能套件,最基本必要的组件自动装备好,可以轻松进行开发。
最初我尝试用Python开发,但由于参考文档不完善且示例较少,感到困难重重。而NodeJS则有丰富的参考文档,没有遇到太大困扰。
另外,在Web控制台上进行调试时,错误分析很难,还有一些繁琐的部分,比如无法启动AudioPlayer。(最初并不知道CloudWatch的存在,甚至在没有日志的情况下进行调试……)
只将个人音乐文件保存在S3上,不要公开技能,只需自己享受乐趣!