AlexaスキルのVUIをより良いものにするtips

[Alexa Advent Calendar 2018](https://adventar.org/calendars/2975?fbclid=IwAR1NSaGRT3OpDuT-OM6Y7ymzSmB3oYtmYt4-NRJgLwuNaTW2DaCrOVG8aLQ)の12月11日を担当しますnorippyです。
以前は技術についてのblogをやっていたのですが、advent calendarを機に再度書き始めることにしました。今回はQiitaを使った初めての投稿をさせていただきます。

私は普段Alexaスキルをメイン、ちょこっとClovaスキルの開発をやっていて、スキルの開発は今年の3月くらいから始めました。
今までにいくつものスキルを公開し、ありがたいことに[Alexa skill award 2018](http://www.rino-make-fun.com/2018/08/09/alexa-skill-%E3%81%8A%E3%81%88%E3%81%8B%E3%81%8D%E3%81%8A%E3%81%AD%E3%81%88%E3%81%95%E3%82%93/)のファイナリスト選出や[LINE BOOT AWARDS 2018](http://www.rino-make-fun.com/2018/11/13/clova-friends-%E3%83%AD%E3%83%9C%E3%83%83%E3%83%88%E5%8C%96%E3%83%8F%E3%83%BC%E3%83%89-%E3%83%AD%E3%83%9C%E3%82%B9%E3%83%BC%E3%83%84/)ではclovaを使ったスキルとハードウェアでエンジニア部門賞をいただくことができました。

そんなスマートスピーカースキル、特にAlexa skillを開発する上でVoice　 User Interfaceのクオリティを高めるTips,サンプルコードを紹介したいと思います。

サンプルコードはlambdaを使い、Node.jsで行う前提のものになります。
また、本職はハードウェアエンジニア。趣味でソフトウェアの開発をしているので、コードが汚いのはご了承ください。

# 複数のセリフを用意する
VUIは複数のセリフを用意して、任意に選んで発話させることが可能です。
こうする事で、毎回起動した時の飽きがないように工夫することができます。
恥ずかしながら、スキルを開発し始めた頃、自分は最初こんなことができるのを知らなくて、毎回同じ事しか話さないスキルを作った記憶があります。

```randomSpeechTextSample.js
//セリフを配列で準備する
const LAUNCH_MESSAGE = [
    '起動しました',
    'ん？呼んだ？',
    'スキルを起動してくれてありがとう'
]

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
         //ここで読み上げるセリフを選ぶ
        const speechText = LAUNCH_MESSAGE[Math.floor(Math.random() * LAUNCH_MESSAGE.length)];
        return handlerInput.responseBuilder
            .speak(speechText)
            .getResponse();
    }
};
```
これを応用すると、問題文と問題の答えが含まれる多次元配列を作れば、クイズスキルを作ることもできたりします。

# 時間や曜日等情報を活用する
普段の生活において、朝話しかける時は”おはよう”、昼に話しかけたら”こんにちは”等挨拶しますよね。Alexaスキルでもいつ起動しても同じ事を話すようでは味気ないです。
Alexaでも同じように時間や曜日によって話す内容を変えてあげましょう。

```sample1.js

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    async handle(handlerInput) {
        const date = new Date();
        const hours = date.getHours();
        const dayOfWeek = date.getDay();	// 曜日(数値)
        const dayOfWeekStr = ["日", "月", "火", "水", "木", "金", "土"][dayOfWeek];

        var greet = '';
        if (hours >= 5 && hours < 11) {
            greet = 'おはよう';
        } else if (hours >= 11 && hours < 17) {
            greet = 'こんにちは';
        } else {
            greet = 'こんばんは';
        }

        var speechOutput = ""
        speechOutput += greet + 'ダイビングログブックです。';

        if (dayOfWeekStr == '土' || dayOfWeekStr == '日') {//土日
            speechOutput += 'あら？ダイビングに行ってきたんですか？いいですね。'
        } else {
            speechOutput += '今日は' + dayOfWeekStr + '曜日。週末は海ですか？楽しみですね!'
        }
        speechOutput += 'それでは、何かご用件はございますか？'
        return handlerInput.responseBuilder
            .speak(speechOutput)
            .reprompt('何かご用件はございますか？') 
            .getResponse();
    }
};
```
この時、Lambda側で環境変数を設定します。日本で使う事を想定しているので、ここではタイムゾーンを"Asia/Tokyo"とします。これで日本時間に対応して発話してくれます。

![lambdaの設定](https://qiita-image-store.s3.amazonaws.com/0/324560/a27672d5-eb7b-463f-a2ab-5a26ef299705.png)

# 感嘆詞やSSMLを活用する
Alexaにはより自然な発話をし表現を豊かにする語句やフレーズが用意されています。
”おはよう”等も含まれるのですが、ただテキストを入力するだけではその機能を使うことはできず、**SSMLタグを追加する必要があります**。
また、会話の中での発話の”間”や”強調”といった事もSSMLタグを追加することで実現できます。
[感嘆詞のリファレンス](https://developer.amazon.com/ja/docs/custom-skills/speechcon-reference-interjections-japanese.html)
[SSMLのリファレンス](https://developer.amazon.com/ja/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)

先ほどのサンプルにSSMLをタグを追加してみます。

```sample2.js
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';//handerinputなので、いろんな値が取れる。別にインテントにこだわる必要はなく、条件に合わせてhandlerを呼びだすことができる。
    },
    async handle(handlerInput) {
        console.log("ここにきました")
        const date = new Date();
        const hours = date.getHours();
        const dayOfWeek = date.getDay();	// 曜日(数値)
        const dayOfWeekStr = ["日", "月", "火", "水", "木", "金", "土"][dayOfWeek];

        var greet = '';
        if (hours >= 5 && hours < 11) {
            greet = '<say-as interpret-as="interjection">おはよう</say-as><break time="500ms"/>';
        } else if (hours >= 11 && hours < 17) {
            greet = '<say-as interpret-as="interjection">こんにちは</say-as><break time="500ms"/>';
        } else {
            greet = '<say-as interpret-as="interjection">こんばんは</say-as><break time="500ms"/>';
        }

        var speechOutput = ""
            speechOutput += greet + 'ダイビングログブックです。<break time="300ms"/>';

            if (dayOfWeekStr == '土' || dayOfWeekStr == '日') {//土日
                speechOutput += 'あら？<break time="500ms"/>ダイビングに行ってきたんですか？いいですね。'
            } else {
                speechOutput += '今日は' + dayOfWeekStr + '曜日。週末は海ですか？楽しみですね!<break time="500ms"/>'
            }
            speechOutput += 'それでは、何かご用件はございますか？'
            return handlerInput.responseBuilder
                .speak(speechOutput)
                .reprompt('何かご用件はございますか？') // これがあることによって会話が終了しない
                .getResponse();
    }
};
```
いい感じに間が入ったことで、より聴きやすいスキルになりました。

# Customer Profile APIを活用する
最近のAlexa skill kitの更新により、ユーザーの情報が簡単に取得できるようになりました。
例えば名前はスキルの中で一番使いやすく、よりユーザーとAlexaの距離を近づけることができる要素の一つではないかと考えています。
Customer Profile APIでは、スキルを使用するユーザーがAlexaアプリのカードから使用許可すると、簡単に値を取得することができるようになります。また1度許可をすれば、ユーザーが許可を切らない限り、情報を取得できるので、次回起動時に改めて確認をとるといったことをする必要もありません。
Customer Profile APIに関してはClassmethodさんの記事が参考になります

[[日本語 #Alexa ] スキルからユーザーの名前やメールアドレス、携帯電話番号が取得できるようになりました](
https://dev.classmethod.jp/voice-assistant/amazon-alexa/using-alexa-new-customer-profile-api-from-sdk/)

この記事を参考に、サンプルコードにCustomer Profile APIを追加してみます。
注意としては、コードだけではなく、Alexa developer consoleからスキルのアクセス権限の項目の設定をする必要があります。


```sample3.js
// カスタマープロファイルにアクセスするためのパーミッション
const PERMISSIONS = ["alexa::profile:given_name:read"];
// const PERMISSIONS = ["alexa::profile:name:read"]

//alexa::profile:given_name:read -> ファーストネーム
//alexa::profile:name:read -> フルネーム


const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    async handle(handlerInput) {
        const date = new Date();
        const hours = date.getHours();
        const dayOfWeek = date.getDay();	// 曜日(数値)
        const dayOfWeekStr = ["日", "月", "火", "水", "木", "金", "土"][dayOfWeek];

        var greet = '';
        if (hours >= 5 && hours < 11) {
            greet = '<say-as interpret-as="interjection">おはよう</say-as><break time="500ms"/>';
        } else if (hours >= 11 && hours < 17) {
            greet = '<say-as interpret-as="interjection">こんにちは</say-as><break time="500ms"/>';
        } else {
            greet = '<say-as interpret-as="interjection">こんばんは</say-as><break time="500ms"/>';
        }

        var speechOutput = ""
        try {
            //ユーザー情報を取得
            const upsServiceClient = handlerInput.serviceClientFactory.getUpsServiceClient();
            const username = await upsServiceClient.getProfileGivenName(); // 名前(ファーストネーム)の取得
            // const username = await upsServiceClient.getProfileName();//フルネームの取得
            speechOutput += username + 'さん、' + greet + 'ダイビングログブックです。<break time="300ms"/>';

            if (dayOfWeekStr == '土' || dayOfWeekStr == '日') {//土日
                speechOutput += 'あら？<break time="500ms"/>ダイビングに行ってきたんですか？いいですね。'
            } else {
                speechOutput += '今日は' + dayOfWeekStr + '曜日。週末は海ですか？楽しみですね!<break time="500ms"/>'
            }
            speechOutput += 'それでは、何かご用件はございますか？'
            return handlerInput.responseBuilder
                .speak(speechOutput)
                .reprompt('何かご用件はございますか？') // これがあることによって会話が終了しない
                .getResponse();

        } catch (error) {
            if (error.name == 'ServiceError') {
                console.log('ERROR:' + error.statusCode + ' ' + error.message);
            }
            speechOutput += greet + '<break time="300ms"/>';
            //永続化情報の取得
            let attributes = await handlerInput.attributesManager.getPersistentAttributes()
            console.log("ログの中身")
            console.log(attributes);
            if (attributes == undefined) {
                speechOutput += '初めまして。ダイビングログブックです。<break time="300ms"/>このスキルでは最後にエントリーした情報を管理することができます。'
            } else {//純粋に
                speechOutput += 'ダイビングログブックです。<break time="300ms"/>'
            }
            speechOutput += 'ユーザー情報を登録すると少しだけ、楽しくなります。ぜひアレクサアプリのカードから許可をしてくださいね。<break time="500ms"/>'

            speechOutput += "それでは、何かご用件はございますか？"
            return handlerInput.responseBuilder
                .speak(speechOutput)
                .withAskForPermissionsConsentCard(PERMISSIONS)
                .reprompt('何かご用件はございますか？')
                .getResponse();
        }
    }
};
```

ファーストネームのやり方はクラメソさんのページにも書いていないので、こちらを参考にしてもらえればと思います。

そして、Customer　Profile APIを使う = ユーザー情報を取得するです。
リリースする際は、個人情報の取り扱いに注意してください。またリリース時はプライバシーポリシーURLが必須となります。

# Amazon Pollyを活用する
10月末、[Amazon Polly](https://aws.amazon.com/jp/polly/)をAlexaスキルでも使うことができるようになりました。
このPollyを使うことで、今まで声色は１つしか無かったものが、３種類に増えました！
特に男性の声が使えるようになったことで、VUIデザインで重要なスキルのキャラクター性の幅が広がっています。
私も早速Pollyを使い、[音リモコン](https://www.amazon.co.jp/rino-products-%E9%9F%B3%E3%83%AA%E3%83%A2%E3%82%B3%E3%83%B3/dp/B07KRBKFFJ/ref=sr_1_1?s=digital-skills&ie=UTF8&qid=1544502936&sr=1-1&keywords=%E9%9F%B3%E3%83%AA%E3%83%A2%E3%82%B3%E3%83%B3)というスキルをリリースしています。このスキルでは全ての発話にPollyを使っているため、よく聞く標準の声は一切出てきません。
サンプルコードは以下のようになります。

```pollySample.js
const S_TAKUMI = '<voice name="Takumi"><prosody rate="105%" pitch="-25%">' //声質を変えるために、rateやpitchを追加
const E_TAKUMI = '</prosody> </voice>'

const LAUNCH_MESSAGE = '音リモコンを起動した。キミの命令でディーティーエムエフの信号を出すのが私の仕事。早速命令を！'

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speechText = S_TAKUMI + LAUNCH_MESSAGE + E_TAKUMI;
        const repromptText =  S_TAKUMI + 'さぁ、私に早く命令を！' + E_TAKUMI;

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(repromptText)
            .getResponse();
    }
};
```
# 効果音を使う
Alexaは喋るだけではなくmp3を再生することができます。ただ喋るだけではなく、クイズスキル等、正解したら効果音を出すようにすると、よりユーザーにとって面白い体験を提供することができます。
実はAmazonさんがAlexa向けに[Alexa Skills Kitサウンドライブラリ](https://developer.amazon.com/ja/docs/custom-skills/ask-soundlibrary.html)というものを公開しており、任意のタイミングで音を出すことが可能になっています。

```soundLibSample.js
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speechText = '<audio src="https://s3.amazonaws.com/ask-soundlibrary/human/amzn_sfx_crowd_applause_01.mp3"/>' + "これはサンプルです。";

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};
```

また、自分で作成した効果音を出力したいという事もあると思います。
Alexa関連のページを見ると、だいたいAWS S3を利用しているかと思います。
ですが、S3は使用した分お金がかかるので(最初の１年は無料だったと思います)、なかなか音を使ったり、マルチモーダル化に踏み出せない方もいるのではないかと思います。

一応自分も最初S3でやっていたのですが、以前Alexaスキルのハッカソンで無料で十分使えるサービスを教えてもらいました。

[ニフティクラウド mobile backend](https://mbaas.nifcloud.com/)

ニフクラのmbaasを使うと、APIリクエスト数100万回まで無料で使うことができるので、すごいアクセス数が多いスキルでなければ基本的に無料の範囲内に収まります。もちろん、証明書が取れているので、問題なくスキル上で使用することができます。
使い方も簡単で、
1.新しいアプリをクリックして、適当な名前のアプリを作る
2.ファイルストアにスキルで使用する画像やmp3ファイルをアップロードする
3.ファイルを公開するようにして、URLを取得
4.soundLibSample.jsで示したように、バックエンドのコード上にURLを追加する
これだけで使えるようになります。
<img width="1248" alt="スクリーンショット 2018-12-11 16.50.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/324560/c9c5244c-f47d-991e-55b4-79a113f7d06a.png">

2019/6/20
最近リリースしたスキルを開発する中で発見したのですが、
1回のリクエストに最大で5つのmp3しか再生することができません。6つ以上のmp3再生命令が入るとエラーが帰ってきて、再生されません。なるべくまとめられるところは纏める工夫が必要です。
これは自分が用意したmp3だけではなく、Amazonが提供してくれている効果音も含みます。


# 最後に
恐縮ながら、自分が今まで開発する中でVUIをどうしたらより良くできるのか、それに対する解としての手法を紹介させていただきました。
皆さんのスキルクオリティ向上に役立ててもらえればと思います。