見出し画像

Alexaの心音再生スキルを開発して公開するまで【アレクサ解説】

こんにちは、心臓フェチのヒンメリです。
今回は表題の通り、Amazon Alexaのスキル開発を行ってみたので、その内容を記事にします。

本記事の要点を3行で
1. Alexaで心音を任意の時間再生するスキルを開発しました。
2. スキルの開発~公開まで、初心者でも簡単にできます。条件を満たしたスキルを公開すると、毎月100ドル分のAWSクレジットがもらえます(2020年5月現在)。
3. ソースコードを載せつつ、音楽再生や多言語対応などといった
 ちょっと踏み込んだ内容についても言及します。

本記事の対象読者
・アレクサについてちょっと詳しくなりたい!という方
・AWSを使って何かアプリケーションを開発してみたいという方
・心臓フェチ、心音フェチ、音フェチ、ASMR好きの人

上記の制作物(無料のスキル)を元に、Alexaの基本的なことから、ソースコード解説まで幅広く解説しますので、下記目次より必要な部分をつまみ読みいただければと思います。

自己紹介

心臓フェチのHimmeli(ヒンメリ)と申します。心臓が好きすぎて、フェチのために生きることにした人間です。どのくらい心臓が好きかは、下記の記事かTwitterを見てもらえたらご理解いただけるかと思います。

一応エンジニアですが、このフェチの活動のために日本やエストニア共和国に会社を起業し、生活費を削りながらフェチのサービスを自前で開発・運用しています。サービスの企画・開発以外にも、フェチを語る会といったイベントや、記事執筆やプロモーション活動など色々やっています。ニッチな方を取材するメディア「Nichっち」さんに面白い記事や動画にしていただいたりもしているので、ご興味があればこちらもどうぞ。

★フェチを語る会で使う、フェチの可視化ワークショップ資料↓

画像9

フェチを語る会 - Rooters様コラボ - ワークショップ資料 - Google スライド -  (2)

★エストニアでの起業方法や、税制についての解説記事も寄稿しています↓

現在に至るまでに、自前で暗号通貨(仮想通貨)を開発したり、全ての会社資産及び個人資産の合計約4300万円を詐欺られてしまったり、鬱になって元々努めていた会社を辞めることになってしまったりと色々ありましたが、一応なんとか心臓は動かし続けています。この辺りについては、近いうちに、業界の裏話やいい話も悪い話もすべて含めて事実をダーっと記した記事にできたらと思っていますので、もしご興味があればまたお読みいただけたら幸いです。(あと、もう貯蓄がないので副業OKな転職先を探しています……。いいところがあれば教えて下さい……)

肝心のフェチのサービスについては、いつも下記のように説明しています。

私は、人種・性別・国籍に関係なく、誰もが理想のフェチコンテンツを購入/販売できる社会の実現を目指して、フェチのオンラインマーケットサービス「fetiquette」の開発を行っています。
もっと具体的に言うと、
・「心臓病の人が、自身の心音を心臓フェチに販売して治療費を得られる」
・「身体障がいを持つ方が、自身の写真集を欠損フェチに販売して義手の製作費用を稼げる」

といったように、フェチを通じてユーザーの個性に価値を付与して提供・社会的貢献ができるようなサービスを開発しています。

fetiquette事業紹介v1-3p.pdf - Adobe Acrobat Pro DC 2020-05-13 03.44.26

fetiquette事業紹介v1-4p.pdf - Adobe Acrobat Pro DC 2020-05-13 03.45.28

簡単に言うと、フェチのメルカリみたいなサービスです。現在、サービスの使いやすさ向上を目的としたリニューアル作業中。私の活動全般や、fetiquetteのサービス開発などに興味がある方は、気軽にお声がけくださいね。

Alexa・Alexaスキルとは

Alexaとできること _ Amazon - Google Chrome 2020-05-12 23.13.33

Alexa(アレクサ)は、Amazonが開発しているAIアシスタントのことです。そのAIが搭載されたスマートスピーカーが、Amazon EchoやAmazon dotといった製品です。その最大の特徴は音声認識による周辺機器、周辺アプリケーションとの連携および操作です。「アレクサ、ただいま」と言ったらリビングの電気がついて、スピーカーからサカナクションの曲が流れて……といったことを実現できるわけですね。詳細は公式サイトからどうぞ。

Alexaとできること _ Amazon - Google Chrome 2020-05-12 23.19.08

この周辺アプリケーションのことをAlexa Skill(アレクサスキル)といい、ユーザが自前で開発することができるものになっています。本記事ではそのスキルの開発について記載しています。どんなスキルがあるかは下記から見られます。

スキルの概要、開発理由

私が開発したスキル「心音タイマー」はその名前の通り、心音を指定した時間再生してくれるシンプルなスキルです。

スキル起動後、「アレクサ、心音を○分流して」といった風に呼びかけることで、指定の時間の心音を再生します。リラックスしたい時や、ちょっと変わったタイマーを利用したい場合などにどうぞ。ASMRがお好きな方にも。

開発の理由は以下の4つです。
1. 私の周りで夜眠れない、という方が多かったから
 (心音を聴くと眠れる、という方は結構いらっしゃるようです)
2. 心臓フェチとして、心臓にまつわるアプリを作ってみたかったから
3. AlexaというAIが我々人類に心音を聴かせてくれる、というシチュエーションがエモくていいなと思ったから
4. Alexaスキル開発者には、Amazonが提供するクラウドサービス(AWS)で使えるクレジット(無料クーポン)がプレゼントされるから

です。あとは単純に勉強のためですね。
4. のクレジットについてはあまり知られていないようですが、AWSを使ってサービス開発をしている方にはとても有用なプロモーションです。

Alexaスキルの開発者は、AWSプロモーションクレジットにお申し込みいただくと、アマゾン ウェブ サービス (AWS) の利用が月100ドル分無料になるクーポン(AWSプロモーションクレジット)を受け取ることができます。
スキルを公開している開発者の皆様には、このAWS無料利用枠に加えて、100ドル分のAWSプロモーションクレジットをご用意いたしました。また、スキルの開発でAWS使用料が発生した場合は、さらに毎月100ドル分のプロモーションクレジットを受け取ることができます。是非ご活用ください。

要約すると、最初に100ドル分もらえ、さらに条件を満たせば毎月100ドルのクレジットがもらえる、ということです。年間にするとサーバ代が12万円分くらい浮くので、ギリギリの生活をしている私のような人間にとってはとても助かります。詳細は下記よりどうぞ。

調べてみると、Googleさんの方でも似たようなプロモーションがあるようですね。Google社製の製品をお持ちの方はぜひチェックしてみてください。

スキルの機能、開発環境

さて、ここからかなり技術色が強くなっていきますが、できるだけ初心者の方でも読みやすいように意識して書いていきますね。

スキル開発にあたって、どんな機能を実装する必要があるかをざっくりと考えておく必要があります。私は開発にあたって以下くらいの内容を考えていました。

■対象ユーザ
・夜、眠れない大人
・夜泣きする赤ちゃん
・子供を含む、心音が好きな人

■基本機能(対象ユーザを意識して仕様を決める)
・起動:起動時に操作方法が分かるように提示してあげる
・再生:時間指定がなくてもデフォルトで長めに再生する
・終了:眠っている人のためにアラームは鳴らさず静かに停止する

■付加価値(他のアプリとの差別化ポイントを決める)
・心音の種類を選択可能:好きな心音で寝付きがよくなる人もいる。
 あと心臓フェチ的に不整脈の心音やバクバク心音は外せない
・多言語対応:夜に眠れないのは日本人だけではない

これを、Alexaの機能に落とし込んでいきました。
ありがたいことに、Alexaのスキルを初めて開発する方向けに公式の丁寧なブログが用意されていますので、どんな機能があるのかということを考えつつ下記の通りに手を動かしてみると良いと思います。このトレーニング通りにやるだけで、基本機能を搭載したスキルは作ることができます。

なお、開発には以下を使用しています。
・言語:Node.js
・データベース:DynamoDB (NoSQL)
・サーバ:AWS Lambda(サーバレス)
・ストレージ:AWS S3

開発の流れ、用語、設定

基本の流れは上記のAlexaスキルトレーニングを参照してもらうとして、まずここでは全体の概要をつかめるように用語と大枠を説明します(特殊な用語が多く、私も最初に躓いたのでそこを優先して解説します)。
開発に使う画面は、大きく分けて2種類あります。

1. Alexa Developer Console
Alexaのスキル名、Alexaが認識する対話の設定、スキルの動作確認など、Alexaの機器に依存する部分を設定したり動作させたりする画面です。

画像4

この画像では、HeartIntentというインテントを設定しています。インテントとは、ユーザの発話に応じて行うアクションの単位であり、「ゆっくりな心音 を 10分 ながして」といった発話に対応する部分の設定をここで行っていることになります。具体的には、「ゆっくりな心音 を 10分 ながして」という発話をAlexaが認識したら、「heartrate」変数に「ゆっくりな心音」を、「duration」変数に「10分」を代入して、HeartIntent用に定義したアクション(関数)を起動する、ということを定義しています。

画像5

続いて、HeartRateというスロットを設定しています。スロットとは、上記のheartrateやdulationといった変数を扱いやすくするための入れ物だと考えてください。私の対応したい「heartrate」(心音の種類)には、「早い心音」「遅い心音」「不整脈心音」などがあり、それらの同意語をここで設定しています。「ゆっくりな心音」という発話をAlexaが認識した際に、それは遅い心音として扱おうね、という設定をしていることになります。AMAZON.DURATIONは時間を管理するために公式が用意したスロットです。「新垣結衣」を「女優」として認識してくれるような人名をまとめたスロットなど、用途に応じて様々な種類があります。

2. AWS Management Console (Lambda, DynamoDB, S3)
上記インテントが起動した際のアクションを定義する際に使用するのがこちらの画面です。

画像6

上記の画面はLambdaを表示しています。ここでは、各関数別に動作確認(テスト)を行うこともできます。なので、開発の流れとしては、
1: Alexa Developer ConsoleでAlexaの設定を行う
2: Lambdaで関数を記載する
3: Lambaで関数の動作確認を行う
4: Alexaで音声入力を含めた動作確認を行う
5: スキルの審査、申請へ

といったイメージになります。

なお、Alexaスキルトレーニングでは、スキル作成時に「Alexa-Hosted」という無料枠付きテンプレート設定を指定していますが、開発に拡張性を持たせたり、100ドルのクレジットの獲得を狙う場合には「ユーザ定義のプロビジョニング」を利用する方が良いでしょう。本記事でもそうしています。

画像7

この場合は、Alexa Developer Console と Lambdaの両方でエンドポイント(使用する関数の宛先)の設定が必要になります。下記が参考になります。

また、各種テストについては下記の記事が参考になります。

【用語の補足】
AWS Lambda
何かのイベントが発生した際に動作し、必要な処理を行ってその処理結果を返してくれるクラウドコンピューティングサービスです。通常、何かサービスを提供する際には、情報の処理を行うサーバがあります。そのサーバはずっと起きたまま情報の処理依頼が来るのを待っています。常に依頼が来るならそれでもいいかもしれませんが、暇な時間がある場合などは、サーバ代が無駄に消費されてしまいます。そのような場合にLambdaを使うと、処理が必要な場合にのみLambdaが動作するため、必要最小限のコンピュータ資源の消費で済みます。この仕組みを利用するとサーバレスで固定費のかからないサービスを実現することもできるわけです。

DynamoDB:従来のデータベース(RDB)よりも拡張性に優れたNoSQLデータベースサービスですRDBだとデータベース設計をしっかり行って、各データとの整合性をきっちり管理する必要がありますが、DynamoDBの場合はとりあえずデータ放り込んでおく、ということができます。ただし、その分、放り込まれたデータの集計には不向きなところがあります。Alexaの開発においては、Alexa-Skill-Kit(ASK)というパッケージを利用するのですが、基本的にDynamoDBを利用するように作られています。なのでこだわりがなければ、DynamoDBでよいかと思います。

心音タイマーでは、DynamoDBに「このユーザはいつまで、どの心音を再生する」という情報を保存しています。スキルにてDynamoDBと接続する場合、Alexa-Skill-Kit(ASK)を標準のものからVersion2にアップデートしてあげた方が何かと便利なので、下記対応を行っておきましょう。

AWS S3:「Amazon Simple Storage Service」の略称で、クラウド上でストレージ機能を提供するサービスです。心音タイマーの開発においては、再生する心音ファイルをこのS3に保存しています。S3は非常に便利なストレージサービスで、ここにHTMLファイルを置くだけで、静的なページであればそれでWebサイトを公開することだってできてしまいます。

ソースコード

さて、いよいよ各コードの詳細について見ていきましょう。いきなりコードを見ても何がなんだか分からないと思うので、まずは前述のAlexaスキルトレーニングを実装してみて、そのコードを編集して仕上げていくといった形式をオススメします。
また前提として、私もまだ2つ目のスキルの開発に取り掛かったくらいの初心者なので、間違ったことを記載している可能性もあります。ご容赦ください。変数名の時点から、スネークケースとキャメルケースが混ざっていたりします……スミマセン(´;ω;`)

各種定義
DynamoDBを使う場合、前項で用意したASK V2のインポートを忘れないようにしましょう。また、コメントに記載している通り、各種処理に必要なSkillBuildersの宣言をAlexa.SkillBuilders.standard();で行うようにします。

// ASK V2のインポート
const Alexa = require('ask-sdk');

// DynamoDBとの接続のため、SkillBuildersをcustom()からstandard()に変更
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
   .addRequestHandlers(
       LaunchRequestHandler, // 起動時に呼ばれる処理:使い方説明など
       HeartIntentHandler, // 心音再生をお願いしたときに呼ばれる処理
       HelpIntentHandler,
       CancelAndStopIntentHandler, // スキル終了時に呼ばれる処理
       SessionEndedRequestHandler,
       PauseIntentHandler,
       ResumeIntentHandler,
       AudioPlayerEventHandler, // 音源再生時に呼ばれる処理
   )
   //.withAutoCreateTable(true) // 手動でテーブルを作成している場合、不要です
   .withTableName("alexa_heart_timer") // DynamoDB プライマリキーは"id"
   .addErrorHandlers(ErrorHandler)
   .addRequestInterceptors(LoadPersistentAttributesRequestInterceptor)
   //.addResponseInterceptors(SavePersistentAttributesResponseInterceptor)
   .lambda();

後半に用意している addRequestInterceptors、addResponseInterceptorsは、Alexaが各インテントの処理を行った前 / 後に追加で何か処理を入れたい場合に使用します。初期化処理とかが必要であれば利用するとよいと思います。心音タイマーでは、S3にある音源リストを読み込む必要があります。「アレクサ、心音タイマーで心音をきかせて」などで起動した場合、LaunchRequestHandlerを飛ばして、いきなりHeartIntentHandlerに入ります。この両方のHandlerの中で読み込み処理を行ってもいいですが、折角なので addRequestInterceptors で前処理としてLoadPersistentAttributesRequestInterceptor関数を呼び、リストの読み込みを行うようにしてみました。

var audioDataList = [];
async function createAudioList() {
   audioDataList = require('track.json');
}

// 前処理:音源を用意
const LoadPersistentAttributesRequestInterceptor = {
    async process(handlerInput) {
        // 該当するAlexa端末と紐づくデータを取得
        const PersistentAttributes = await handlerInput.attributesManager.getPersistentAttributes();

        // audioListを生成
        if (audioDataList.length === 0) {
            if (PersistentAttributes.audioDataList) {
                // 既に対象音源、再生終了予定時刻などが保存されていたらそれを読み込む
                audioDataList = PersistentAttributes.audioDataList;
             } else {
                // 完全に新規なら jsonファイルから情報を読み込む
                await createAudioList();
             }
         }
         // DBにデータが何もない場合に属性値を初期化
         if (Object.keys(PersistentAttributes).length === 0) {
             handlerInput.attributesManager.setPersistentAttributes({
                 playingTitle: '',
                 playingUrl:'',
                 endtime:'',
                 audioDataList // 読み込んだ情報を設定しておく
             });
         }
    },
};
// track.json の中身
[ 
 {"title" : "normals.mp3",
   "url": "https://*****/normals.mp3"},
 {"title": "fasts.mp3",
   "url": "https://*****/fasts.mp3"},
 {"title": "slows.mp3",
   "url": "https://*****/slows.mp3"},
 {"title": "irres.mp3",
   "url": "https://*****/irres.mp3"}
]

DBの属性読み込み周りは、デフォルトだと簡略化され過ぎていて不安になります。私はこれが嫌で、現在開発・審査中のスキル(みんなで自分の好きなフェチに投票し、集計できるスキル)では下記のようにAWS.DynamoDB.DocumentClient() の put や query 関数を使って明示的にDB操作を行うようにしています。参考までにどうぞ。

/*
   DynamoDB:alexa_fetish_questionnaireテーブルに、フェチ投票情報を保存する関数
   先に setFetishCounter() でカウンタ情報をセットしてから本関数を使用する
   引数: handlerInput, 保存するデータ(String)
   return: 保存時のログ
*/
async function saveData(handlerInput, stringData) {
   var docClient = new AWS.DynamoDB.DocumentClient();
   var now = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
   var params = {
       TableName: 'alexa_fetish_questionnaire',
       Item:{
           id: handlerInput.requestEnvelope.context.System.device.deviceId,
           created: now.getTime(),
           fetish: stringData
       }
   };
   return await docClient.put(params, function(err, data) {
       if (err) console.log("saveData(): " + err);
       else console.log("Put to alexa_fetish_questionnaire: " + JSON.stringify(data));
   });
}

多言語対応
下記のように、各種テキストメッセージを配列にして[日本語,英語]の順で定義しています。とりあえず英語だけの対応です。

// 英語のロケール 英語だけで5地域もあります
const EN_LOCALE = ['en-US', 'en-AU', 'en-CA', 'en-GB', 'en-IN'];

// メッセージ 
const HELP_MESSAGE = ["心音を指定した分数(ふんすう)だけ再生します。「アレクサ、心音を5分聞かせて」「アレクサ、ゆっくりな心音を15分」などお声がけください。","This skill play heartbeat in a specified number of minutes. Please say something like, Alexa, let me hear heartbeat for five minutes, or, Alexa, slow heartbeat for 15 minutes."];
const LAUNCH_MESSAGE = ["心音タイマーを起動しました。おやすみ時(じ)などに心音はいかがですか?", "I activated the heartbeat timer. Would you like to hear heartbeat when you go to sleep?"];
const REPROMPT_MESSAGE = ["「アレクサ、心音を流して」「アレクサ、ゆっくりな心音を15分」などお声がけください。","You can say something like, Alexa, start your heartbeat, or, Alexa, slow your heartbeat for 15 minutes."];
const START_MESSAGE_ACCEPT = ["",""];
const START_MESSAGE_DURATION = ["再生します。"," for you."];
const STOP_MESSAGE = ["終了します。","Finish the playback."];
const ERROR_MESSAGE = ["その機能には対応していません。", "Sorry, I cannot support that feature."];

// 音源タイプ(全ての音源は10sの長さ)
const HEART_NORMAL = ["心音","Normal heartbeat"];
const HEART_FAST = ["早い心音","Fast heartbeat"];
const HEART_SLOW = ["遅い心音","Slow heartbeat"];
const HEART_IRR = ["不整脈心音","Irregular heartbeat"];
// 多言語対応のためにLocaleを取得する関数
function getLocaleId(handlerInput) {
    let locale = handlerInput.requestEnvelope.request.locale;
    let locale_id;
    if( locale === 'ja-JP') {
        locale_id = 0; // 日本語
    } else if (EN_LOCALE.includes(locale)) {
        locale_id = 1; // English
    }
    else{
        locale_id = 1; // English
    }
    return locale_id;
}

// 利用例
// const speakOutput = STOP_MESSAGE[getLocaleId(handlerInput)];

詳しい対応についてはこちらを参照ください。

LaunchRequestHandler

// スキル開始処理
const LaunchRequestHandler = {
   canHandle(handlerInput) {
       return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
   },
   handle(handlerInput) {
       const speakOutput = LAUNCH_MESSAGE[getLocaleId(handlerInput)];
       return handlerInput.responseBuilder
           .speak(speakOutput)
           .reprompt(speakOutput)
           .getResponse();
   }
};

   /*
   return時のセッションについてのメモ
       repromptありの場合...セッションが継続される (ShouldEndSessionがfalseの扱い)
       repromptなしの場合...セッションが終了される (ShouldEndSessionがtrueの扱い)
       .withShouldEndSession(bool)といった形式でも指定が可能
   */

シンプルですね。下部のメモにも書いていますが、レスポンスに .reprompt() を設定することで、Alexaとの対話がそのまま継続します。このため、起動後にそのまま「心音を5分」など発言することで次のインテントに進むことができます。

HeartIntentHandler
本スキルのメインの処理部分です。長いですが、コメントを多めに入れているので、読んでいっていただければなんとなく分かるかと思います。

// メイン処理:「アレクサ、ゆっくりな心音を15分聞かせて」などに対応
const HeartIntentHandler = {
   canHandle(handlerInput) {
       return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
           && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HeartIntent';
   },
   handle(handlerInput) {
       // heartrateを読み込み デフォルトでは「心音」=普通の心音
       var heartrate;
       if (handlerInput.requestEnvelope.request.intent.slots.heartrate.value === undefined || handlerInput.requestEnvelope.request.intent.slots.heartrate.resolutions.resolutionsPerAuthority[0].status.code === 'ER_SUCCESS_NO_MATCH') {
         if (getLocaleId(handlerInput) === 0) {
           heartrate = '心音';
         } else {
           heartrate = 'Normal heartbeat';
         }
       } else {
            heartrate = handlerInput.requestEnvelope.request.intent.slots.heartrate.resolutions.resolutionsPerAuthority[0].values[0].value.name;
       }
       console.log(heartrate);

       // durationを読み込み デフォルトでは「15分」
       var time = handlerInput.requestEnvelope.request.intent.slots.duration.value;
       if (time === undefined) {
           time = 'PT15M' // durationスロットの仕様で、「15分」は「PT15M」と解釈される
       }

       // audioデータを読み込み
       const audio = audioInit(heartrate, getLocaleId(handlerInput));

       // 再生終了予定時刻を計算
       let endtime = calculateEndtime(convertTimeSpeech(time));

       // 再生情報を保存
       savePlayinfo(handlerInput, audio, endtime.getTime());

       var speakOutput;
       if (getLocaleId(handlerInput) === 0) {
         // 発話例「心音を5分再生します」
         speakOutput = heartrate + 'を' + convertTimeSpeech(time) + START_MESSAGE_DURATION[0];
       } else {
         // 発話例「I play heartbeat for five minutes for you.」
         speakOutput = heartrate + 'for' + convertTimeSpeech(time) + START_MESSAGE_DURATION[getLocaleId(handlerInput)];
       }
       return handlerInput.responseBuilder
           .speak(speakOutput)
           .addAudioPlayerPlayDirective('REPLACE_ALL', audio.url, audio.title, 0, null)
           .getResponse();
   }
};

以下はユーティリティの関数の部分です。

/*
   DynamoDBに再生情報を保存する関数
   引数: handlerInput, audioオブジェクト, endtime(Unix time stamp)
   return: -
*/
async function savePlayinfo(handlerInput, audio, endtime) {
   let attributes = await handlerInput.attributesManager.getPersistentAttributes();
   attributes.playingTitle = audio.title;
   attributes.playingUrl = audio.url;
   attributes.endtime = endtime;
}

/*
   再生する音源の情報を読み込む関数
   引数: (HeartRate)スロットの標準名, Locale Id
   return: audioオブジェクト
*/
function audioInit(heartrate, localeId) {
   var no = 0;
   switch (heartrate) {
     case HEART_NORMAL[localeId]:
       no = 0;
       break;
     case HEART_FAST[localeId]:
       no = 1;
       break;
     case HEART_SLOW[localeId]:
       no = 2;
       break;
     case HEART_IRR[localeId]:
       no = 3;
       break;
     default:
       no = 0;
   }
   
   let audioData = {};
   audioData.title = audioDataList[no].title;
   audioData.url = audioDataList[no].url;
   return audioData;
}
/*
 AMAZON.DURATION形式を発話形式時間に変換する関数
 引数: AMAZON.DURATION形式(ex. PT10M)
 return: 発話形式時間(ex. 10分)
*/
function convertTimeSpeech(time) {
   time = time.replace("PT", "");
   time = time.replace("S", "秒");
   time = time.replace("M", "分");
   time = time.replace("H", "時間");
   return time
}

/*
 タイマーの設定後の時間を計算する関数
 引数: 発話形式時間(ex. 10分)
 return: Date型終了時刻(ex. 現在時刻+10分)
*/
function calculateEndtime(dulation) {
   // JSTの現在時刻を取得
   let date = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));

   console.log("current time: " + date);
   if (dulation.includes("時間")) {
       let cut_str = '時';
       let index = dulation.indexOf(cut_str);
       let hour = dulation.substring(0, index);
       date.setHours(parseInt(date.getHours())+parseInt(hour));
       dulation = dulation.slice(index + 2);
   }
   if (dulation.includes("分")) {
       let cut_str = '分';
       let index = dulation.indexOf(cut_str);
       let min = dulation.substring(0, index);
       date.setMinutes(parseInt(date.getMinutes())+parseInt(min));
       dulation = dulation.slice(index + 1);
   }
   if (dulation.includes("秒")) {
       let cut_str = '秒';
       let index = dulation.indexOf(cut_str);
       let sec = dulation.substring(0, index);
       date.setSeconds(parseInt(date.getSeconds())+parseInt(sec));
   }
   
   return date;
}

AudioPlayerEventHandler
音楽再生部分です。私は音源周りでかなり詰まりました。
音源再生時は、
 ・再生開始時
 ・終了時
 ・一時停止時
 ・終了が近づいて来た時
に、それぞれ "AudioPlayer"からはじまるリクエストが来るので、その内容を解析して処理を行っています。心音タイマーの場合、音源再生終了が近づいてきたら、終了予定時刻に達しているかどうかを判定し、達していない場合は次も同じ音源を読み込みさせる、という内容になっています。

// 音源再生処理 * Consoleのインターフェース設定にてAudioPlayerをOnにする必要あり
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':
       break;
     case 'PlaybackFinished':
       // Audio再生時に .speak させることはできないっぽい?です
       break;
     case 'PlaybackStopped':
       break;
     case 'PlaybackNearlyFinished': // 再生終了が近づいてきた際の処理
       {
         let attributes = await handlerInput.attributesManager.getPersistentAttributes();
         const expectedPreviousToken = attributes.playingTitle;

         // タイマー時間まで現在の音源をループ再生
         let current_time = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
         if (current_time.getTime() <= attributes.endtime) {
           const audioTitle = attributes.playingTitle;
           const audioUrl = attributes.playingUrl;
           responseBuilder.addAudioPlayerPlayDirective('ENQUEUE', audioUrl , audioTitle , 0, expectedPreviousToken);
         } else {
           responseBuilder.addAudioPlayerClearQueueDirective('CLEAR_ENQUEUED');
         }
       break;
       }
   }
   return responseBuilder.getResponse();
 },
};

こちらの方の記事も、音楽再生スキルの開発から公開までが丁寧に解説されているので、参照していただければと思います。

本スキルの主要なコードは以上です。その他の細かい部分は、スキルトレーニングで用意するテンプレートと変わりありません。

審査ポイントなど、技術以外で注意すべき点

ここが結構大事なポイントかも知れません。

Alexaの審査では、
1: そのスキルが公序良俗的に問題がない(ように対策されている)こと
2: アレクサの性格を勝手にイメージ付けるような発話がないこと
3: 音源、画像、説明分などに著作権や肖像権といった問題がないこと

などがチェックされます。
1: そのスキルが公序良俗的に問題がない(ように対策されている)こと
こちらは、例えばフェチ投票のスキルにおいては下記のような指摘をいただいています。

ポリシーに違反する内容(性的、暴力、宗教、民族性、および文化など)を含まないよう、フィルターをする、または、ユーザーの応答をすぐに反映させずに承認制するなどの対応が必要となります。テスト手順にフィルター、承認制などの方法とポリシーに違反する内容が含まれないよう対応されている旨を明記してください。

2: アレクサの性格を勝手にイメージ付けるような発話がないこと
これはブランディングの問題ですね。心音タイマーでは、アレクサが「私の心音をお聴かせしますね」と発話するようにしたかったのですが、下記のような指摘がなされることがあると伺ったため、無難に「心音を再生します」という発話に変更しました。

 Alexaのパーソナリティであるかのように振舞ったり、模倣したり、それらの行為によりユーザーに混乱を招く様なサードパーティー作成のスキルは認められません。

3: 音源、画像、説明分などに著作権や肖像権といった問題がないこと
ここは最も意識すべき点かもしれません。これについては下記の記事がとても参考になりました。

第三者の商標またはブランドがスキルの応答内容や、詳細ページなどに含まれている。情報自体に秘匿性がなくとも、特定の固有名詞が含まれている場合は、商標またはブランドの所有者からの許諾が無いと審査で却下されます。
例:スキル名や応答内容にに"モン○ター○ンター"が含まれていなくても、応答メッセージにゲーム内の固有名詞である"リオ○ウス"が含まれているとアウトです。

私の場合は、心音が私自身が録音したものである旨を、自身のTwitter/Youtubeに投稿しているURLと合わせて記載することでOKがでました。

テスト方法の書き方
審査にあたっては、Amazonの担当者がスキルの動作確認をするための「テスト方法」について記載する必要があります。こちらについては、過去の記事などを読んでもあまり記載例を挙げている方がいなかったので、私が記載し審査に通ったものをそのまま記載しておきます。

■起動確認
「アレクサ、心音タイマー」
「アレクサ、心音タイマーを開いて」
■起動後の再生確認
「心音を流して」→ 通常の心音が約15分間再生され終了
「心音を一分再生して」→ 通常の心音が約1分間再生され終了
「はやい心音を一分再生して」→ 早い心音が約1分間再生され終了
「おそい心音を一分再生して」→ 遅い心音が約1分間再生され終了
「乱れた心音を一分再生して」→ 不整脈心音が約1分間再生され終了
「コーヒーを入れて」→ 通常の心音が約15分間再生され終了(聞き取れなかった場合などは通常心音を15分再生する仕様)
■起動と同時に再生確認
「アレクサ、心音タイマーで心音を聞かせて」→ 通常の心音が約15分間再生され終了
「アレクサ、心音タイマーではやい心音を一分流して」→ はやい心音が約一分再生され終了
■再生の停止
(心音再生中に)「アレクサ、ストップ」→ 心音の再生が停止され終了

※心音タイマーは心音の再生を繰り返すことで擬似的なタイマーとしているため、きっちり正確な時間を測る目的のものではありません。あくまでリラックス、胎教、睡眠導入などのためのスキルであり、その旨を説明文にも記載しています。なお、使用している心音はすべて作者本人の心音です。
下記の心音を編集してペースを変更して使用しています。
https://youtu.be/JgVs2-YgBws

基本的には、作成した Intent(アクション)ごとに起動例と起動結果を記載すれば良さそうです。

スキルを公開してから

スキルを公開すると、そのスキルのレポートを Alexa Developer Consoleから確認できるようになります。まだ私はSNSでも公開告知などを何もしていないのですが、それでも公開初日に10件、その後1日あたり3~5件くらいずつスキルを有効化していただいているようです。うれしい。

画像12

画像11

そして、有効化を行ったら、忘れずにAWSの$100クレジットプロモーションに申請しておきましょう。私はまだ審査中ですが、知り合いの方は音楽再生スキルで審査が通ったようです。

謝辞

本スキルの開発にあたって、てんでん さん(人形フェチ、マネキンフェチ、欠損フェチ、食人フェチの方)に相談にのっていただきました。本当にありがとうございました。

てんでん さんは、食品の賞味期限を管理するスキルとバトルビート用の音楽再生スキルを公開していらっしゃるので、もし興味がある方はこちらも有効化してみてくださいね。

この記事を読んでくださった皆様も、もし良かったらスキルの開発にチャレンジしてみてくださいね。私は先に述べた通り、「フェチ投票:あなたと同じフェチの人は全国に何人?」という、自分のフェチに投票できるスキルを開発・審査中です。こちらも審査が通りましたら、また別途記事にいたしますね。

追記:審査通りました!

それでは、また次の記事でお会いしましょう。


*****

私はすべての記事を無料で公開しています。活動を応援してくださる方は、サポート機能やサークル参加などをご検討いただけたら幸いです。サークル参加者専用のSlackコミュニティなどの特典があります(まだほとんど人がいませんが……)。いただいた応援は、イベント開催やフェチコンテンツ制作に使用させていただきます。

******

この記事が参加している募集

おうち時間を工夫で楽しく

フェチでお悩みの方は、いつでもお気軽にご相談ください。フェチで悩む者同士、助け合っていけたら良いですね。 すべての記事を無料で公開していますので、もし宜しければ、サークル参加やサポートなどでご支援いただけたら幸いです。イベント開催やフェチコンテンツ制作に使用させていただきます。